mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-28 23:42:21 +00:00
Merge branch 'master' of github.com:chai2010/advanced-go-programming-book
This commit is contained in:
commit
b776a437bb
@ -4,7 +4,7 @@
|
||||
|
||||

|
||||
|
||||
- 作者:柴树杉 (chai2010, chaishushan@gmail.com)
|
||||
- 作者:柴树杉 (chai2010, chaishushan@gmail.com), 曹春晖 (cch123, https://github.com/cch123)
|
||||
- 网址:https://github.com/chai2010/advanced-go-programming-book
|
||||
|
||||
## 在线阅读
|
||||
|
@ -34,12 +34,9 @@
|
||||
* [第四章 RPC和Protobuf](ch4-rpc/readme.md)
|
||||
* [4.1. RPC入门](ch4-rpc/ch4-01-rpc-intro.md)
|
||||
* [4.2. Protobuf](ch4-rpc/ch4-02-pb-intro.md)
|
||||
* [4.3. 玩转RPC(TODO)](ch4-rpc/ch4-03-netrpc-hack.md)
|
||||
* [4.4. GRPC入门(TODO)](ch4-rpc/ch4-04-grpc.md)
|
||||
* [4.5. GRPC进阶(TODO)](ch4-rpc/ch4-05-grpc-hack.md)
|
||||
* [4.6. Protobuf扩展语法和插件(TODO)](ch4-rpc/ch4-06-pb-option.md)
|
||||
* [4.7. 其它RPC系统(TODO)](ch4-rpc/ch4-07-other-rpc.md)
|
||||
* [4.8. 补充说明(TODO)](ch4-rpc/ch4-08-faq.md)
|
||||
* [4.3. 玩转RPC](ch4-rpc/ch4-03-netrpc-hack.md)
|
||||
* [4.4. GRPC入门](ch4-rpc/ch4-04-grpc.md)
|
||||
* [4.5. 补充说明](ch4-rpc/ch4-05-faq.md)
|
||||
* [第五章 Go和Web](ch5-web/readme.md)
|
||||
* [5.1. Web开发简介](ch5-web/ch5-01-introduction.md)
|
||||
* [5.2. Router请求路由](ch5-web/ch5-02-router.md)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# 附录C:作者简介
|
||||
|
||||
- [柴树杉(网络ID:chai2010)](https://github.com/chai2010) 国内第一批Go语言爱好者,创建了最早的QQ讨论组和golang-china邮件列表,组织 [Go语言官方文档](https://github.com/golang-china) 和 [《Go语言圣经》](https://github.com/golang-china/gopl-zh) 的翻译工作,Go语言代码的贡献者。目前在[青云QingCloud](https://www.qingcloud.com/)从事开源的多云应用管理平台[OpenPitrix](https://github.com/openpitrix/openpitrix)开发工作。
|
||||
- [cch123](https://github.com/cch123): TODO
|
||||
- [曹春晖(网络ID:cch123)](https://github.com/cch123) 在 web 领域工作多年,开源爱好者。对大型网站系统的架构和相关工具的实现很感兴趣,并且有一些研究成果。目前在滴滴平台技术部工作。
|
||||
|
@ -177,7 +177,7 @@ s1 := "hello, world"[:5]
|
||||
s2 := "hello, world"[7:]
|
||||
```
|
||||
|
||||
和数组一样,内置的`len`和`cap`函数返回相同的结果,都对应字符串的长度。也可以通过`reflect.StringHeader`结构访问字符串的长度(这里只是为了演示字符串的结构,并不是推荐的做法):
|
||||
字符串和数组类似,内置的`len`函数返回字符串的长度。也可以通过`reflect.StringHeader`结构访问字符串的长度(这里只是为了演示字符串的结构,并不是推荐的做法):
|
||||
|
||||
```go
|
||||
fmt.Println("len(s):", (*reflect.StringHeader)(unsafe.Pointer(&s)).Len) // 12
|
||||
|
@ -244,14 +244,14 @@ func main() {
|
||||
mu.Lock()
|
||||
go func(){
|
||||
println("你好, 世界")
|
||||
mu.Unock()
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
mu.Lock()
|
||||
}
|
||||
```
|
||||
|
||||
可以确定后台线程的`mu.Unock()`必然在`println("你好, 世界")`完成后发生(同一个线程满足顺序一致性),`main`函数的第二个`mu.Lock()`必然在后台线程的`mu.Unock()`之后发生(`sync.Mutex`保证),此时后台线程的打印工作已经顺利完成了。
|
||||
可以确定后台线程的`mu.Unlock()`必然在`println("你好, 世界")`完成后发生(同一个线程满足顺序一致性),`main`函数的第二个`mu.Lock()`必然在后台线程的`mu.Unlock()`之后发生(`sync.Mutex`保证),此时后台线程的打印工作已经顺利完成了。
|
||||
|
||||
## 初始化顺序
|
||||
|
||||
|
@ -66,7 +66,7 @@ func CopyFile(dstName, srcName string) (written int64, err error) {
|
||||
}
|
||||
```
|
||||
|
||||
上面的代码虽然能够工作,但是隐藏一个bug。如果第一个`os.Open`调用失败,那么会在没有释放`src`文件资源的情况下返回。虽然我们可以通过在第二个返回语句前添加`src.Close()`调用来修复这个BUG;但是当代码变得复杂时,类似的问题将很难被发现和修复。我们可以通过`defer`语句来确保每个被正常打开的文件都能被正常关闭:
|
||||
上面的代码虽然能够工作,但是隐藏一个bug。如果第一个`os.Open`调用成功,但是第二个`os.Create`调用失败,那么会在没有释放`src`文件资源的情况下返回。虽然我们可以通过在第二个返回语句前添加`src.Close()`调用来修复这个BUG;但是当代码变得复杂时,类似的问题将很难被发现和修复。我们可以通过`defer`语句来确保每个被正常打开的文件都能被正常关闭:
|
||||
|
||||
```go
|
||||
func CopyFile(dstName, srcName string) (written int64, err error) {
|
||||
|
@ -152,7 +152,7 @@ DATA ·Name+8(SB)/8,$6
|
||||
|
||||
因为在Go汇编语言中,go.string."gopher"不是一个合法的符号,我们无法手工创建(这是给编译器保留的部分特权,因为手工创建类似符号可能打破编译器输出代码的某些规则)。因此我们新创建了一个·NameData符号表示底层的字符串数据。
|
||||
|
||||
然后定义·Name符号为两个16字节,其中前8个字节用·NameData符号对应的地址初始化,后8个字节为常量6表示字符串长度。
|
||||
然后定义·Name符号内存大小为16字节,其中前8个字节用·NameData符号对应的地址初始化,后8个字节为常量6表示字符串长度。
|
||||
|
||||
通过以下代码测试输出Name变量:
|
||||
|
||||
@ -172,7 +172,7 @@ func main() {
|
||||
pkgpath.NameData: missing Go //type information for global symbol: size 8
|
||||
```
|
||||
|
||||
提示汇编中定义的NameData符号没有类型信息。其实Go汇编语言中定义的数据并没有所谓的类型,每个符号只不过是对应一个内存而且。出现这种错误的原因是,Go语言的垃圾回收器在扫描NameData变量的时候,无法知晓该变量内部是否包含指针。因此,真正错误的原因并不是NameData没有类型,二是NameData变量没有标注是否会含有指针信息。
|
||||
提示汇编中定义的NameData符号没有类型信息。其实Go汇编语言中定义的数据并没有所谓的类型,每个符号只不过是对应一块内存而已。出现这种错误的原因是,Go语言的垃圾回收器在扫描NameData变量的时候,无法知晓该变量内部是否包含指针。因此,真正错误的原因并不是NameData没有类型,而是NameData变量没有标注是否会含有指针信息。
|
||||
|
||||
通过给NameData变量增加一个标志,表示其中不会包含指针数据可以修复该错误:
|
||||
|
||||
@ -193,9 +193,9 @@ var NameData [8]byte
|
||||
var Name string
|
||||
```
|
||||
|
||||
我们将NameData声明为长度为8的字节数组。因为编译器可以通过类型分析出该变量不会包含指针,因此汇编代码中可以NOPTR标志信息。
|
||||
我们将NameData声明为长度为8的字节数组。编译器可以通过类型分析出该变量不会包含指针,因此汇编代码中可以省略NOPTR标志。
|
||||
|
||||
在这个实现中,Name字符串底层其实引用的是NameData内存对应的“gopher”字符串数据。因此,如果NameData发生变化的化,Name字符串的数据也会跟着变化的。
|
||||
在这个实现中,Name字符串底层其实引用的是NameData内存对应的“gopher”字符串数据。因此,如果NameData发生变化,Name字符串的数据也会跟着变化。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@ -218,7 +218,7 @@ DATA ·Name+8(SB)/8,$6
|
||||
DATA ·Name+16(SB)/8,$"gopher"
|
||||
```
|
||||
|
||||
在新的结构中,Name符号对应的内存从16字节变为24字节,多出的8个字节用户存放底层的“gopher”字符串。·Name符号前16个字节依然对应reflect.StringHeader结构体:Data部分对应`$·Name+16(SB)`,表示数据的地址为Name符号往后偏移16个字节的位置;Len部分依然对应6个字节的长度。
|
||||
在新的结构中,Name符号对应的内存从16字节变为24字节,多出的8个字节存放底层的“gopher”字符串。·Name符号前16个字节依然对应reflect.StringHeader结构体:Data部分对应`$·Name+16(SB)`,表示数据的地址为Name符号往后偏移16个字节的位置;Len部分依然对应6个字节的长度。
|
||||
|
||||
|
||||
## 定义main函数
|
||||
@ -245,7 +245,7 @@ TEXT ·main(SB), $16-0
|
||||
CALL runtime·printnl(SB)
|
||||
RET
|
||||
```
|
||||
`TEXT ·main(SB), $16-0`用于定义`main`函数,其中`$16-0`表示`main`函数的帧大小是16个字节(对应string头的大小,用于给`runtime·printstring`函数传递参数),`0`表示`main`函数没有参数和返回值。`main`函数内部通过调用运行时内部的`runtime·printstring(SB)`函数来打印字符串。然后调用runtime·printnl打印换行符号。
|
||||
`TEXT ·main(SB), $16-0`用于定义`main`函数,其中`$16-0`表示`main`函数的帧大小是16个字节(对应string头部结构体的大小,用于给`runtime·printstring`函数传递参数),`0`表示`main`函数没有参数和返回值。`main`函数内部通过调用运行时内部的`runtime·printstring(SB)`函数来打印字符串。然后调用`runtime·printnl`打印换行符号。
|
||||
|
||||
Go语言函数在函数调用时,完全通过栈传递调用参数和返回值。先通过MOVQ指令,将helloworld对应的字符串头部结构体的16个字节复制到栈指针SP对应的16字节的空间,然后通过CALL指令调用对应函数。最后使用RET指令表示当前函数返回。
|
||||
|
||||
@ -254,7 +254,7 @@ Go语言函数在函数调用时,完全通过栈传递调用参数和返回值
|
||||
|
||||
Go语言函数或方法符号在编译为目标文件后,目标文件中的每个符号均包含对应包的绝对导入路径。因此目标文件的符号可能非常复杂,比如“path/to/pkg.(*SomeType).SomeMethod”或“go.string."abc"”。目标文件的符号名中不仅仅包含普通的字母,还可能包含诸多特殊字符。而Go语言的汇编器是从plan9移植过来的二把刀,并不能处理这些特殊的字符,导致了用Go汇编语言手工实现Go诸多特性时遇到种种限制。
|
||||
|
||||
Go汇编语言同样遵循Go语言少即是多的哲学,它只保留了最基本的特性:定义变量和全局函数。同时为了简化Go汇编器的词法扫描程序的实现,特别引入了Unicode中的中点`·`和大写的除法`/`,对应的Unicode码点为`U+00B7`和`U+2215`。汇编器编译后,中点`·`会被替换为ASCII中的点“.”,大写点除法会被替换为ASCII码中的除法“/”,比如`math/rand·Int`会被替换为`math/rand.Int`。这样可以将点和浮点数中的小数点、大写的除法和表达式中的除法符号分开,可以简化汇编程序此法分析部分的实现。
|
||||
Go汇编语言同样遵循Go语言少即是多的哲学,它只保留了最基本的特性:定义变量和全局函数。同时为了简化Go汇编器的词法扫描程序的实现,特别引入了Unicode中的中点`·`和大写的除法`/`,对应的Unicode码点为`U+00B7`和`U+2215`。汇编器编译后,中点`·`会被替换为ASCII中的点“.”,大写的除法会被替换为ASCII码中的除法“/”,比如`math/rand·Int`会被替换为`math/rand.Int`。这样可以将中点和浮点数中的小数点、大写的除法和表达式中的除法符号分开,可以简化汇编程序词法分析部分的实现。
|
||||
|
||||
即使暂时抛开Go汇编语言设计取舍的问题,中点`·`和除法`/`两个字符的如何输入就是一个挑战。这两个字符在 https://golang.org/doc/asm 文档中均有描述,因此直接从该页面复制是最简单可靠的方式。
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
## 故意设计没有goid
|
||||
|
||||
根据官方的相关资料显示,Go语言刻意没有提供goid的原因是为了避免被滥用。因为大部分用户在轻松拿到goid之后,在之后的编程中会不自觉地编写出强依赖goid的代码。强依赖goid将导致这些代码不好移植,同时也会导致并发模型复杂化。同时,Go语言中可能同时存在海量的Goroutine,但是每个Goroutine合适被销毁并不好实时监控,这也会导致依赖goid的资源无法很好地自动回收(需要手工回收)。如果你是Go汇编语言用户,完全可以忽略这些借口。
|
||||
根据官方的相关资料显示,Go语言刻意没有提供goid的原因是为了避免被滥用。因为大部分用户在轻松拿到goid之后,在之后的编程中会不自觉地编写出强依赖goid的代码。强依赖goid将导致这些代码不好移植,同时也会导致并发模型复杂化。同时,Go语言中可能同时存在海量的Goroutine,但是每个Goroutine何时被销毁并不好实时监控,这也会导致依赖goid的资源无法很好地自动回收(需要手工回收)。如果你是Go汇编语言用户,完全可以忽略这些借口。
|
||||
|
||||
## 纯Go方式获取goid
|
||||
|
||||
@ -191,7 +191,7 @@ TEXT ·getg(SB), NOSPLIT, $32-16
|
||||
RET
|
||||
```
|
||||
|
||||
其中AX寄存器对应g指针,BX寄存器对应g结构体的类型。然后通过runtime·convT2E函数将类型转为接口。因为我们使用的不是是g结构体指针类型,因此返回的接口表示的g结构体值类型。理论上我们也可以构造g指针类型的接口,但是因为Go汇编语言的限制,我们无法`type·*runtime·g`标识符。
|
||||
其中AX寄存器对应g指针,BX寄存器对应g结构体的类型。然后通过runtime·convT2E函数将类型转为接口。因为我们使用的不是g结构体指针类型,因此返回的接口表示的g结构体值类型。理论上我们也可以构造g指针类型的接口,但是因为Go汇编语言的限制,我们无法`type·*runtime·g`标识符。
|
||||
|
||||
基于g返回的接口,就可以容易获取goid了:
|
||||
|
||||
@ -264,7 +264,7 @@ TEXT ·getg(SB), NOSPLIT, $32-16
|
||||
RET
|
||||
```
|
||||
|
||||
其中NO_LOCAL_POINTERS表示函数没有局部指针变量。同时对返回的接口进行零值初始化,初始化完成后通过GO_RESULTS_INITIALIZED告知GC。这样可以在保证栈分裂是,GC能够正确处理返回值和局部变量中的指针。
|
||||
其中NO_LOCAL_POINTERS表示函数没有局部指针变量。同时对返回的接口进行零值初始化,初始化完成后通过GO_RESULTS_INITIALIZED告知GC。这样可以在保证栈分裂时,GC能够正确处理返回值和局部变量中的指针。
|
||||
|
||||
|
||||
## goid的应用: 局部存储
|
||||
@ -316,7 +316,7 @@ func Delete(key interface{}) {
|
||||
}
|
||||
```
|
||||
|
||||
最后我们再提供一个Clean函数,用于是否Goroutine对应的map资源:
|
||||
最后我们再提供一个Clean函数,用于释放Goroutine对应的map资源:
|
||||
|
||||
```go
|
||||
func Clean() {
|
||||
@ -350,5 +350,5 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
通过Goroutine局部存储,不同层次函数之间可以共享存储资源。同时未来避免资源泄露,需要再Goroutine的根函数中,通过defer语句调用gls.Clean()函数释放资源。
|
||||
通过Goroutine局部存储,不同层次函数之间可以共享存储资源。同时为了避免资源泄漏,需要在Goroutine的根函数中,通过defer语句调用gls.Clean()函数释放资源。
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
# 3.8. 补充说明
|
||||
|
||||
得益于Go语言的设计,Go汇编语言的优势也非常明显:跨操作系统、不同CPU之间的用法也非常相似、支持C语言预处理器、支持模块。同时Go汇编语言也存在很多不足:它不是一个独立的语言,底层需要依赖Go语言甚至操作系统;很多高级特性很难通过手工汇编完成。虽然Go语言官方尽量保持Go汇编语言简单,但是汇编语言是一个比较大的话题,大到足以写一本Go汇编语言的教程。本章的目的是让大家对Go汇编语言简单入门,在看到底层汇编代码的时候不会一头雾水,在某些遇到性能或禁制的场合能够通过Go汇编突破限制。这只是一个开始,后续版本会继续完善。
|
||||
得益于Go语言的设计,Go汇编语言的优势也非常明显:跨操作系统、不同CPU之间的用法也非常相似、支持C语言预处理器、支持模块。同时Go汇编语言也存在很多不足:它不是一个独立的语言,底层需要依赖Go语言甚至操作系统;很多高级特性很难通过手工汇编完成。虽然Go语言官方尽量保持Go汇编语言简单,但是汇编语言是一个比较大的话题,大到足以写一本Go汇编语言的教程。本章的目的是让大家对Go汇编语言简单入门,在看到底层汇编代码的时候不会一头雾水,在某些遇到性能受限制的场合能够通过Go汇编突破限制。这只是一个开始,后续版本会继续完善。
|
||||
|
@ -82,7 +82,7 @@ func RegisterHelloService(svc HelloServiceInterface) error {
|
||||
}
|
||||
```
|
||||
|
||||
我们将RPC服务的接口规范分为三个部分:首选是服务的名字,然后是服务要实现的详细方法列表,最后是注册该类型服务的函数。为了避免名字冲突,我们在RPC服务的名字中增加了包路径前缀(这个是RPC服务抽象的包路径,并非完全等价Go语言的包路径)。RegisterHelloService注册服务时,编译器会要求传入的对象满足HelloServiceInterface接口。
|
||||
我们将RPC服务的接口规范分为三个部分:首先是服务的名字,然后是服务要实现的详细方法列表,最后是注册该类型服务的函数。为了避免名字冲突,我们在RPC服务的名字中增加了包路径前缀(这个是RPC服务抽象的包路径,并非完全等价Go语言的包路径)。RegisterHelloService注册服务时,编译器会要求传入的对象满足HelloServiceInterface接口。
|
||||
|
||||
在定义了RPC服务接口规范之后,客户端就可以根据规范编写RPC调用的代码了:
|
||||
|
||||
@ -101,7 +101,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
其中唯一的变化是client.Call的第一个参数用`HelloServiceName+".Hello"`代理了"HelloService.Hello"。然后通过client.Call函数调用RPC方法依然比较繁琐,同时参数的类型依然无法得到编译器提供的安全保障。
|
||||
其中唯一的变化是client.Call的第一个参数用`HelloServiceName+".Hello"代替了"HelloService.Hello"。然而通过client.Call函数调用RPC方法依然比较繁琐,同时参数的类型依然无法得到编译器提供的安全保障。
|
||||
|
||||
为了简化客户端用户调用RPC函数,我们在可以在接口规范部分增加对客户端的简单包装:
|
||||
|
||||
@ -125,7 +125,7 @@ func (p *HelloServiceClient) Hello(request string, reply *string) error {
|
||||
}
|
||||
```
|
||||
|
||||
我们在接口规范中针对客户端新增加了HelloServiceClient类型,改类型也必须满足HelloServiceInterface接口,这样客户端用户就可以直接通过接口对应的方法调用RPC函数。同时提供了一个DialHelloService方法,直接拨号HelloService服务。
|
||||
我们在接口规范中针对客户端新增加了HelloServiceClient类型,该类型也必须满足HelloServiceInterface接口,这样客户端用户就可以直接通过接口对应的方法调用RPC函数。同时提供了一个DialHelloService方法,直接拨号HelloService服务。
|
||||
|
||||
基于新的客户端接口,我们可以简化客户端用户的代码:
|
||||
|
||||
@ -175,14 +175,14 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
在新的RPC服务端实现中,我们用RegisterHelloService函数来注册函数,这样不仅可以避免服务名称的工作,同时也保证了传入的服务对象满足了RPC接口定义的定义。最后我们支持多个TCP链接,然后为每个TCP链接建立RPC服务。
|
||||
在新的RPC服务端实现中,我们用RegisterHelloService函数来注册函数,这样不仅可以避免命名服务名称的工作,同时也保证了传入的服务对象满足了RPC接口的定义。最后我们支持多个TCP链接,然后为每个TCP链接建立RPC服务。
|
||||
|
||||
|
||||
## 跨语言的RPC
|
||||
|
||||
标准库的RPC默认采用Go语言特有的gob规范编码,因此从其它语言调用Go语言实现的RPC服务将比较困难。在互联网的微服务时代,每个RPC以及服务的使用者都可能采用不同的编程语言,因此跨语言是互联网时代RPC的一个首要条件。得益于RPC的框架设计,Go语言的RPC其实也是很容易实现跨语言支持的。
|
||||
|
||||
Go语言的RPC框架有两个比较有特色的设计:一个是RPC数据打包时可以通过插件实现自定义的编码和解码;另一个是RPC建立在抽象的io.ReadWriteCloser接口之上的,我们可以将RPC架设在不同的通讯协议之上。我们这里将尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的PPC。
|
||||
Go语言的RPC框架有两个比较有特色的设计:一个是RPC数据打包时可以通过插件实现自定义的编码和解码;另一个是RPC建立在抽象的io.ReadWriteCloser接口之上的,我们可以将RPC架设在不同的通讯协议之上。这里我们将尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的PPC。
|
||||
|
||||
首先是基于json实现RPC服务:
|
||||
|
||||
@ -237,9 +237,9 @@ func main() {
|
||||
{"method":"HelloService.Hello","params":["hello"],"id":0}
|
||||
```
|
||||
|
||||
这是一个json编码的数据,其中method部分对应要调用的rpc服务和方法组合成的名字,params部分的第一个元素为参数部分,id是由调用端维护的一个唯一的调用编号。
|
||||
这是一个json编码的数据,其中method部分对应要调用的rpc服务和方法组合成的名字,params部分的第一个元素为参数,id是由调用端维护的一个唯一的调用编号。
|
||||
|
||||
请求的json数据对应在内部对应两个结构体:客户端是clientRequest,服务端是serverRequest。clientRequest和serverRequest结构体的内容基本是一致的:
|
||||
请求的json数据对象在内部对应两个结构体:客户端是clientRequest,服务端是serverRequest。clientRequest和serverRequest结构体的内容基本是一致的:
|
||||
|
||||
```go
|
||||
type clientRequest struct {
|
||||
@ -255,7 +255,7 @@ type serverRequest struct {
|
||||
}
|
||||
```
|
||||
|
||||
在获取到RPC调用对应的json数据后,我们可以通过直接向假设了RPC服务的TCP服务器发送json数据模拟RPC方法调用:
|
||||
在获取到RPC调用对应的json数据后,我们可以通过直接向架设了RPC服务的TCP服务器发送json数据模拟RPC方法调用:
|
||||
|
||||
```
|
||||
$ echo -e '{"method":"HelloService.Hello","params":["hello"],"id":1}' | nc localhost 1234
|
||||
@ -285,13 +285,13 @@ type serverResponse struct {
|
||||
}
|
||||
```
|
||||
|
||||
因此无论是采用任何语言,只要遵循同样的json结构,以同样的流程就可以和Go语言编写的RPC服务进行通信。这样我们就实现了跨语言的RPC。
|
||||
因此无论采用何种语言,只要遵循同样的json结构,以同样的流程就可以和Go语言编写的RPC服务进行通信。这样我们就实现了跨语言的RPC。
|
||||
|
||||
## Http上的RPC
|
||||
|
||||
Go语言内在的RPC框架已经支持在Http协议上提供RPC服务。但是框架的http服务同样采用了内置的gob协议,并且没有提供采用其它协议的接口,因此从其它语言依然无法访问的。在前面的例子中,我们已经实现了在纯的TCP协议之上运行jsonrpc服务,并且可以通过nc命令行工具成功实现了RPC方法调用。现在我们尝试在http协议上提供jsonrpc服务。
|
||||
|
||||
心的RPC服务其实是一个类似REST规范的接口,采用请求和相应处理流程:
|
||||
新的RPC服务其实是一个类似REST规范的接口,接收请求和采用相应处理流程:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@ -315,7 +315,7 @@ func main() {
|
||||
|
||||
RPC的服务假设在“/jsonrpc”路径,在处理函数中基于http.ResponseWriter和http.Request类型的参数构造一个io.ReadWriteCloser类型的conn通道。然后基于conn构建针对服务端的json编码解码器。最后通过rpc.ServeRequest处理一次RPC方法调用。
|
||||
|
||||
模拟一次RPC调用的过程就是向该链接发生一个json字符串:
|
||||
模拟一次RPC调用的过程就是向该链接发送一个json字符串:
|
||||
|
||||
```
|
||||
$ curl localhost:1234/jsonrpc -X POST --data '{"method":"HelloService.Hello","params":["hello"],"id":0}'
|
||||
|
@ -1,10 +1,10 @@
|
||||
# 4.2. Protobuf
|
||||
|
||||
Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,并于2008年对外开源。Protobuf刚开源时的定位类似于XML、JSON等数据描述语言,通过附带工具生成等代码提实现将结构化数据序列化的功能。但是我们更关注的是Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言PRC接口的基础工具。
|
||||
Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,并于2008年对外开源。Protobuf刚开源时的定位类似于XML、JSON等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能。但是我们更关注的是Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言PRC接口的基础工具。
|
||||
|
||||
## Protobuf入门
|
||||
|
||||
对于没有用过Protobuf读者,建议先从官网了解下基本用法。这里我们尝试如何将Protobuf和RPC结合在一起使用,通过Protobuf来最终保证RPC的接口规范和完全。Protobuf中最基本的数据单元是message,是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。
|
||||
对于没有用过Protobuf读者,建议先从官网了解下基本用法。这里我们尝试如何将Protobuf和RPC结合在一起使用,通过Protobuf来最终保证RPC的接口规范和安全。Protobuf中最基本的数据单元是message,是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。
|
||||
|
||||
首先创建hello.proto文件,其中包装HelloService服务中用到的字符串类型:
|
||||
|
||||
@ -18,11 +18,11 @@ message String {
|
||||
}
|
||||
```
|
||||
|
||||
开头的syntax语句表示采用Protobuf第三版本的语法。第三版的Protobuf对语言进行的提炼简化,所有成员均采用类似Go语言中的零值初始化(不在支持自定义默认值),同时消息成员也不再支持required特性。然后package指令指明当前是main包(这样可以和Go的包明保持一致),当然用户也可以针对不同的语言定制对应的包路径和名称。最后message关键字定义一个新的String类型,在最终生成的Go语言代码中对应一个String结构体。String类型中只有一个字符串类型的value成员,该成员的Protobuf编码时的成员编号为1。
|
||||
开头的syntax语句表示采用Protobuf第三版本的语法。第三版的Protobuf对语言进行了提炼简化,所有成员均采用类似Go语言中的零值初始化(不再支持自定义默认值),同时消息成员也不再支持required特性。然后package指令指明当前是main包(这样可以和Go的包明保持一致),当然用户也可以针对不同的语言定制对应的包路径和名称。最后message关键字定义一个新的String类型,在最终生成的Go语言代码中对应一个String结构体。String类型中只有一个字符串类型的value成员,该成员的Protobuf编码时的成员编号为1。
|
||||
|
||||
在XML或JSON成数据描述语言中,一遍通过成员的名字来绑定对应的数据。但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,但是也非常不便于人类查阅。我们目前并不关注Protobuf的编码技术,最终生成的Go结构体可以自由采用JSON或gob等编码格式,因此大家可以暂时忽略Protobuf的成员编号部分。
|
||||
在XML或JSON等数据描述语言中,一般通过成员的名字来绑定对应的数据。但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,但是也非常不便于人类查阅。我们目前并不关注Protobuf的编码技术,最终生成的Go结构体可以自由采用JSON或gob等编码格式,因此大家可以暂时忽略Protobuf的成员编码部分。
|
||||
|
||||
Protobuf核心的工具集是C++语言开发的,在官方的protoc编译器中并不支持Go语言。要想基于上面的hello.proto文件生成相应的Go代码,需要安装相应的工具。首先是安装官方的protoc工具,可以从 https://github.com/google/protobuf/releases 下载。然后是安装针对Go语言的代码生成插件,可以通过`go get github.com/golang/protobuf/protoc-gen-go`命令按安装。
|
||||
Protobuf核心的工具集是C++语言开发的,在官方的protoc编译器中并不支持Go语言。要想基于上面的hello.proto文件生成相应的Go代码,需要安装相应的工具。首先是安装官方的protoc工具,可以从 https://github.com/google/protobuf/releases 下载。然后是安装针对Go语言的代码生成插件,可以通过`go get github.com/golang/protobuf/protoc-gen-go`命令安装。
|
||||
|
||||
然后通过以下命令生成相应的Go代码:
|
||||
|
||||
@ -30,7 +30,7 @@ Protobuf核心的工具集是C++语言开发的,在官方的protoc编译器中
|
||||
$ protoc --go_out=. hello.proto
|
||||
```
|
||||
|
||||
其中`go_out`参数告知protoc编译器取加载对应的protoc-gen-go工具,然后通过该工具生成代码,生成代码放到当前目录。最后是一系列要处理的protobuf文件的列表。
|
||||
其中`go_out`参数告知protoc编译器去加载对应的protoc-gen-go工具,然后通过该工具生成代码,生成代码放到当前目录。最后是一系列要处理的protobuf文件的列表。
|
||||
|
||||
这里只生成了一个hello.pb.go文件,其中String结构体内容如下:
|
||||
|
||||
@ -57,7 +57,7 @@ func (m *String) GetValue() string {
|
||||
}
|
||||
```
|
||||
|
||||
生成的结构体中有一些以`XXX_`为前缀名字的成员,目前可以忽略这些成员。同时String类型还自动生成了一组方法,其中ProtoMessage方法表示这是一个实现了proto.Message接口的方法。此外Protobuf还为每个成员生成了一个Get方法,Get方法不仅可以处理空指针类型,而且可以和Protobuf第三版的方法保持一致(第二版的自定义默认值特性依赖这类方法)。
|
||||
生成的结构体中有一些以`XXX_`为名字前缀的成员,目前可以忽略这些成员。同时String类型还自动生成了一组方法,其中ProtoMessage方法表示这是一个实现了proto.Message接口的方法。此外Protobuf还为每个成员生成了一个Get方法,Get方法不仅可以处理空指针类型,而且可以和Protobuf第二版的方法保持一致(第二版的自定义默认值特性依赖这类方法)。
|
||||
|
||||
基于新的String类型,我们可以重新实现HelloService:
|
||||
|
||||
@ -74,7 +74,7 @@ func (p *HelloService) Hello(request *String, reply *String) error {
|
||||
|
||||
至此,我们初步实现了Protobuf和RPC组合工作。在启动RPC服务时,我们依然可以选择默认的gob或手工指定json编码,甚至可以重新基于protobuf编码实现一个插件。虽然做了这么多工作,但是似乎并没有看到什么收益!
|
||||
|
||||
回顾第一章中更安全的PRC接口部分的内容,当时我们花费了极大的力气去给RPC服务增加安全的保障。最终得到的更安全的PRC接口的代码本书就非常繁琐比利于手工维护,同时全部安全相关的代码只适用于Go语言环境!既然使用了Protobuf定义的输入和输出参数,那么RPC服务接口是否也可以通过Protobuf定义呢?其实用Protobuf定义语言无关的PRC服务接口才是它真正的价值所在!
|
||||
回顾第一章中更安全的PRC接口部分的内容,当时我们花费了极大的力气去给RPC服务增加安全的保障。最终得到的更安全的PRC接口的代码本书就非常繁琐的使用手工维护,同时全部安全相关的代码只适用于Go语言环境!既然使用了Protobuf定义的输入和输出参数,那么RPC服务接口是否也可以通过Protobuf定义呢?其实用Protobuf定义语言无关的PRC服务接口才是它真正的价值所在!
|
||||
|
||||
下面更新hello.proto文件,通过Protobuf来定义HelloService服务:
|
||||
|
||||
@ -92,7 +92,7 @@ service HelloService {
|
||||
$ protoc --go_out=plugins=grpc:. hello.proto
|
||||
```
|
||||
|
||||
在生成的代码中多了一些类似HelloServiceServer、HelloServiceClient的新类型。这些类似是为grpc服务的,并不符合我们的RPC要求。
|
||||
在生成的代码中多了一些类似HelloServiceServer、HelloServiceClient的新类型。这些类型是为grpc服务的,并不符合我们的RPC要求。
|
||||
|
||||
grpc插件为我们提供了改进思路,下面我们将探索如何为我们的RPC生成安全的代码。
|
||||
|
||||
@ -233,7 +233,7 @@ func main() {
|
||||
$ protoc --go-netrpc_out=plugins=netrpc:. hello.proto
|
||||
```
|
||||
|
||||
其中`--go-netrpc_out`参数高中protoc编译器加载名为protoc-gen-go-netrpc的插件,插件中的`plugins=netrpc`指示启用内部名为netrpc的netrpcPlugin插件。
|
||||
其中`--go-netrpc_out`参数告知protoc编译器加载名为protoc-gen-go-netrpc的插件,插件中的`plugins=netrpc`指示启用内部名为netrpc的netrpcPlugin插件。
|
||||
|
||||
在新生成的hello.pb.go文件中将包含增加的注释代码。至此,手工定制的Protobuf代码生成插件终于可以工作了。
|
||||
|
||||
|
@ -1,24 +1,151 @@
|
||||
# 4.3. 玩转RPC
|
||||
|
||||
TODO
|
||||
在不同的场景中RPC有着不同的需求,因此开源的社区就诞生了各种RPC框架。本节我们将尝试Go内置RPC框架在一些比较特殊场景的用法。
|
||||
|
||||
<!--
|
||||
## 反向RPC
|
||||
|
||||
认证/反向/gls?,能够拿到req吗,基于req的gls
|
||||
通常的RPC是基于C/S结构,RPC的服务端对应网络的服务器,RPC的客户端也对应网络客户端。但是对于一些特殊场景,比如在公司内网提供一个RPC服务,但是在外网无法链接到内网的服务器。这种时候我们可以参考类似反向代理的技术,首先从内网主动链接到外网的TCP服务器,然后基于TCP链接向外网提供RPC服务。
|
||||
|
||||
--
|
||||
以下是启动反向RPC服务的代码:
|
||||
|
||||
pb 和 json 是类似的,
|
||||
```go
|
||||
func main() {
|
||||
rpc.Register(new(HelloService))
|
||||
|
||||
唯一的差异的 protoc 工具
|
||||
for {
|
||||
conn, _ := net.Dial("tcp", "localhost:1234")
|
||||
if conn == nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
有了代码生成,一切就发生变化了
|
||||
rpc.ServeConn(conn)
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
pb 的重要性不再时底层的编码,二是 api 的通用语言!!!
|
||||
反向RPC的内网服务将不再主导提供TCP监听服务,而是首先主动链接到对方的TCP服务器。然后基于每个建立的TCP链接向对方提供RPC服务。
|
||||
|
||||
但是 pb 个 rest 是无法一一等价的
|
||||
而RPC客户端则需要在一个公共的地址提供一个TCP服务,用于接受RPC服务器的链接请求:
|
||||
|
||||
生成简单的代码,增加接口约束
|
||||
```go
|
||||
func main() {
|
||||
listener, err := net.Listen("tcp", ":1234")
|
||||
if err != nil {
|
||||
log.Fatal("ListenTCP error:", err)
|
||||
}
|
||||
|
||||
甚至最终回归到 gob 或 json 编码
|
||||
-->
|
||||
clientChan := make(chan *rpc.Client)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
log.Fatal("Accept error:", err)
|
||||
}
|
||||
|
||||
clientChan <- rpc.NewClient(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
doClientWork(clientChan)
|
||||
}
|
||||
```
|
||||
|
||||
当每个链接建立后,基于网络链接构造RPC客户端对象并发送到clientChan管道。
|
||||
|
||||
客户端执行RPC调用的操作在doClientWork函数完成:
|
||||
|
||||
```go
|
||||
func doClientWork(clientChan <-chan *rpc.Client) {
|
||||
client := <-clientChan
|
||||
defer client.Close()
|
||||
|
||||
var reply string
|
||||
err = client.Call("HelloService.Hello", "hello", &reply)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(reply)
|
||||
}
|
||||
```
|
||||
|
||||
首先从管道去取一个RPC客户端对象,并且通过defer语句指定在函数退出前关闭客户端。然后是执行正常的RPC调用。
|
||||
|
||||
|
||||
## 上下文信息
|
||||
|
||||
首先是上下文信息,基于上下文我们可以针对不同客户端提供定制化的RPC服务。我们可以通过为每个信道提供独立的RPC服务来实现对上下文特性的支持。
|
||||
|
||||
首先改造HelloService,里面增加了对应链接的conn成员:
|
||||
|
||||
```go
|
||||
type HelloService struct {
|
||||
conn net.Conn
|
||||
}
|
||||
```
|
||||
|
||||
然后为每个信道启动独立的RPC服务:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
listener, err := net.Listen("tcp", ":1234")
|
||||
if err != nil {
|
||||
log.Fatal("ListenTCP error:", err)
|
||||
}
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
log.Fatal("Accept error:", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
|
||||
p := rpc.NewServer()
|
||||
p.Register(&HelloService{conn: conn})
|
||||
p.ServeConn(conn)
|
||||
} ()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hello方法中就可以根据conn成员识别不同信道的RPC调用:
|
||||
|
||||
```go
|
||||
func (p *HelloService) Hello(request string, reply *string) error {
|
||||
*reply = "hello:" + request + ", from" + p.conn.RemoteAddr().String()
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
基于上下文信息,我们可以方便地为RPC服务增加简单的登陆状态的验证:
|
||||
|
||||
```go
|
||||
type HelloService struct {
|
||||
conn net.Conn
|
||||
isLogin bool
|
||||
}
|
||||
|
||||
func (p *HelloService) Login(request string, reply *string) error {
|
||||
if request != "user:password" {
|
||||
return fmt.Errorf("auth failed")
|
||||
}
|
||||
log.Println("login ok")
|
||||
p.isLogin = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HelloService) Hello(request string, reply *string) error {
|
||||
if !p.isLogin {
|
||||
return fmt.Errorf("please login")
|
||||
}
|
||||
*reply = "hello:" + request + ", from" + p.conn.RemoteAddr().String()
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
这样可以要求在客户端链接RPC服务时,首先要执行登陆操作,登陆成功后才能正常执行其他的服务。
|
||||
|
@ -1,8 +1,223 @@
|
||||
# 4.4. GRPC入门
|
||||
|
||||
GRPC是Google公司基于Protobuf开发的跨语言的开源RPC框架。GRPC基于HTTP/2协议设计,可以基于一个HTTP/2链接提供多个服务,对于移动设备更加友好。本节将讲述GRPC的简单用法。
|
||||
|
||||
## GRPC入门
|
||||
|
||||
如果从Protobuf的角度看,GRPC只不过是一个针对service接口生成代码的生成器。我们在本章的第二节中手工实现了一个简单的Protobuf代码生成器插件,只不过当时生成的代码是适配标准库的RPC框架的。
|
||||
|
||||
创建hello.proto文件,定义HelloService接口:
|
||||
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
|
||||
package main;
|
||||
|
||||
message String {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
service HelloService {
|
||||
rpc Hello (String) returns (String);
|
||||
}
|
||||
```
|
||||
|
||||
使用protoc-gen-go内置的grpc插件生成GRPC代码:
|
||||
|
||||
```
|
||||
$ protoc --go_out=plugins=grpc:. hello.proto
|
||||
```
|
||||
|
||||
GRPC插件会为服务端和客户端生成不同的接口:
|
||||
|
||||
```go
|
||||
type HelloServiceServer interface {
|
||||
Hello(context.Context, *String) (*String, error)
|
||||
}
|
||||
|
||||
type HelloServiceClient interface {
|
||||
Hello(ctx context.Context, in *String, opts ...grpc.CallOption) (*String, error)
|
||||
}
|
||||
```
|
||||
|
||||
GRPC通过context.Context参数,为每个方法调用提供了上下文支持。客户端在调用方法的时候,可以通过可选的grpc.CallOption类型的参数提供额外的上下文信息。
|
||||
|
||||
基于服务端的HelloServiceServer接口可以重新实现HelloService服务:
|
||||
|
||||
```go
|
||||
type HelloServiceImpl struct{}
|
||||
|
||||
func (p *HelloServiceImpl) Hello(ctx context.Context, args *String) (*String, error) {
|
||||
reply := &String{Value: "hello:" + args.GetValue()}
|
||||
return reply, nil
|
||||
}
|
||||
```
|
||||
|
||||
GRPC服务的启动流程和标准库的RPC服务启动流程类似:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
grpcServer := grpc.NewServer()
|
||||
RegisterHelloServiceServer(grpcServer, &HelloServiceImpl{})
|
||||
|
||||
lis, err := net.Listen("tcp", ":1234")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
grpcServer.Serve(lis)
|
||||
}
|
||||
```
|
||||
|
||||
首先是通过`grpc.NewServer()`构造一个GRPC服务对象,然后通过GRPC插件生成的RegisterHelloServiceServer函数注册我们实现的HelloServiceImpl服务。然后通过`grpcServer.Serve(lis)`在一个监听端口上提供GRPC服务。
|
||||
|
||||
然后就可以通过客户端链接GRPC服务了:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := NewHelloServiceClient(conn)
|
||||
reply, err := client.Hello(context.Background(), &String{Value: "hello"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(reply.GetValue())
|
||||
}
|
||||
```
|
||||
|
||||
其中grpc.Dial负责和GRPC服务建立链接,然后NewHelloServiceClient函数基于已经建立的链接构造HelloServiceClient对象。返回的client其实是一个HelloServiceClient接口对象,通过接口定义的方法就可以调用服务端对应的GRPC服务提供的方法。
|
||||
|
||||
GRPC和标准库的RPC框架还有一个区别,GRPC生成的接口并不支持异步调用。
|
||||
|
||||
## GRPC流
|
||||
|
||||
RPC是远程函数调用,因此每次调用的函数参数和返回值不能太大,否则将严重影响每次调用的性能。因此传统的RPC方法调用对于上传和下载较大数据量场景并不适合。同时传统RPC模式也不适用于对时间不确定的订阅和发布模式。为此,GRPC框架分别提供了服务器端和客户端的流特性。
|
||||
|
||||
服务端或客户端的单向流是双向流的特例,我们在HelloService增加一个支持双向流的Channel方法:
|
||||
|
||||
```proto
|
||||
service HelloService {
|
||||
rpc Hello (String) returns (String);
|
||||
|
||||
rpc Channel (stream String) returns (stream String);
|
||||
}
|
||||
```
|
||||
|
||||
关键字stream指定启用流特性,参数部分是接收客户端参数的流,返回值是返回给客户端的流。
|
||||
|
||||
重新生成代码可以看到接口中新增加的Channel方法的定义:
|
||||
|
||||
```go
|
||||
type HelloServiceServer interface {
|
||||
Hello(context.Context, *String) (*String, error)
|
||||
Channel(HelloService_ChannelServer) error
|
||||
}
|
||||
type HelloServiceClient interface {
|
||||
Hello(ctx context.Context, in *String, opts ...grpc.CallOption) (*String, error)
|
||||
Channel(ctx context.Context, opts ...grpc.CallOption) (HelloService_ChannelClient, error)
|
||||
}
|
||||
```
|
||||
|
||||
在服务端的Channel方法参数是一个新的HelloService_ChannelServer类型的参数,可以用于和客户端双向通信。客户端的Channel方法返回一个HelloService_ChannelClient类型的返回值,可以用于和服务端进行双向通信。
|
||||
|
||||
HelloService_ChannelServer和HelloService_ChannelClient均为接口类型:
|
||||
|
||||
```go
|
||||
type HelloService_ChannelServer interface {
|
||||
Send(*String) error
|
||||
Recv() (*String, error)
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
type HelloService_ChannelClient interface {
|
||||
Send(*String) error
|
||||
Recv() (*String, error)
|
||||
grpc.ClientStream
|
||||
}
|
||||
```
|
||||
|
||||
可以发现服务端和客户端的流辅助接口均定义了Send和Recv方法用于流数据的双向通信。
|
||||
|
||||
现在我们可以实现流服务:
|
||||
|
||||
```go
|
||||
func (p *HelloServiceImpl) Channel(stream HelloService_ChannelServer) error {
|
||||
for {
|
||||
args, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
reply := &String{Value: "hello:" + args.GetValue()}
|
||||
|
||||
err = stream.Send(reply)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
服务端在循环中接收客户端发来的数据,如果遇到io.EOF表示客户端流被关闭,如果函数退出表示服务端流关闭。然后生成返回的数据通过流发送给客户端。需要注意的是,发送和接收的操作并不需要一一对应,用户可以根据真实场景进行组织代码。
|
||||
|
||||
客户端需要先调用Channel方法获取返回的流对象:
|
||||
|
||||
```go
|
||||
stream, err := client.Channel(context.Background())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
在客户端我们将发送和接收操作放到两个独立的Goroutine。首先是向服务端发送数据:
|
||||
|
||||
```go
|
||||
go func() {
|
||||
for {
|
||||
if err := stream.Send(&String{Value: "hi"}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
然后在循环中接收服务端返回的数据:
|
||||
|
||||
```go
|
||||
for {
|
||||
reply, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(reply.GetValue())
|
||||
}
|
||||
```
|
||||
|
||||
这样就完成了完整的流接收和发送支持。
|
||||
|
||||
|
||||
<!--
|
||||
Publish
|
||||
Watch
|
||||
|
||||
TODO
|
||||
|
||||
<!--
|
||||
## 认证
|
||||
|
||||
TODO
|
||||
|
||||
|
||||
|
||||
入门/流/认证
|
||||
|
||||
|
4
ch4-rpc/ch4-05-faq.md
Normal file
4
ch4-rpc/ch4-05-faq.md
Normal file
@ -0,0 +1,4 @@
|
||||
# 4.5. 补充说明
|
||||
|
||||
本章重点讲述了Go标准库的RPC和基于Protobuf衍生的GRPC框架,同时也简单展示了如何自己定制一个RPC框架。之所以聚焦在这几个有限的主题,是因为这几个技术都是Go语言团队官方在进行维护,和Go语言契合也最为默契。不过RPC依然是一个庞大的主题,足以单独成书。目前开源世界也有很多富有特色的RPC框架,还有针对分布式系统进行深度定制的RPC系统,用户可以根据自己实际需求选择合适的工具。
|
||||
|
@ -1,8 +0,0 @@
|
||||
# 4.8. 补充说明
|
||||
|
||||
TODO
|
||||
|
||||
<!--
|
||||
|
||||
参考资料
|
||||
-->
|
@ -127,7 +127,7 @@ for _, endpoint := range endpointList {
|
||||
```shell
|
||||
ls /platform/order-system/create-order-service-http
|
||||
|
||||
[]
|
||||
['10.1.23.1:1023', '10.11.23.1:1023']
|
||||
```
|
||||
|
||||
当与 zk 断开连接时,注册在该节点下的临时节点也会消失,即实现了服务节点故障时的被动摘除。
|
||||
@ -136,16 +136,51 @@ ls /platform/order-system/create-order-service-http
|
||||
|
||||
## 基于 zk 的完整服务发现流程
|
||||
|
||||
节点故障断开 zk 连接时,zk 会负责将该消息通知所有监听方。
|
||||
|
||||
用代码来实现一下上面的几个逻辑。
|
||||
|
||||
### 临时节点注册
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/samuel/go-zookeeper/zk"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res, err := c.Create("/platform/order-system/create-order-service-http/10.1.13.3:1043", []byte("1"),
|
||||
zk.FlagEphemeral, zk.WorldACL(zk.PermAll))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
println(res)
|
||||
time.Sleep(time.Second * 50)
|
||||
}
|
||||
```
|
||||
|
||||
### 服务节点获取
|
||||
在 sleep 的时候我们在 cli 中查看写入的临时节点数据:
|
||||
|
||||
```shell
|
||||
ls /platform/order-system/create-order-service-http
|
||||
['10.1.13.3:1043']
|
||||
```
|
||||
|
||||
在程序结束之后,很快这条数据也消失了:
|
||||
|
||||
```shell
|
||||
ls /platform/order-system/create-order-service-http
|
||||
[]
|
||||
```
|
||||
|
||||
### watch 数据变化
|
||||
|
||||
### 消息通知
|
||||
|
||||
|
@ -94,3 +94,28 @@ Go 自身的 timer 就是用时间堆来实现的,不过并没有使用二叉
|
||||
### 时间轮
|
||||
|
||||

|
||||
|
||||
用时间轮来实现 timer 时,我们需要定义每一个格子的“刻度”,可以将时间轮想像成一个时钟,中心有秒针顺时针转动。每次转动到一个刻度时,我们就需要去查看该刻度挂载的 tasklist 是否有已经到期的任务。
|
||||
|
||||
从结构上来讲,时间轮和哈希表很相似,如果我们把哈希算法定义为:触发时间%时间轮元素大小。那么这就是一个简单的哈希表。在哈希冲突时,采用链表挂载哈希冲突的定时器。
|
||||
|
||||
除了这种单层时间轮,业界也有一些时间轮采用多层实现,这里就不再赘述了。
|
||||
|
||||
## 任务分发
|
||||
|
||||
有了基本的 timer 实现方案,如果我们开发的是单机系统,那么就可以撸起袖子开干了,不过本章我们讨论的是分布式,距离“分布式”还稍微有一些距离。
|
||||
|
||||
我们还需要把这些“定时”或是“延时”(本质也是定时)任务分发出去。下面是一种思路:
|
||||
|
||||

|
||||
|
||||
每一个实例每隔一小时,会去数据库里把下一个小时需要处理的定时任务捞出来,捞取的时候只要取那些 task_id % shard_count = shard_id 的那些 task 即可。
|
||||
|
||||
当这些定时任务被触发之后需要通知用户侧,有两种思路:
|
||||
|
||||
1. 将任务被触发的信息封装为一条 event 消息,发往消息队列,由用户侧对消息队列进行监听。
|
||||
2. 对用户预先配置的回调函数进行调用。
|
||||
|
||||
两种方案各有优缺点,如果采用 1,那么如果消息队列出故障会导致整个系统不可用,当然,现在的消息队列一般也会有自身的高可用方案,大多数时候我们不用担心这个问题。其次一般业务流程中间走消息队列的话会导致延时增加,定时任务若必须在触发后的几十毫秒到几百毫秒内完成,那么采用消息队列就会有一定的风险。如果采用 2,会加重定时任务系统的负担。我们知道,单机的 timer 执行时最害怕的就是回调函数执行时间长,这样会阻塞后续的任务执行。在分布式场景下,这种忧虑依然是适用的。一个不负责任的业务回调可能就会直接拖垮整个定时任务系统。所以我们还要考虑在回调的基础上增加经过测试的超时时间设置,并且对由用户填入的超时时间做慎重的审核。
|
||||
|
||||
## rebalance 和幂等考量
|
||||
|
@ -1,12 +0,0 @@
|
||||
package main
|
||||
|
||||
type HelloService struct{}
|
||||
|
||||
func (p *HelloService) Hello(request String, reply *String) error {
|
||||
reply.Value = "hello:" + request.GetValue()
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
// todo
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
// todo
|
||||
}
|
BIN
images/ch6-task-sched.png
Normal file
BIN
images/ch6-task-sched.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
@ -4,7 +4,7 @@
|
||||
|
||||

|
||||
|
||||
- 作者:柴树杉 (chai2010, chaishushan@gmail.com)
|
||||
- 作者:柴树杉 (chai2010, chaishushan@gmail.com), 曹春晖 (cch123, https://github.com/cch123)
|
||||
- 网址:https://github.com/chai2010/advanced-go-programming-book
|
||||
|
||||
## 在线阅读
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user