1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-23 20:02:22 +00:00

修改排版 ch4/*

This commit is contained in:
igmainc 2022-01-24 15:37:08 +08:00
parent c8b48f13b8
commit a60f1e80f0
10 changed files with 294 additions and 296 deletions

View File

@ -1,12 +1,12 @@
# 4.1 RPC入门
# 4.1 RPC 入门
RPC是远程过程调用的简称是分布式系统中不同节点间流行的通信方式。在互联网时代RPC已经和IPC一样成为一个不可或缺的基础构件。因此Go语言的标准库也提供了一个简单的RPC实现我们将以此为入口学习RPC的各种用法。
RPC 是远程过程调用的简称是分布式系统中不同节点间流行的通信方式。在互联网时代RPC 已经和 IPC 一样成为一个不可或缺的基础构件。因此 Go 语言的标准库也提供了一个简单的 RPC 实现,我们将以此为入口学习 RPC 的各种用法。
## 4.1.1 RPC版"Hello, World"
## 4.1.1 RPC 版 “Hello, World”
Go语言的RPC包的路径为net/rpc也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。在第一章“Hello, World”革命一节最后我们基于http实现了一个打印例子。下面我们尝试基于rpc实现一个类似的例子。
Go 语言的 RPC 包的路径为 net/rpc也就是放在了 net 包目录下面。因此我们可以猜测该 RPC 包是建立在 net 包基础之上的。在第一章 “Hello, World” 革命一节最后,我们基于 http 实现了一个打印例子。下面我们尝试基于 rpc 实现一个类似的例子。
我们先构造一个HelloService类型其中的Hello方法用于实现打印功能
我们先构造一个 HelloService 类型,其中的 Hello 方法用于实现打印功能:
```go
type HelloService struct {}
@ -17,9 +17,9 @@ func (p *HelloService) Hello(request string, reply *string) error {
}
```
其中Hello方法必须满足Go语言的RPC规则方法只能有两个可序列化的参数其中第二个参数是指针类型并且返回一个error类型同时必须是公开的方法。
其中 Hello 方法必须满足 Go 语言的 RPC 规则:方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个 error 类型,同时必须是公开的方法。
然后就可以将HelloService类型的对象注册为一个RPC服务
然后就可以将 HelloService 类型的对象注册为一个 RPC 服务:
```go
func main() {
@ -39,9 +39,9 @@ func main() {
}
```
其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数所有注册的方法会放在“HelloService”服务空间之下。然后我们建立一个唯一的TCP链接并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。
其中 rpc.Register 函数调用会将对象类型中所有满足 RPC 规则的对象方法注册为 RPC 函数,所有注册的方法会放在 “HelloService” 服务空间之下。然后我们建立一个唯一的 TCP 链接,并且通过 rpc.ServeConn 函数在该 TCP 链接上为对方提供 RPC 服务。
下面是客户端请求HelloService服务的代码
下面是客户端请求 HelloService 服务的代码:
```go
func main() {
@ -60,15 +60,15 @@ func main() {
}
```
首先是通过rpc.Dial拨号RPC服务然后通过client.Call调用具体的RPC方法。在调用client.Call时第一个参数是用点号链接的RPC服务名字和方法名字第二和第三个参数分别我们定义RPC方法的两个参数。
首先是通过 rpc.Dial 拨号 RPC 服务,然后通过 client.Call 调用具体的 RPC 方法。在调用 client.Call 时,第一个参数是用点号链接的 RPC 服务名字和方法名字,第二和第三个参数分别我们定义 RPC 方法的两个参数。
由这个例子可以看出RPC的使用其实非常简单。
由这个例子可以看出 RPC 的使用其实非常简单。
## 4.1.2 更安全的RPC接口
## 4.1.2 更安全的 RPC 接口
在涉及RPC的应用中作为开发人员一般至少有三种角色首先是服务端实现RPC方法的开发人员其次是客户端调用RPC方法的人员最后也是最重要的是制定服务端和客户端RPC接口规范的设计人员。在前面的例子中我们为了简化将以上几种角色的工作全部放到了一起虽然看似实现简单但是不利于后期的维护和工作的切割。
在涉及 RPC 的应用中,作为开发人员一般至少有三种角色:首先是服务端实现 RPC 方法的开发人员,其次是客户端调用 RPC 方法的人员,最后也是最重要的是制定服务端和客户端 RPC 接口规范的设计人员。在前面的例子中我们为了简化将以上几种角色的工作全部放到了一起,虽然看似实现简单,但是不利于后期的维护和工作的切割。
如果要重构HelloService服务第一步需要明确服务的名字和接口
如果要重构 HelloService 服务,第一步需要明确服务的名字和接口:
```go
const HelloServiceName = "path/to/pkg.HelloService"
@ -82,9 +82,9 @@ func RegisterHelloService(svc HelloServiceInterface) error {
}
```
我们将RPC服务的接口规范分为三个部分首先是服务的名字然后是服务要实现的详细方法列表最后是注册该类型服务的函数。为了避免名字冲突我们在RPC服务的名字中增加了包路径前缀这个是RPC服务抽象的包路径并非完全等价Go语言的包路径。RegisterHelloService注册服务时编译器会要求传入的对象满足HelloServiceInterface接口。
我们将 RPC 服务的接口规范分为三个部分:首先是服务的名字,然后是服务要实现的详细方法列表,最后是注册该类型服务的函数。为了避免名字冲突,我们在 RPC 服务的名字中增加了包路径前缀(这个是 RPC 服务抽象的包路径,并非完全等价 Go 语言的包路径。RegisterHelloService 注册服务时,编译器会要求传入的对象满足 HelloServiceInterface 接口。
在定义了RPC服务接口规范之后客户端就可以根据规范编写RPC调用的代码了
在定义了 RPC 服务接口规范之后,客户端就可以根据规范编写 RPC 调用的代码了:
```go
func main() {
@ -101,9 +101,9 @@ func main() {
}
```
其中唯一的变化是client.Call的第一个参数用HelloServiceName+".Hello"代替了"HelloService.Hello"。然而通过client.Call函数调用RPC方法依然比较繁琐同时参数的类型依然无法得到编译器提供的安全保障。
其中唯一的变化是 client.Call 的第一个参数用 HelloServiceName+".Hello" 代替了 "HelloService.Hello"。然而通过 client.Call 函数调用 RPC 方法依然比较繁琐,同时参数的类型依然无法得到编译器提供的安全保障。
为了简化客户端用户调用RPC函数我们在可以在接口规范部分增加对客户端的简单包装
为了简化客户端用户调用 RPC 函数,我们在可以在接口规范部分增加对客户端的简单包装:
```go
type HelloServiceClient struct {
@ -125,7 +125,7 @@ func (p *HelloServiceClient) Hello(request string, reply *string) error {
}
```
我们在接口规范中针对客户端新增加了HelloServiceClient类型该类型也必须满足HelloServiceInterface接口这样客户端用户就可以直接通过接口对应的方法调用RPC函数。同时提供了一个DialHelloService方法直接拨号HelloService服务。
我们在接口规范中针对客户端新增加了 HelloServiceClient 类型,该类型也必须满足 HelloServiceInterface 接口,这样客户端用户就可以直接通过接口对应的方法调用 RPC 函数。同时提供了一个 DialHelloService 方法,直接拨号 HelloService 服务。
基于新的客户端接口,我们可以简化客户端用户的代码:
@ -144,9 +144,9 @@ func main() {
}
```
现在客户端用户不用再担心RPC方法名字或参数类型不匹配等低级错误的发生。
现在客户端用户不用再担心 RPC 方法名字或参数类型不匹配等低级错误的发生。
最后是基于RPC接口规范编写真实的服务端代码
最后是基于 RPC 接口规范编写真实的服务端代码:
```go
type HelloService struct {}
@ -175,16 +175,16 @@ func main() {
}
```
在新的RPC服务端实现中我们用RegisterHelloService函数来注册函数这样不仅可以避免命名服务名称的工作同时也保证了传入的服务对象满足了RPC接口的定义。最后我们新的服务改为支持多个TCP链接然后为每个TCP链接提供RPC服务。
在新的 RPC 服务端实现中,我们用 RegisterHelloService 函数来注册函数,这样不仅可以避免命名服务名称的工作,同时也保证了传入的服务对象满足了 RPC 接口的定义。最后我们新的服务改为支持多个 TCP 链接,然后为每个 TCP 链接提供 RPC 服务。
## 4.1.3 跨语言的RPC
## 4.1.3 跨语言的 RPC
标准库的RPC默认采用Go语言特有的gob编码因此从其它语言调用Go语言实现的RPC服务将比较困难。在互联网的微服务时代每个RPC以及服务的使用者都可能采用不同的编程语言因此跨语言是互联网时代RPC的一个首要条件。得益于RPC的框架设计Go语言的RPC其实也是很容易实现跨语言支持的。
标准库的 RPC 默认采用 Go 语言特有的 gob 编码,因此从其它语言调用 Go 语言实现的 RPC 服务将比较困难。在互联网的微服务时代,每个 RPC 以及服务的使用者都可能采用不同的编程语言,因此跨语言是互联网时代 RPC 的一个首要条件。得益于 RPC 的框架设计Go 语言的 RPC 其实也是很容易实现跨语言支持的。
Go语言的RPC框架有两个比较有特色的设计一个是RPC数据打包时可以通过插件实现自定义的编码和解码另一个是RPC建立在抽象的io.ReadWriteCloser接口之上的我们可以将RPC架设在不同的通讯协议之上。这里我们将尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的RPC。
Go 语言的 RPC 框架有两个比较有特色的设计:一个是 RPC 数据打包时可以通过插件实现自定义的编码和解码;另一个是 RPC 建立在抽象的 io.ReadWriteCloser 接口之上的,我们可以将 RPC 架设在不同的通讯协议之上。这里我们将尝试通过官方自带的 net/rpc/jsonrpc 扩展实现一个跨语言的 RPC。
首先是基于json编码重新实现RPC服务
首先是基于 json 编码重新实现 RPC 服务:
```go
func main() {
@ -206,9 +206,9 @@ func main() {
}
```
代码中最大的变化是用rpc.ServeCodec函数替代了rpc.ServeConn函数传入的参数是针对服务端的json编解码器。
代码中最大的变化是用 rpc.ServeCodec 函数替代了 rpc.ServeConn 函数,传入的参数是针对服务端的 json 编解码器。
然后是实现json版本的客户端
然后是实现 json 版本的客户端:
```go
func main() {
@ -229,17 +229,17 @@ func main() {
}
```
先手工调用net.Dial函数建立TCP链接然后基于该链接建立针对客户端的json编解码器。
先手工调用 net.Dial 函数建立 TCP 链接,然后基于该链接建立针对客户端的 json 编解码器。
在确保客户端可以正常调用RPC服务的方法之后我们用一个普通的TCP服务代替Go语言版本的RPC服务这样可以查看客户端调用时发送的数据格式。比如通过nc命令`nc -l 1234`在同样的端口启动一个TCP服务。然后再次执行一次RPC调用将会发现nc输出了以下的信息
在确保客户端可以正常调用 RPC 服务的方法之后,我们用一个普通的 TCP 服务代替 Go 语言版本的 RPC 服务,这样可以查看客户端调用时发送的数据格式。比如通过 nc 命令 `nc -l 1234` 在同样的端口启动一个 TCP 服务。然后再次执行一次 RPC 调用将会发现 nc 输出了以下的信息:
```json
{"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,21 +255,21 @@ 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
```
返回的结果也是一个json格式的数据
返回的结果也是一个 json 格式的数据:
```json
{"id":1,"result":"hello:hello","error":null}
```
其中id对应输入的id参数result为返回的结果error部分在出问题时表示错误信息。对于顺序调用来说id不是必须的。但是Go语言的RPC框架支持异步调用当返回结果的顺序和调用的顺序不一致时可以通过id来识别对应的调用。
其中 id 对应输入的 id 参数result 为返回的结果error 部分在出问题时表示错误信息。对于顺序调用来说id 不是必须的。但是 Go 语言的 RPC 框架支持异步调用,当返回结果的顺序和调用的顺序不一致时,可以通过 id 来识别对应的调用。
返回的json数据也是对应内部的两个结构体客户端是clientResponse服务端是serverResponse。两个结构体的内容同样也是类似的
返回的 json 数据也是对应内部的两个结构体:客户端是 clientResponse服务端是 serverResponse。两个结构体的内容同样也是类似的
```go
type clientResponse struct {
@ -285,13 +285,13 @@ type serverResponse struct {
}
```
因此无论采用何种语言只要遵循同样的json结构以同样的流程就可以和Go语言编写的RPC服务进行通信。这样我们就实现了跨语言的RPC。
因此无论采用何种语言,只要遵循同样的 json 结构,以同样的流程就可以和 Go 语言编写的 RPC 服务进行通信。这样我们就实现了跨语言的 RPC。
## 4.1.4 Http上的RPC
## 4.1.4 Http 上的 RPC
Go语言内在的RPC框架已经支持在Http协议上提供RPC服务。但是框架的http服务同样采用了内置的gob协议并且没有提供采用其它协议的接口因此从其它语言依然无法访问的。在前面的例子中我们已经实现了在TCP协议之上运行jsonrpc服务并且通过nc命令行工具成功实现了RPC方法调用。现在我们尝试在http协议上提供jsonrpc服务。
Go 语言内在的 RPC 框架已经支持在 Http 协议上提供 RPC 服务。但是框架的 http 服务同样采用了内置的 gob 协议,并且没有提供采用其它协议的接口,因此从其它语言依然无法访问的。在前面的例子中,我们已经实现了在 TCP 协议之上运行 jsonrpc 服务,并且通过 nc 命令行工具成功实现了 RPC 方法调用。现在我们尝试在 http 协议上提供 jsonrpc 服务。
新的RPC服务其实是一个类似REST规范的接口接收请求并采用相应处理流程
新的 RPC 服务其实是一个类似 REST 规范的接口,接收请求并采用相应处理流程:
```go
func main() {
@ -313,20 +313,20 @@ func main() {
}
```
RPC的服务架设在“/jsonrpc”路径在处理函数中基于http.ResponseWriter和http.Request类型的参数构造一个io.ReadWriteCloser类型的conn通道。然后基于conn构建针对服务端的json编码解码器。最后通过rpc.ServeRequest函数为每次请求处理一次RPC方法调用。
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}'
```
返回的结果依然是json字符串
返回的结果依然是 json 字符串:
```json
{"id":0,"result":"hello:hello","error":null}
```
这样就可以很方便地从不同语言中访问RPC服务了。
这样就可以很方便地从不同语言中访问 RPC 服务了。

View File

@ -1,12 +1,12 @@
# 4.2 Protobuf
Protobuf是Protocol Buffers的简称它是Google公司开发的一种数据描述语言并于2008年对外开源。Protobuf刚开源时的定位类似于XML、JSON等数据描述语言通过附带工具生成代码并实现将结构化数据序列化的功能。但是我们更关注的是Protobuf作为接口规范的描述语言可以作为设计安全的跨语言PRC接口的基础工具。
Protobuf Protocol Buffers 的简称,它是 Google 公司开发的一种数据描述语言,并于 2008 年对外开源。Protobuf 刚开源时的定位类似于 XML、JSON 等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能。但是我们更关注的是 Protobuf 作为接口规范的描述语言,可以作为设计安全的跨语言 PRC 接口的基础工具。
## 4.2.1 Protobuf入门
## 4.2.1 Protobuf 入门
对于没有用过Protobuf的读者建议先从官网了解下基本用法。这里我们尝试将Protobuf和RPC结合在一起使用通过Protobuf来最终保证RPC的接口规范和安全。Protobuf中最基本的数据单元是message是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。
对于没有用过 Protobuf 的读者,建议先从官网了解下基本用法。这里我们尝试将 Protobuf RPC 结合在一起使用,通过 Protobuf 来最终保证 RPC 的接口规范和安全。Protobuf 中最基本的数据单元是 message是类似 Go 语言中结构体的存在。在 message 中可以嵌套 message 或其它的基础数据类型的成员。
首先创建hello.proto文件其中包装HelloService服务中用到的字符串类型
首先创建 hello.proto 文件,其中包装 HelloService 服务中用到的字符串类型:
```protobuf
syntax = "proto3";
@ -18,21 +18,21 @@ message String {
}
```
开头的syntax语句表示采用proto3的语法。第三版的Protobuf对语言进行了提炼简化所有成员均采用类似Go语言中的零值初始化不再支持自定义默认值因此消息成员也不再需要支持required特性。然后package指令指明当前是main包这样可以和Go的包名保持一致简化例子代码当然用户也可以针对不同的语言定制对应的包路径和名称。最后message关键字定义一个新的String类型在最终生成的Go语言代码中对应一个String结构体。String类型中只有一个字符串类型的value成员该成员编码时用1编号代替名字。
开头的 syntax 语句表示采用 proto3 的语法。第三版的 Protobuf 对语言进行了提炼简化,所有成员均采用类似 Go 语言中的零值初始化(不再支持自定义默认值),因此消息成员也不再需要支持 required 特性。然后 package 指令指明当前是 main 包(这样可以和 Go 的包名保持一致,简化例子代码),当然用户也可以针对不同的语言定制对应的包路径和名称。最后 message 关键字定义一个新的 String 类型,在最终生成的 Go 语言代码中对应一个 String 结构体。String 类型中只有一个字符串类型的 value 成员,该成员编码时用 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代码
然后通过以下命令生成相应的 Go 代码:
```
$ protoc --go_out=. hello.proto
```
其中`go_out`参数告知protoc编译器去加载对应的protoc-gen-go工具然后通过该工具生成代码生成代码放到当前目录。最后是一系列要处理的protobuf文件的列表。
其中 `go_out` 参数告知 protoc 编译器去加载对应的 protoc-gen-go 工具,然后通过该工具生成代码,生成代码放到当前目录。最后是一系列要处理的 protobuf 文件的列表。
这里只生成了一个hello.pb.go文件其中String结构体内容如下
这里只生成了一个 hello.pb.go 文件,其中 String 结构体内容如下:
```go
type String struct {
@ -54,9 +54,9 @@ 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服务
基于新的 String 类型,我们可以重新实现 HelloService 服务:
```go
type HelloService struct{}
@ -67,13 +67,13 @@ func (p *HelloService) Hello(request *String, reply *String) error {
}
```
其中Hello方法的输入参数和输出的参数均改用Protobuf定义的String类型表示。因为新的输入参数为结构体类型因此改用指针类型作为输入参数函数的内部代码同时也做了相应的调整。
其中 Hello 方法的输入参数和输出的参数均改用 Protobuf 定义的 String 类型表示。因为新的输入参数为结构体类型,因此改用指针类型作为输入参数,函数的内部代码同时也做了相应的调整。
至此我们初步实现了Protobuf和RPC组合工作。在启动RPC服务时我们依然可以选择默认的gob或手工指定json编码甚至可以重新基于protobuf编码实现一个插件。虽然做了这么多工作但是似乎并没有看到什么收益
至此,我们初步实现了 Protobuf RPC 组合工作。在启动 RPC 服务时,我们依然可以选择默认的 gob 或手工指定 json 编码,甚至可以重新基于 protobuf 编码实现一个插件。虽然做了这么多工作,但是似乎并没有看到什么收益!
回顾第一章中更安全的RPC接口部分的内容当时我们花费了极大的力气去给RPC服务增加安全的保障。最终得到的更安全的RPC接口的代码本身就非常繁琐的使用手工维护同时全部安全相关的代码只适用于Go语言环境既然使用了Protobuf定义的输入和输出参数那么RPC服务接口是否也可以通过Protobuf定义呢其实用Protobuf定义语言无关的RPC服务接口才是它真正的价值所在
回顾第一章中更安全的 RPC 接口部分的内容,当时我们花费了极大的力气去给 RPC 服务增加安全的保障。最终得到的更安全的 RPC 接口的代码本身就非常繁琐的使用手工维护,同时全部安全相关的代码只适用于 Go 语言环境!既然使用了 Protobuf 定义的输入和输出参数,那么 RPC 服务接口是否也可以通过 Protobuf 定义呢?其实用 Protobuf 定义语言无关的 RPC 服务接口才是它真正的价值所在!
下面更新hello.proto文件通过Protobuf来定义HelloService服务
下面更新 hello.proto 文件,通过 Protobuf 来定义 HelloService 服务:
```protobuf
service HelloService {
@ -81,24 +81,24 @@ service HelloService {
}
```
但是重新生成的Go代码并没有发生变化。这是因为世界上的RPC实现有千万种protoc编译器并不知道该如何为HelloService服务生成代码。
但是重新生成的 Go 代码并没有发生变化。这是因为世界上的 RPC 实现有千万种protoc 编译器并不知道该如何为 HelloService 服务生成代码。
不过在protoc-gen-go内部已经集成了一个名字为`grpc`的插件可以针对gRPC生成代码
不过在 protoc-gen-go 内部已经集成了一个名字为 `grpc` 的插件,可以针对 gRPC 生成代码:
```
$ protoc --go_out=plugins=grpc:. hello.proto
```
在生成的代码中多了一些类似HelloServiceServer、HelloServiceClient的新类型。这些类型是为gRPC服务的并不符合我们的RPC要求。
在生成的代码中多了一些类似 HelloServiceServer、HelloServiceClient 的新类型。这些类型是为 gRPC 服务的,并不符合我们的 RPC 要求。
不过gRPC插件为我们提供了改进的思路下面我们将探索如何为我们的RPC生成安全的代码。
不过 gRPC 插件为我们提供了改进的思路,下面我们将探索如何为我们的 RPC 生成安全的代码。
## 4.2.2 定制代码生成插件
Protobuf的protoc编译器是通过插件机制实现对不同语言的支持。比如protoc命令出现`--xxx_out`格式的参数那么protoc将首先查询是否有内置的xxx插件如果没有内置的xxx插件那么将继续查询当前系统中是否存在protoc-gen-xxx命名的可执行程序最终通过查询到的插件生成代码。对于Go语言的protoc-gen-go插件来说里面又实现了一层静态插件系统。比如protoc-gen-go内置了一个gRPC插件用户可以通过`--go_out=plugins=grpc`参数来生成gRPC相关代码否则只会针对message生成相关代码。
Protobuf protoc 编译器是通过插件机制实现对不同语言的支持。比如 protoc 命令出现 `--xxx_out` 格式的参数,那么 protoc 将首先查询是否有内置的 xxx 插件,如果没有内置的 xxx 插件那么将继续查询当前系统中是否存在 protoc-gen-xxx 命名的可执行程序,最终通过查询到的插件生成代码。对于 Go 语言的 protoc-gen-go 插件来说,里面又实现了一层静态插件系统。比如 protoc-gen-go 内置了一个 gRPC 插件,用户可以通过 `--go_out=plugins=grpc` 参数来生成 gRPC 相关代码,否则只会针对 message 生成相关代码。
参考gRPC插件的代码可以发现generator.RegisterPlugin函数可以用来注册插件。插件是一个generator.Plugin接口
参考 gRPC 插件的代码,可以发现 generator.RegisterPlugin 函数可以用来注册插件。插件是一个 generator.Plugin 接口:
```go
// A Plugin provides functionality to add to the output during
@ -119,16 +119,16 @@ type Plugin interface {
}
```
其中Name方法返回插件的名字这是Go语言的Protobuf实现的插件体系和protoc插件的名字并无关系。然后Init函数是通过g参数对插件进行初始化g参数中包含Proto文件的所有信息。最后的Generate和GenerateImports方法用于生成主体代码和对应的导入包代码。
其中 Name 方法返回插件的名字,这是 Go 语言的 Protobuf 实现的插件体系,和 protoc 插件的名字并无关系。然后 Init 函数是通过 g 参数对插件进行初始化g 参数中包含 Proto 文件的所有信息。最后的 Generate GenerateImports 方法用于生成主体代码和对应的导入包代码。
因此我们可以设计一个netrpcPlugin插件用于为标准库的RPC框架生成代码
因此我们可以设计一个 netrpcPlugin 插件,用于为标准库的 RPC 框架生成代码:
```go
import (
"github.com/golang/protobuf/protoc-gen-go/generator"
)
type netrpcPlugin struct{ *generator.Generator }
type netrpcPlugin struct{*generator.Generator}
func (p *netrpcPlugin) Name() string { return "netrpc" }
func (p *netrpcPlugin) Init(g *generator.Generator) { p.Generator = g }
@ -146,9 +146,9 @@ func (p *netrpcPlugin) Generate(file *generator.FileDescriptor) {
}
```
首先Name方法返回插件的名字。netrpcPlugin插件内置了一个匿名的`*generator.Generator`成员然后在Init初始化的时候用参数g进行初始化因此插件是从g参数对象继承了全部的公有方法。其中GenerateImports方法调用自定义的genImportCode函数生成导入代码。Generate方法调用自定义的genServiceCode方法生成每个服务的代码。
首先 Name 方法返回插件的名字。netrpcPlugin 插件内置了一个匿名的 `*generator.Generator` 成员,然后在 Init 初始化的时候用参数 g 进行初始化,因此插件是从 g 参数对象继承了全部的公有方法。其中 GenerateImports 方法调用自定义的 genImportCode 函数生成导入代码。Generate 方法调用自定义的 genServiceCode 方法生成每个服务的代码。
目前自定义的genImportCode和genServiceCode方法只是输出一行简单的注释
目前,自定义的 genImportCode genServiceCode 方法只是输出一行简单的注释:
```go
func (p *netrpcPlugin) genImportCode(file *generator.FileDescriptor) {
@ -156,11 +156,11 @@ func (p *netrpcPlugin) genImportCode(file *generator.FileDescriptor) {
}
func (p *netrpcPlugin) genServiceCode(svc *descriptor.ServiceDescriptorProto) {
p.P("// TODO: service code, Name = " + svc.GetName())
p.P("// TODO: service code, Name =" + svc.GetName())
}
```
要使用该插件需要先通过generator.RegisterPlugin函数注册插件可以在init函数中完成
要使用该插件需要先通过 generator.RegisterPlugin 函数注册插件,可以在 init 函数中完成:
```go
func init() {
@ -168,7 +168,7 @@ func init() {
}
```
因为Go语言的包只能静态导入我们无法向已经安装的protoc-gen-go添加我们新编写的插件。我们将重新克隆protoc-gen-go对应的main函数
因为 Go 语言的包只能静态导入,我们无法向已经安装的 protoc-gen-go 添加我们新编写的插件。我们将重新克隆 protoc-gen-go 对应的 main 函数:
```go
package main
@ -220,21 +220,21 @@ func main() {
}
```
为了避免对protoc-gen-go插件造成干扰我们将我们的可执行程序命名为protoc-gen-go-netrpc表示包含了netrpc插件。然后用以下命令重新编译hello.proto文件
为了避免对 protoc-gen-go 插件造成干扰,我们将我们的可执行程序命名为 protoc-gen-go-netrpc表示包含了 netrpc 插件。然后用以下命令重新编译 hello.proto 文件:
```
$ protoc --go-netrpc_out=plugins=netrpc:. hello.proto
```
其中`--go-netrpc_out`参数告知protoc编译器加载名为protoc-gen-go-netrpc的插件插件中的`plugins=netrpc`指示启用内部唯一的名为netrpc的netrpcPlugin插件。在新生成的hello.pb.go文件中将包含增加的注释代码。
其中 `--go-netrpc_out` 参数告知 protoc 编译器加载名为 protoc-gen-go-netrpc 的插件,插件中的 `plugins=netrpc` 指示启用内部唯一的名为 netrpc netrpcPlugin 插件。在新生成的 hello.pb.go 文件中将包含增加的注释代码。
至此手工定制的Protobuf代码生成插件终于可以工作了。
至此,手工定制的 Protobuf 代码生成插件终于可以工作了。
## 4.2.3 自动生成完整的RPC代码
## 4.2.3 自动生成完整的 RPC 代码
在前面的例子中我们已经构建了最小化的netrpcPlugin插件并且通过克隆protoc-gen-go的主程序创建了新的protoc-gen-go-netrpc的插件程序。现在开始继续完善netrpcPlugin插件最终目标是生成RPC安全接口。
在前面的例子中我们已经构建了最小化的 netrpcPlugin 插件,并且通过克隆 protoc-gen-go 的主程序创建了新的 protoc-gen-go-netrpc 的插件程序。现在开始继续完善 netrpcPlugin 插件,最终目标是生成 RPC 安全接口。
首先是自定义的genImportCode方法中生成导入包的代码
首先是自定义的 genImportCode 方法中生成导入包的代码:
```go
func (p *netrpcPlugin) genImportCode(file *generator.FileDescriptor) {
@ -242,9 +242,9 @@ func (p *netrpcPlugin) genImportCode(file *generator.FileDescriptor) {
}
```
然后要在自定义的genServiceCode方法中为每个服务生成相关的代码。分析可以发现每个服务最重要的是服务的名字然后每个服务有一组方法。而对于服务定义的方法最重要的是方法的名字还有输入参数和输出参数类型的名字。
然后要在自定义的 genServiceCode 方法中为每个服务生成相关的代码。分析可以发现每个服务最重要的是服务的名字,然后每个服务有一组方法。而对于服务定义的方法,最重要的是方法的名字,还有输入参数和输出参数类型的名字。
为此我们定义了一个ServiceSpec类型用于描述服务的元信息
为此我们定义了一个 ServiceSpec 类型,用于描述服务的元信息:
```go
type ServiceSpec struct {
@ -259,7 +259,7 @@ type ServiceMethodSpec struct {
}
```
然后我们新建一个buildServiceSpec方法用来解析每个服务的ServiceSpec元信息
然后我们新建一个 buildServiceSpec 方法用来解析每个服务的 ServiceSpec 元信息:
```go
func (p *netrpcPlugin) buildServiceSpec(
@ -281,9 +281,9 @@ func (p *netrpcPlugin) buildServiceSpec(
}
```
其中输入参数是`*descriptor.ServiceDescriptorProto`类型,完整描述了一个服务的所有信息。然后通过`svc.GetName()`就可以获取Protobuf文件中定义的服务的名字。Protobuf文件中的名字转为Go语言的名字后需要通过`generator.CamelCase`函数进行一次转换。类似的在for循环中我们通过`m.GetName()`获取方法的名字然后再转为Go语言中对应的名字。比较复杂的是对输入和输出参数名字的解析首先需要通过`m.GetInputType()`获取输入参数的类型,然后通过`p.ObjectNamed`获取类型对应的类对象信息,最后获取类对象的名字。
其中输入参数是 `*descriptor.ServiceDescriptorProto` 类型,完整描述了一个服务的所有信息。然后通过 `svc.GetName()` 就可以获取 Protobuf 文件中定义的服务的名字。Protobuf 文件中的名字转为 Go 语言的名字后,需要通过 `generator.CamelCase` 函数进行一次转换。类似的,在 for 循环中我们通过 `m.GetName()` 获取方法的名字,然后再转为 Go 语言中对应的名字。比较复杂的是对输入和输出参数名字的解析:首先需要通过 `m.GetInputType()` 获取输入参数的类型,然后通过 `p.ObjectNamed` 获取类型对应的类对象信息,最后获取类对象的名字。
然后我们就可以基于buildServiceSpec方法构造的服务的元信息生成服务的代码
然后我们就可以基于 buildServiceSpec 方法构造的服务的元信息生成服务的代码:
```go
func (p *netrpcPlugin) genServiceCode(svc *descriptor.ServiceDescriptorProto) {
@ -300,7 +300,7 @@ func (p *netrpcPlugin) genServiceCode(svc *descriptor.ServiceDescriptorProto) {
}
```
为了便于维护我们基于Go语言的模板来生成服务代码其中tmplService是服务的模板。
为了便于维护,我们基于 Go 语言的模板来生成服务代码,其中 tmplService 是服务的模板。
在编写模板之前,我们先查看下我们期望生成的最终代码大概是什么样子:
@ -335,7 +335,7 @@ func (p *HelloServiceClient) Hello(in String, out *String) error {
}
```
其中HelloService是服务名字同时还有一系列的方法相关的名字。
其中 HelloService 是服务名字,同时还有一系列的方法相关的名字。
参考最终要生成的代码可以构建如下模板:
@ -384,5 +384,5 @@ func (p *{{$root.ServiceName}}Client) {{$m.MethodName}}(
`
```
当Protobuf的插件定制工作完成后每次hello.proto文件中RPC服务的变化都可以自动生成代码。也可以通过更新插件的模板调整或增加生成代码的内容。在掌握了定制Protobuf插件技术后你将彻底拥有这个技术。
Protobuf 的插件定制工作完成后,每次 hello.proto 文件中 RPC 服务的变化都可以自动生成代码。也可以通过更新插件的模板,调整或增加生成代码的内容。在掌握了定制 Protobuf 插件技术后,你将彻底拥有这个技术。

View File

@ -1,10 +1,10 @@
# 4.3 玩转RPC
# 4.3 玩转 RPC
在不同的场景中RPC有着不同的需求因此开源的社区就诞生了各种RPC框架。本节我们将尝试Go内置RPC框架在一些比较特殊场景的用法。
在不同的场景中 RPC 有着不同的需求,因此开源的社区就诞生了各种 RPC 框架。本节我们将尝试 Go 内置 RPC 框架在一些比较特殊场景的用法。
## 4.3.1 客户端RPC的实现原理
## 4.3.1 客户端 RPC 的实现原理
Go语言的RPC库最简单的使用方式是通过`Client.Call`方法进行同步阻塞调用,该方法的实现如下:
Go 语言的 RPC 库最简单的使用方式是通过 `Client.Call` 方法进行同步阻塞调用,该方法的实现如下:
```go
func (client *Client) Call(
@ -16,9 +16,9 @@ func (client *Client) Call(
}
```
首先通过`Client.Go`方法进行一次异步调用,返回一个表示这次调用的`Call`结构体。然后等待`Call`结构体的Done管道返回调用结果。
首先通过 `Client.Go` 方法进行一次异步调用,返回一个表示这次调用的 `Call` 结构体。然后等待 `Call` 结构体的 Done 管道返回调用结果。
我们也可以通过`Client.Go`方法异步调用前面的HelloService服务
我们也可以通过 `Client.Go` 方法异步调用前面的 HelloService 服务:
```go
func doClientWork(client *rpc.Client) {
@ -37,9 +37,9 @@ func doClientWork(client *rpc.Client) {
}
```
在异步调用命令发出后一般会执行其他的任务因此异步调用的输入参数和返回值可以通过返回的Call变量进行获取。
在异步调用命令发出后,一般会执行其他的任务,因此异步调用的输入参数和返回值可以通过返回的 Call 变量进行获取。
执行异步调用的`Client.Go`方法实现如下:
执行异步调用的 `Client.Go` 方法实现如下:
```go
func (client *Client) Go(
@ -58,9 +58,9 @@ func (client *Client) Go(
}
```
首先是构造一个表示当前调用的call变量然后通过`client.send`将call的完整参数发送到RPC框架。`client.send`方法调用是线程安全的因此可以从多个Goroutine同时向同一个RPC链接发送调用指令。
首先是构造一个表示当前调用的 call 变量,然后通过 `client.send` call 的完整参数发送到 RPC 框架。`client.send` 方法调用是线程安全的,因此可以从多个 Goroutine 同时向同一个 RPC 链接发送调用指令。
当调用完成或者发生错误时,将调用`call.done`方法通知完成:
当调用完成或者发生错误时,将调用 `call.done` 方法通知完成:
```go
func (call *Call) done() {
@ -74,13 +74,13 @@ func (call *Call) done() {
}
```
`Call.done`方法的实现可以得知`call.Done`管道会将处理后的call返回。
`Call.done` 方法的实现可以得知 `call.Done` 管道会将处理后的 call 返回。
## 4.3.2 基于RPC实现Watch功能
## 4.3.2 基于 RPC 实现 Watch 功能
在很多系统中都提供了Watch监视功能的接口当系统满足某种条件时Watch方法返回监控的结果。在这里我们可以尝试通过RPC框架实现一个基本的Watch功能。如前文所描述因为`client.send`是线程安全的我们也可以通过在不同的Goroutine中同时并发阻塞调用RPC方法。通过在一个独立的Goroutine中调用Watch函数进行监控。
在很多系统中都提供了 Watch 监视功能的接口,当系统满足某种条件时 Watch 方法返回监控的结果。在这里我们可以尝试通过 RPC 框架实现一个基本的 Watch 功能。如前文所描述,因为 `client.send` 是线程安全的,我们也可以通过在不同的 Goroutine 中同时并发阻塞调用 RPC 方法。通过在一个独立的 Goroutine 中调用 Watch 函数进行监控。
为了便于演示我们计划通过RPC构造一个简单的内存KV数据库。首先定义服务如下
为了便于演示,我们计划通过 RPC 构造一个简单的内存 KV 数据库。首先定义服务如下:
```go
type KVStoreService struct {
@ -97,9 +97,9 @@ func NewKVStoreService() *KVStoreService {
}
```
其中`m`成员是一个map类型用于存储KV数据。`filter`成员对应每个Watch调用时定义的过滤器函数列表。而`mu`成员为互斥锁用于在多个Goroutine访问或修改时对其它成员提供保护。
其中 `m` 成员是一个 map 类型,用于存储 KV 数据。`filter` 成员对应每个 Watch 调用时定义的过滤器函数列表。而 `mu` 成员为互斥锁,用于在多个 Goroutine 访问或修改时对其它成员提供保护。
然后就是Get和Set方法
然后就是 Get Set 方法:
```go
func (p *KVStoreService) Get(key string, value *string) error {
@ -131,9 +131,9 @@ func (p *KVStoreService) Set(kv [2]string, reply *struct{}) error {
}
```
在Set方法中输入参数是key和value组成的数组用一个匿名的空结构体表示忽略了输出参数。当修改某个key对应的值时会调用每一个过滤器函数。
Set 方法中,输入参数是 key value 组成的数组,用一个匿名的空结构体表示忽略了输出参数。当修改某个 key 对应的值时会调用每一个过滤器函数。
而过滤器列表在Watch方法中提供
而过滤器列表在 Watch 方法中提供:
```go
func (p *KVStoreService) Watch(timeoutSecond int, keyChanged *string) error {
@ -156,9 +156,9 @@ func (p *KVStoreService) Watch(timeoutSecond int, keyChanged *string) error {
}
```
Watch方法的输入参数是超时的秒数。当有key变化时将key作为返回值返回。如果超过时间后依然没有key被修改则返回超时的错误。Watch的实现中用唯一的id表示每个Watch调用然后根据id将自身对应的过滤器函数注册到`p.filter`列表。
Watch 方法的输入参数是超时的秒数。当有 key 变化时将 key 作为返回值返回。如果超过时间后依然没有 key 被修改则返回超时的错误。Watch 的实现中,用唯一的 id 表示每个 Watch 调用,然后根据 id 将自身对应的过滤器函数注册到 `p.filter` 列表。
KVStoreService服务的注册和启动过程我们不再赘述。下面我们看看如何从客户端使用Watch方法
KVStoreService 服务的注册和启动过程我们不再赘述。下面我们看看如何从客户端使用 Watch 方法:
```go
func doClientWork(client *rpc.Client) {
@ -183,13 +183,13 @@ func doClientWork(client *rpc.Client) {
}
```
首先启动一个独立的Goroutine监控key的变化。同步的watch调用会阻塞直到有key发生变化或者超时。然后在通过Set方法修改KV值时服务器会将变化的key通过Watch方法返回。这样我们就可以实现对某些状态的监控。
首先启动一个独立的 Goroutine 监控 key 的变化。同步的 watch 调用会阻塞,直到有 key 发生变化或者超时。然后在通过 Set 方法修改 KV 值时,服务器会将变化的 key 通过 Watch 方法返回。这样我们就可以实现对某些状态的监控。
## 4.3.3 反向RPC
## 4.3.3 反向 RPC
通常的RPC是基于C/S结构RPC的服务端对应网络的服务器RPC的客户端也对应网络客户端。但是对于一些特殊场景比如在公司内网提供一个RPC服务但是在外网无法链接到内网的服务器。这种时候我们可以参考类似反向代理的技术首先从内网主动链接到外网的TCP服务器然后基于TCP链接向外网提供RPC服务。
通常的 RPC 是基于 C/S 结构RPC 的服务端对应网络的服务器RPC 的客户端也对应网络客户端。但是对于一些特殊场景,比如在公司内网提供一个 RPC 服务,但是在外网无法链接到内网的服务器。这种时候我们可以参考类似反向代理的技术,首先从内网主动链接到外网的 TCP 服务器,然后基于 TCP 链接向外网提供 RPC 服务。
以下是启动反向RPC服务的代码
以下是启动反向 RPC 服务的代码:
```go
func main() {
@ -208,9 +208,9 @@ func main() {
}
```
反向RPC的内网服务将不再主动提供TCP监听服务而是首先主动链接到对方的TCP服务器。然后基于每个建立的TCP链接向对方提供RPC服务。
反向 RPC 的内网服务将不再主动提供 TCP 监听服务,而是首先主动链接到对方的 TCP 服务器。然后基于每个建立的 TCP 链接向对方提供 RPC 服务。
而RPC客户端则需要在一个公共的地址提供一个TCP服务用于接受RPC服务器的链接请求
RPC 客户端则需要在一个公共的地址提供一个 TCP 服务,用于接受 RPC 服务器的链接请求:
```go
func main() {
@ -236,9 +236,9 @@ func main() {
}
```
当每个链接建立后基于网络链接构造RPC客户端对象并发送到clientChan管道。
当每个链接建立后,基于网络链接构造 RPC 客户端对象并发送到 clientChan 管道。
客户端执行RPC调用的操作在doClientWork函数完成
客户端执行 RPC 调用的操作在 doClientWork 函数完成:
```go
func doClientWork(clientChan <-chan *rpc.Client) {
@ -255,14 +255,14 @@ func doClientWork(clientChan <-chan *rpc.Client) {
}
```
首先从管道去取一个RPC客户端对象并且通过defer语句指定在函数退出前关闭客户端。然后是执行正常的RPC调用。
首先从管道去取一个 RPC 客户端对象,并且通过 defer 语句指定在函数退出前关闭客户端。然后是执行正常的 RPC 调用。
## 4.3.4 上下文信息
基于上下文我们可以针对不同客户端提供定制化的RPC服务。我们可以通过为每个链接提供独立的RPC服务来实现对上下文特性的支持。
基于上下文我们可以针对不同客户端提供定制化的 RPC 服务。我们可以通过为每个链接提供独立的 RPC 服务来实现对上下文特性的支持。
首先改造HelloService里面增加了对应链接的conn成员
首先改造 HelloService里面增加了对应链接的 conn 成员:
```go
type HelloService struct {
@ -270,7 +270,7 @@ type HelloService struct {
}
```
然后为每个链接启动独立的RPC服务
然后为每个链接启动独立的 RPC 服务:
```go
func main() {
@ -296,7 +296,7 @@ func main() {
}
```
Hello方法中就可以根据conn成员识别不同链接的RPC调用
Hello 方法中就可以根据 conn 成员识别不同链接的 RPC 调用:
```go
func (p *HelloService) Hello(request string, reply *string) error {
@ -305,7 +305,7 @@ func (p *HelloService) Hello(request string, reply *string) error {
}
```
基于上下文信息我们可以方便地为RPC服务增加简单的登陆状态的验证
基于上下文信息,我们可以方便地为 RPC 服务增加简单的登陆状态的验证:
```go
type HelloService struct {
@ -331,4 +331,4 @@ func (p *HelloService) Hello(request string, reply *string) error {
}
```
这样可以要求在客户端链接RPC服务时首先要执行登陆操作登陆成功后才能正常执行其他的服务。
这样可以要求在客户端链接 RPC 服务时,首先要执行登陆操作,登陆成功后才能正常执行其他的服务。

View File

@ -1,22 +1,22 @@
# 4.4 gRPC入门
# 4.4 gRPC 入门
gRPC是Google公司基于Protobuf开发的跨语言的开源RPC框架。gRPC基于HTTP/2协议设计可以基于一个HTTP/2链接提供多个服务对于移动设备更加友好。本节将讲述gRPC的简单用法。
gRPC Google 公司基于 Protobuf 开发的跨语言的开源 RPC 框架。gRPC 基于 HTTP/2 协议设计,可以基于一个 HTTP/2 链接提供多个服务,对于移动设备更加友好。本节将讲述 gRPC 的简单用法。
## 4.4.1 gRPC技术栈
## 4.4.1 gRPC 技术栈
Go语言的gRPC技术栈如图4-1所示
Go 语言的 gRPC 技术栈如图 4-1 所示:
![](../images/ch4-1-grpc-go-stack.png)
*图4-1 gRPC技术栈*
*图 4-1 gRPC 技术栈*
最底层为TCP或Unix Socket协议在此之上是HTTP/2协议的实现然后在HTTP/2协议之上又构建了针对Go语言的gRPC核心库。应用程序通过gRPC插件生产的Stub代码和gRPC核心库通信也可以直接和gRPC核心库通信。
最底层为 TCP Unix Socket 协议,在此之上是 HTTP/2 协议的实现,然后在 HTTP/2 协议之上又构建了针对 Go 语言的 gRPC 核心库。应用程序通过 gRPC 插件生产的 Stub 代码和 gRPC 核心库通信,也可以直接和 gRPC 核心库通信。
## 4.4.2 gRPC入门
## 4.4.2 gRPC 入门
如果从Protobuf的角度看gRPC只不过是一个针对service接口生成代码的生成器。我们在本章的第二节中手工实现了一个简单的Protobuf代码生成器插件只不过当时生成的代码是适配标准库的RPC框架的。现在我们将学习gRPC的用法。
如果从 Protobuf 的角度看gRPC 只不过是一个针对 service 接口生成代码的生成器。我们在本章的第二节中手工实现了一个简单的 Protobuf 代码生成器插件,只不过当时生成的代码是适配标准库的 RPC 框架的。现在我们将学习 gRPC 的用法。
创建hello.proto文件定义HelloService接口
创建 hello.proto 文件,定义 HelloService 接口:
```proto
syntax = "proto3";
@ -32,13 +32,13 @@ service HelloService {
}
```
使用protoc-gen-go内置的gRPC插件生成gRPC代码
使用 protoc-gen-go 内置的 gRPC 插件生成 gRPC 代码:
```
$ protoc --go_out=plugins=grpc:. hello.proto
```
gRPC插件会为服务端和客户端生成不同的接口
gRPC 插件会为服务端和客户端生成不同的接口:
```go
type HelloServiceServer interface {
@ -50,9 +50,9 @@ type HelloServiceClient interface {
}
```
gRPC通过context.Context参数为每个方法调用提供了上下文支持。客户端在调用方法的时候可以通过可选的grpc.CallOption类型的参数提供额外的上下文信息。
gRPC 通过 context.Context 参数,为每个方法调用提供了上下文支持。客户端在调用方法的时候,可以通过可选的 grpc.CallOption 类型的参数提供额外的上下文信息。
基于服务端的HelloServiceServer接口可以重新实现HelloService服务
基于服务端的 HelloServiceServer 接口可以重新实现 HelloService 服务:
```go
type HelloServiceImpl struct{}
@ -65,7 +65,7 @@ func (p *HelloServiceImpl) Hello(
}
```
gRPC服务的启动流程和标准库的RPC服务启动流程类似
gRPC 服务的启动流程和标准库的 RPC 服务启动流程类似:
```go
func main() {
@ -80,9 +80,9 @@ func main() {
}
```
首先是通过`grpc.NewServer()`构造一个gRPC服务对象然后通过gRPC插件生成的RegisterHelloServiceServer函数注册我们实现的HelloServiceImpl服务。然后通过`grpcServer.Serve(lis)`在一个监听端口上提供gRPC服务。
首先是通过 `grpc.NewServer()` 构造一个 gRPC 服务对象,然后通过 gRPC 插件生成的 RegisterHelloServiceServer 函数注册我们实现的 HelloServiceImpl 服务。然后通过 `grpcServer.Serve(lis)` 在一个监听端口上提供 gRPC 服务。
然后就可以通过客户端链接gRPC服务了
然后就可以通过客户端链接 gRPC 服务了:
```go
func main() {
@ -101,15 +101,15 @@ func main() {
}
```
其中grpc.Dial负责和gRPC服务建立链接然后NewHelloServiceClient函数基于已经建立的链接构造HelloServiceClient对象。返回的client其实是一个HelloServiceClient接口对象通过接口定义的方法就可以调用服务端对应的gRPC服务提供的方法。
其中 grpc.Dial 负责和 gRPC 服务建立链接,然后 NewHelloServiceClient 函数基于已经建立的链接构造 HelloServiceClient 对象。返回的 client 其实是一个 HelloServiceClient 接口对象,通过接口定义的方法就可以调用服务端对应的 gRPC 服务提供的方法。
gRPC和标准库的RPC框架有一个区别gRPC生成的接口并不支持异步调用。不过我们可以在多个Goroutine之间安全地共享gRPC底层的HTTP/2链接因此可以通过在另一个Goroutine阻塞调用的方式模拟异步调用。
gRPC 和标准库的 RPC 框架有一个区别gRPC 生成的接口并不支持异步调用。不过我们可以在多个 Goroutine 之间安全地共享 gRPC 底层的 HTTP/2 链接,因此可以通过在另一个 Goroutine 阻塞调用的方式模拟异步调用。
## 4.4.3 gRPC流
## 4.4.3 gRPC
RPC是远程函数调用因此每次调用的函数参数和返回值不能太大否则将严重影响每次调用的响应时间。因此传统的RPC方法调用对于上传和下载较大数据量场景并不适合。同时传统RPC模式也不适用于对时间不确定的订阅和发布模式。为此gRPC框架针对服务器端和客户端分别提供了流特性。
RPC 是远程函数调用,因此每次调用的函数参数和返回值不能太大,否则将严重影响每次调用的响应时间。因此传统的 RPC 方法调用对于上传和下载较大数据量场景并不适合。同时传统 RPC 模式也不适用于对时间不确定的订阅和发布模式。为此gRPC 框架针对服务器端和客户端分别提供了流特性。
服务端或客户端的单向流是双向流的特例我们在HelloService增加一个支持双向流的Channel方法
服务端或客户端的单向流是双向流的特例,我们在 HelloService 增加一个支持双向流的 Channel 方法:
```proto
service HelloService {
@ -119,9 +119,9 @@ service HelloService {
}
```
关键字stream指定启用流特性参数部分是接收客户端参数的流返回值是返回给客户端的流。
关键字 stream 指定启用流特性,参数部分是接收客户端参数的流,返回值是返回给客户端的流。
重新生成代码可以看到接口中新增加的Channel方法的定义
重新生成代码可以看到接口中新增加的 Channel 方法的定义:
```go
type HelloServiceServer interface {
@ -138,9 +138,9 @@ type HelloServiceClient interface {
}
```
在服务端的Channel方法参数是一个新的HelloService_ChannelServer类型的参数可以用于和客户端双向通信。客户端的Channel方法返回一个HelloService_ChannelClient类型的返回值可以用于和服务端进行双向通信。
在服务端的 Channel 方法参数是一个新的 HelloService_ChannelServer 类型的参数,可以用于和客户端双向通信。客户端的 Channel 方法返回一个 HelloService_ChannelClient 类型的返回值,可以用于和服务端进行双向通信。
HelloService_ChannelServer和HelloService_ChannelClient均为接口类型
HelloService_ChannelServer HelloService_ChannelClient 均为接口类型:
```go
type HelloService_ChannelServer interface {
@ -156,7 +156,7 @@ type HelloService_ChannelClient interface {
}
```
可以发现服务端和客户端的流辅助接口均定义了Send和Recv方法用于流数据的双向通信。
可以发现服务端和客户端的流辅助接口均定义了 Send Recv 方法用于流数据的双向通信。
现在我们可以实现流服务:
@ -181,9 +181,9 @@ func (p *HelloServiceImpl) Channel(stream HelloService_ChannelServer) error {
}
```
服务端在循环中接收客户端发来的数据如果遇到io.EOF表示客户端流被关闭如果函数退出表示服务端流关闭。生成返回的数据通过流发送给客户端双向流数据的发送和接收都是完全独立的行为。需要注意的是发送和接收的操作并不需要一一对应用户可以根据真实场景进行组织代码。
服务端在循环中接收客户端发来的数据,如果遇到 io.EOF 表示客户端流被关闭,如果函数退出表示服务端流关闭。生成返回的数据通过流发送给客户端,双向流数据的发送和接收都是完全独立的行为。需要注意的是,发送和接收的操作并不需要一一对应,用户可以根据真实场景进行组织代码。
客户端需要先调用Channel方法获取返回的流对象
客户端需要先调用 Channel 方法获取返回的流对象:
```go
stream, err := client.Channel(context.Background())
@ -192,7 +192,7 @@ if err != nil {
}
```
在客户端我们将发送和接收操作放到两个独立的Goroutine。首先是向服务端发送数据
在客户端我们将发送和接收操作放到两个独立的 Goroutine。首先是向服务端发送数据
```go
go func() {
@ -224,9 +224,9 @@ for {
## 4.4.4 发布和订阅模式
在前一节中我们基于Go内置的RPC库实现了一个简化版的Watch方法。基于Watch的思路虽然也可以构造发布和订阅系统但是因为RPC缺乏流机制导致每次只能返回一个结果。在发布和订阅模式中由调用者主动发起的发布行为类似一个普通函数调用而被动的订阅者则类似gRPC客户端单向流中的接收者。现在我们可以尝试基于gRPC的流特性构造一个发布和订阅系统。
在前一节中,我们基于 Go 内置的 RPC 库实现了一个简化版的 Watch 方法。基于 Watch 的思路虽然也可以构造发布和订阅系统,但是因为 RPC 缺乏流机制导致每次只能返回一个结果。在发布和订阅模式中,由调用者主动发起的发布行为类似一个普通函数调用,而被动的订阅者则类似 gRPC 客户端单向流中的接收者。现在我们可以尝试基于 gRPC 的流特性构造一个发布和订阅系统。
发布订阅是一个常见的设计模式开源社区中已经存在很多该模式的实现。其中docker项目中提供了一个pubsub的极简实现下面是基于pubsub包实现的本地发布订阅代码
发布订阅是一个常见的设计模式,开源社区中已经存在很多该模式的实现。其中 docker 项目中提供了一个 pubsub 的极简实现,下面是基于 pubsub 包实现的本地发布订阅代码:
```go
import (
@ -269,9 +269,9 @@ func main() {
}
```
其中`pubsub.NewPublisher`构造一个发布对象,`p.SubscribeTopic()`可以通过函数筛选感兴趣的主题进行订阅。
其中 `pubsub.NewPublisher` 构造一个发布对象,`p.SubscribeTopic()` 可以通过函数筛选感兴趣的主题进行订阅。
现在尝试基于gRPC和pubsub包提供一个跨网络的发布和订阅系统。首先通过Protobuf定义一个发布订阅服务接口
现在尝试基于 gRPC pubsub 包,提供一个跨网络的发布和订阅系统。首先通过 Protobuf 定义一个发布订阅服务接口:
```protobuf
service PubsubService {
@ -280,7 +280,7 @@ service PubsubService {
}
```
其中Publish是普通的RPC方法Subscribe则是一个单向的流服务。然后gRPC插件会为服务端和客户端生成对应的接口
其中 Publish 是普通的 RPC 方法Subscribe 则是一个单向的流服务。然后 gRPC 插件会为服务端和客户端生成对应的接口:
```go
type PubsubServiceServer interface {
@ -300,7 +300,7 @@ type PubsubService_SubscribeServer interface {
}
```
因为Subscribe是服务端的单向流因此生成的PubsubService_SubscribeServer接口中只有Send方法。
因为 Subscribe 是服务端的单向流,因此生成的 PubsubService_SubscribeServer 接口中只有 Send 方法。
然后就可以实现发布和订阅服务了:
@ -407,5 +407,5 @@ func main() {
}
```
到此我们就基于gRPC简单实现了一个跨网络的发布和订阅服务。
到此我们就基于 gRPC 简单实现了一个跨网络的发布和订阅服务。

View File

@ -1,10 +1,10 @@
# 4.5 gRPC进阶
# 4.5 gRPC 进阶
作为一个基础的RPC框架安全和扩展是经常遇到的问题。本节将简单介绍如何对gRPC进行安全认证。然后介绍通过gRPC的截取器特性以及如何通过截取器优雅地实现Token认证、调用跟踪以及Panic捕获等特性。最后介绍了gRPC服务如何和其他Web服务共存。
作为一个基础的 RPC 框架,安全和扩展是经常遇到的问题。本节将简单介绍如何对 gRPC 进行安全认证。然后介绍通过 gRPC 的截取器特性,以及如何通过截取器优雅地实现 Token 认证、调用跟踪以及 Panic 捕获等特性。最后介绍了 gRPC 服务如何和其他 Web 服务共存。
## 4.5.1 证书认证
gRPC建立在HTTP/2协议之上对TLS提供了很好的支持。我们前面章节中gRPC的服务都没有提供证书支持因此客户端在链接服务器中通过`grpc.WithInsecure()`选项跳过了对服务器证书的验证。没有启用证书的gRPC服务在和客户端进行的是明文通讯信息面临被任何第三方监听的风险。为了保障gRPC通信不被第三方监听篡改或伪造我们可以对服务器启动TLS加密特性。
gRPC 建立在 HTTP/2 协议之上,对 TLS 提供了很好的支持。我们前面章节中 gRPC 的服务都没有提供证书支持,因此客户端在链接服务器中通过 `grpc.WithInsecure()` 选项跳过了对服务器证书的验证。没有启用证书的 gRPC 服务在和客户端进行的是明文通讯,信息面临被任何第三方监听的风险。为了保障 gRPC 通信不被第三方监听篡改或伪造,我们可以对服务器启动 TLS 加密特性。
可以用以下命令为服务器和客户端分别生成私钥和证书:
@ -20,9 +20,9 @@ $ openssl req -new -x509 -days 3650 \
-key client.key -out client.crt
```
以上命令将生成server.key、server.crt、client.key和client.crt四个文件。其中以.key为后缀名的是私钥文件需要妥善保管。以.crt为后缀名是证书文件也可以简单理解为公钥文件并不需要秘密保存。在subj参数中的`/CN=server.grpc.io`表示服务器的名字为`server.grpc.io`,在验证服务器的证书时需要用到该信息。
以上命令将生成 server.key、server.crt、client.key client.crt 四个文件。其中以. key 为后缀名的是私钥文件,需要妥善保管。以. crt 为后缀名是证书文件,也可以简单理解为公钥文件,并不需要秘密保存。在 subj 参数中的 `/CN=server.grpc.io` 表示服务器的名字为 `server.grpc.io`,在验证服务器的证书时需要用到该信息。
有了证书之后我们就可以在启动gRPC服务时传入证书选项参数
有了证书之后,我们就可以在启动 gRPC 服务时传入证书选项参数:
```go
func main() {
@ -37,7 +37,7 @@ func main() {
}
```
其中credentials.NewServerTLSFromFile函数是从文件为服务器构造证书对象然后通过grpc.Creds(creds)函数将证书包装为选项后作为参数传入grpc.NewServer函数。
其中 credentials.NewServerTLSFromFile 函数是从文件为服务器构造证书对象,然后通过 grpc.Creds(creds) 函数将证书包装为选项后作为参数传入 grpc.NewServer 函数。
在客户端基于服务器的证书和服务器名字就可以对服务器进行验证:
@ -62,7 +62,7 @@ func main() {
}
```
其中credentials.NewClientTLSFromFile是构造客户端用的证书对象第一个参数是服务器的证书文件第二个参数是签发证书的服务器的名字。然后通过grpc.WithTransportCredentials(creds)将证书对象转为参数选项传人grpc.Dial函数。
其中 credentials.NewClientTLSFromFile 是构造客户端用的证书对象,第一个参数是服务器的证书文件,第二个参数是签发证书的服务器的名字。然后通过 grpc.WithTransportCredentials(creds) 将证书对象转为参数选项传人 grpc.Dial 函数。
以上这种方式,需要提前将服务器的证书告知客户端,这样客户端在链接服务器时才能进行对服务器证书认证。在复杂的网络环境中,服务器证书的传输本身也是一个非常危险的问题。如果在中间某个环节,服务器证书被监听或替换那么对服务器的认证也将不再可靠。
@ -90,9 +90,9 @@ $ openssl x509 -req -sha256 \
-out server.crt
```
签名的过程中引入了一个新的以.csr为后缀名的文件它表示证书签名请求文件。在证书签名完成之后可以删除.csr文件。
签名的过程中引入了一个新的以. csr 为后缀名的文件,它表示证书签名请求文件。在证书签名完成之后可以删除. csr 文件。
然后在客户端就可以基于CA证书对服务器进行证书验证
然后在客户端就可以基于 CA 证书对服务器进行证书验证:
```go
func main() {
@ -128,9 +128,9 @@ func main() {
}
```
在新的客户端代码中我们不再直接依赖服务器端证书文件。在credentials.NewTLS函数调用中客户端通过引入一个CA根证书和服务器的名字来实现对服务器进行验证。客户端在链接服务器时会首先请求服务器的证书然后使用CA根证书对收到的服务器端证书进行验证。
在新的客户端代码中,我们不再直接依赖服务器端证书文件。在 credentials.NewTLS 函数调用中,客户端通过引入一个 CA 根证书和服务器的名字来实现对服务器进行验证。客户端在链接服务器时会首先请求服务器的证书,然后使用 CA 根证书对收到的服务器端证书进行验证。
如果客户端的证书也采用CA根证书签名的话服务器端也可以对客户端进行证书认证。我们用CA根证书对客户端证书签名
如果客户端的证书也采用 CA 根证书签名的话,服务器端也可以对客户端进行证书认证。我们用 CA 根证书对客户端证书签名:
```
$ openssl req -new \
@ -143,7 +143,7 @@ $ openssl x509 -req -sha256 \
-out client.crt
```
因为引入了CA根证书签名在启动服务器时同样要配置根证书
因为引入了 CA 根证书签名,在启动服务器时同样要配置根证书:
```go
func main() {
@ -172,15 +172,15 @@ func main() {
}
```
服务器端同样改用credentials.NewTLS函数生成证书通过ClientCAs选择CA根证书并通过ClientAuth选项启用对客户端进行验证。
服务器端同样改用 credentials.NewTLS 函数生成证书,通过 ClientCAs 选择 CA 根证书,并通过 ClientAuth 选项启用对客户端进行验证。
到此我们就实现了一个服务器和客户端进行双向证书验证的通信可靠的gRPC系统。
到此我们就实现了一个服务器和客户端进行双向证书验证的通信可靠的 gRPC 系统。
## 4.5.2 Token认证
## 4.5.2 Token 认证
前面讲述的基于证书的认证是针对每个gRPC链接的认证。gRPC还为每个gRPC方法调用提供了认证支持这样就基于用户Token对不同的方法访问进行权限管理。
前面讲述的基于证书的认证是针对每个 gRPC 链接的认证。gRPC 还为每个 gRPC 方法调用提供了认证支持,这样就基于用户 Token 对不同的方法访问进行权限管理。
要实现对每个gRPC方法进行认证需要实现grpc.PerRPCCredentials接口
要实现对每个 gRPC 方法进行认证,需要实现 grpc.PerRPCCredentials 接口:
```go
type PerRPCCredentials interface {
@ -202,9 +202,9 @@ type PerRPCCredentials interface {
}
```
在GetRequestMetadata方法中返回认证需要的必要信息。RequireTransportSecurity方法表示是否要求底层使用安全链接。在真实的环境中建议必须要求底层启用安全的链接否则认证信息有泄露和被篡改的风险。
GetRequestMetadata 方法中返回认证需要的必要信息。RequireTransportSecurity 方法表示是否要求底层使用安全链接。在真实的环境中建议必须要求底层启用安全的链接,否则认证信息有泄露和被篡改的风险。
我们可以创建一个Authentication类型用于实现用户名和密码的认证
我们可以创建一个 Authentication 类型,用于实现用户名和密码的认证:
```go
type Authentication struct {
@ -222,9 +222,9 @@ func (a *Authentication) RequireTransportSecurity() bool {
}
```
在GetRequestMetadata方法中我们返回地认证信息包装login和password两个信息。为了演示代码简单RequireTransportSecurity方法表示不要求底层使用安全链接。
GetRequestMetadata 方法中,我们返回地认证信息包装 login password 两个信息。为了演示代码简单RequireTransportSecurity 方法表示不要求底层使用安全链接。
然后在每次请求gRPC服务时就可以将Token信息作为参数选项传人
然后在每次请求 gRPC 服务时就可以将 Token 信息作为参数选项传人:
```go
func main() {
@ -243,12 +243,12 @@ func main() {
}
```
通过grpc.WithPerRPCCredentials函数将Authentication对象转为grpc.Dial参数。因为这里没有启用安全链接需要传人grpc.WithInsecure()表示忽略证书认证。
通过 grpc.WithPerRPCCredentials 函数将 Authentication 对象转为 grpc.Dial 参数。因为这里没有启用安全链接,需要传人 grpc.WithInsecure() 表示忽略证书认证。
然后在gRPC服务端的每个方法中通过Authentication类型的Auth方法进行身份认证
然后在 gRPC 服务端的每个方法中通过 Authentication 类型的 Auth 方法进行身份认证:
```go
type grpcServer struct { auth *Authentication }
type grpcServer struct {auth *Authentication}
func (p *grpcServer) SomeMethod(
ctx context.Context, in *HelloRequest,
@ -257,7 +257,7 @@ func (p *grpcServer) SomeMethod(
return nil, err
}
return &HelloReply{Message: "Hello " + in.Name}, nil
return &HelloReply{Message: "Hello" + in.Name}, nil
}
func (a *Authentication) Auth(ctx context.Context) error {
@ -280,13 +280,13 @@ func (a *Authentication) Auth(ctx context.Context) error {
}
```
详细地认证工作主要在Authentication.Auth方法中完成。首先通过metadata.FromIncomingContext从ctx上下文中获取元信息然后取出相应的认证信息进行认证。如果认证失败则返回一个codes.Unauthenticated类型地错误。
详细地认证工作主要在 Authentication.Auth 方法中完成。首先通过 metadata.FromIncomingContext ctx 上下文中获取元信息,然后取出相应的认证信息进行认证。如果认证失败,则返回一个 codes.Unauthenticated 类型地错误。
## 4.5.3 截取器
gRPC中的grpc.UnaryInterceptor和grpc.StreamInterceptor分别对普通方法和流方法提供了截取器的支持。我们这里简单介绍普通方法的截取器用法。
gRPC 中的 grpc.UnaryInterceptor grpc.StreamInterceptor 分别对普通方法和流方法提供了截取器的支持。我们这里简单介绍普通方法的截取器用法。
要实现普通方法的截取器需要为grpc.UnaryInterceptor的参数实现一个函数
要实现普通方法的截取器,需要为 grpc.UnaryInterceptor 的参数实现一个函数:
```go
func filter(ctx context.Context,
@ -298,19 +298,19 @@ func filter(ctx context.Context,
}
```
函数的ctx和req参数就是每个普通的RPC方法的前两个参数。第三个info参数表示当前是对应的那个gRPC方法第四个handler参数对应当前的gRPC方法函数。上面的函数中首先是日志输出info参数然后调用handler对应的gRPC方法函数。
函数的 ctx req 参数就是每个普通的 RPC 方法的前两个参数。第三个 info 参数表示当前是对应的那个 gRPC 方法,第四个 handler 参数对应当前的 gRPC 方法函数。上面的函数中首先是日志输出 info 参数,然后调用 handler 对应的 gRPC 方法函数。
要使用filter截取器函数只需要在启动gRPC服务时作为参数输入即可
要使用 filter 截取器函数,只需要在启动 gRPC 服务时作为参数输入即可:
```go
server := grpc.NewServer(grpc.UnaryInterceptor(filter))
```
然后服务器在收到每个gRPC方法调用之前会首先输出一行日志然后再调用对方的方法。
然后服务器在收到每个 gRPC 方法调用之前,会首先输出一行日志,然后再调用对方的方法。
如果截取器函数返回了错误那么该次gRPC方法调用将被视作失败处理。因此我们可以在截取器中对输入的参数做一些简单的验证工作。同样也可以对handler返回的结果做一些验证工作。截取器也非常适合前面对Token认证工作。
如果截取器函数返回了错误,那么该次 gRPC 方法调用将被视作失败处理。因此,我们可以在截取器中对输入的参数做一些简单的验证工作。同样,也可以对 handler 返回的结果做一些验证工作。截取器也非常适合前面对 Token 认证工作。
下面是截取器增加了对gRPC方法异常的捕获
下面是截取器增加了对 gRPC 方法异常的捕获:
```go
func filter(
@ -330,9 +330,9 @@ func filter(
}
```
不过gRPC框架中只能为每个服务设置一个截取器因此所有的截取工作只能在一个函数中完成。开源的grpc-ecosystem项目中的go-grpc-middleware包已经基于gRPC对截取器实现了链式截取器的支持。
不过 gRPC 框架中只能为每个服务设置一个截取器,因此所有的截取工作只能在一个函数中完成。开源的 grpc-ecosystem 项目中的 go-grpc-middleware 包已经基于 gRPC 对截取器实现了链式截取器的支持。
以下是go-grpc-middleware包中链式截取器的简单用法
以下是 go-grpc-middleware 包中链式截取器的简单用法
```go
import "github.com/grpc-ecosystem/go-grpc-middleware"
@ -347,13 +347,13 @@ myServer := grpc.NewServer(
)
```
感兴趣的同学可以参考go-grpc-middleware包的代码。
感兴趣的同学可以参考 go-grpc-middleware 包的代码。
## 4.5.4 和Web服务共存
## 4.5.4 和 Web 服务共存
gRPC构建在HTTP/2协议之上因此我们可以将gRPC服务和普通的Web服务架设在同一个端口之上。
gRPC 构建在 HTTP/2 协议之上,因此我们可以将 gRPC 服务和普通的 Web 服务架设在同一个端口之上。
对于没有启动TLS协议的服务则需要对HTTP/2特性做适当的调整
对于没有启动 TLS 协议的服务则需要对 HTTP/2 特性做适当的调整:
```go
func main() {
@ -365,7 +365,7 @@ func main() {
}
```
启用普通的https服务器则非常简单
启用普通的 https 服务器则非常简单:
```go
func main() {
@ -383,7 +383,7 @@ func main() {
}
```
而单独启用带证书的gRPC服务也是同样的简单
而单独启用带证书的 gRPC 服务也是同样的简单:
```go
func main() {
@ -398,9 +398,9 @@ func main() {
}
```
因为gRPC服务已经实现了ServeHTTP方法可以直接作为Web路由处理对象。如果将gRPC和Web服务放在一起会导致gRPC和Web路径的冲突在处理时我们需要区分两类服务。
因为 gRPC 服务已经实现了 ServeHTTP 方法,可以直接作为 Web 路由处理对象。如果将 gRPC Web 服务放在一起,会导致 gRPC Web 路径的冲突,在处理时我们需要区分两类服务。
我们可以通过以下方式生成同时支持Web和gRPC协议的路由处理函数
我们可以通过以下方式生成同时支持 Web gRPC 协议的路由处理函数:
```go
func main() {
@ -426,7 +426,7 @@ func main() {
}
```
首先gRPC是建立在HTTP/2版本之上如果HTTP不是HTTP/2协议则必然无法提供gRPC支持。同时每个gRPC调用请求的Content-Type类型会被标注为"application/grpc"类型。
首先 gRPC 是建立在 HTTP/2 版本之上,如果 HTTP 不是 HTTP/2 协议则必然无法提供 gRPC 支持。同时,每个 gRPC 调用请求的 Content-Type 类型会被标注为 "application/grpc" 类型。
这样我们就可以在gRPC端口上同时提供Web服务了。
这样我们就可以在 gRPC 端口上同时提供 Web 服务了。

View File

@ -1,12 +1,12 @@
# 4.6 gRPC和Protobuf扩展
# 4.6 gRPC Protobuf 扩展
目前开源社区已经围绕Protobuf和gRPC开发出众多扩展形成了庞大的生态。本节我们将简单介绍验证器和REST接口扩展。
目前开源社区已经围绕 Protobuf gRPC 开发出众多扩展,形成了庞大的生态。本节我们将简单介绍验证器和 REST 接口扩展。
## 4.6.1 验证器
到目前为止我们接触的全部是第三版的Protobuf语法。第二版的Protobuf有个默认值特性可以为字符串或数值类型的成员定义默认值。
到目前为止,我们接触的全部是第三版的 Protobuf 语法。第二版的 Protobuf 有个默认值特性,可以为字符串或数值类型的成员定义默认值。
我们采用第二版的Protobuf语法创建文件
我们采用第二版的 Protobuf 语法创建文件:
```protobuf
syntax = "proto2";
@ -19,9 +19,9 @@ message Message {
}
```
内置的默认值语法其实是通过Protobuf的扩展选项特性实现。在第三版的Protobuf中不再支持默认值特性但是我们可以通过扩展选项自己模拟默认值特性。
内置的默认值语法其实是通过 Protobuf 的扩展选项特性实现。在第三版的 Protobuf 中不再支持默认值特性,但是我们可以通过扩展选项自己模拟默认值特性。
下面是用proto3语法的扩展特性重新改写上述的proto文件
下面是用 proto3 语法的扩展特性重新改写上述的 proto 文件:
```protobuf
syntax = "proto3";
@ -41,7 +41,7 @@ message Message {
}
```
其中成员后面的方括号内部的就是扩展语法。重新生成Go语言代码里面会包含扩展选项相关的元信息
其中成员后面的方括号内部的就是扩展语法。重新生成 Go 语言代码,里面会包含扩展选项相关的元信息:
```go
var E_DefaultString = &proto.ExtensionDesc{
@ -63,15 +63,15 @@ var E_DefaultInt = &proto.ExtensionDesc{
}
```
我们可以在运行时通过类似反射的技术解析出Message每个成员定义的扩展选项然后从每个扩展的相关联的信息中解析出我们定义的默认值。
我们可以在运行时通过类似反射的技术解析出 Message 每个成员定义的扩展选项,然后从每个扩展的相关联的信息中解析出我们定义的默认值。
在开源社区中github.com/mwitkow/go-proto-validators 已经基于Protobuf的扩展特性实现了功能较为强大的验证器功能。要使用该验证器首先需要下载其提供的代码生成插件
在开源社区中github.com/mwitkow/go-proto-validators 已经基于 Protobuf 的扩展特性实现了功能较为强大的验证器功能。要使用该验证器首先需要下载其提供的代码生成插件:
```
$ go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators
```
然后基于go-proto-validators验证器的规则为Message成员增加验证规则
然后基于 go-proto-validators 验证器的规则为 Message 成员增加验证规则:
```protobuf
syntax = "proto3";
@ -90,9 +90,9 @@ message Message {
}
```
在方括弧表示的成员扩展中validator.field表示扩展是validator包中定义的名为field扩展选项。validator.field的类型是FieldValidator结构体在导入的validator.proto文件中定义。
在方括弧表示的成员扩展中validator.field 表示扩展是 validator 包中定义的名为 field 扩展选项。validator.field 的类型是 FieldValidator 结构体,在导入的 validator.proto 文件中定义。
所有的验证规则都由validator.proto文件中的FieldValidator定义
所有的验证规则都由 validator.proto 文件中的 FieldValidator 定义:
```protobuf
syntax = "proto2";
@ -116,7 +116,7 @@ message FieldValidator {
}
```
从FieldValidator定义的注释中我们可以看到验证器扩展的一些语法其中regex表示用于字符串验证的正则表达式int_gt和int_lt表示数值的范围。
FieldValidator 定义的注释中我们可以看到验证器扩展的一些语法:其中 regex 表示用于字符串验证的正则表达式int_gt int_lt 表示数值的范围。
然后采用以下的命令生成验证函数代码:
@ -129,10 +129,10 @@ protoc \
hello.proto
```
> windows:替换 `${GOPATH}``%GOPATH%` 即可.
> windows: 替换 `${GOPATH}``%GOPATH%` 即可.
以上的命令会调用protoc-gen-govalidators程序生成一个独立的名为hello.validator.pb.go的文件
以上的命令会调用 protoc-gen-govalidators 程序,生成一个独立的名为 hello.validator.pb.go 的文件:
```go
var _regex_Message_ImportantString = regexp.MustCompile("^[a-z]{2,5}$")
@ -144,7 +144,7 @@ func (this *Message) Validate() error {
this.ImportantString,
))
}
if !(this.Age > 0) {
if !(this.Age> 0) {
return go_proto_validators.FieldError("Age", fmt.Errorf(
`value '%v' must be greater than '0'`, this.Age,
))
@ -158,23 +158,23 @@ func (this *Message) Validate() error {
}
```
生成的代码为Message结构体增加了一个Validate方法用于验证该成员是否满足Protobuf中定义的条件约束。无论采用何种类型所有的Validate方法都用相同的签名因此可以满足相同的验证接口。
生成的代码为 Message 结构体增加了一个 Validate 方法,用于验证该成员是否满足 Protobuf 中定义的条件约束。无论采用何种类型,所有的 Validate 方法都用相同的签名,因此可以满足相同的验证接口。
通过生成的验证函数并结合gRPC的截取器我们可以很容易为每个方法的输入参数和返回值进行验证。
通过生成的验证函数,并结合 gRPC 的截取器,我们可以很容易为每个方法的输入参数和返回值进行验证。
## 4.6.2 REST接口
## 4.6.2 REST 接口
gRPC服务一般用于集群内部通信如果需要对外暴露服务一般会提供等价的REST接口。通过REST接口比较方便前端JavaScript和后端交互。开源社区中的grpc-gateway项目就实现了将gRPC服务转为REST服务的能力。
gRPC 服务一般用于集群内部通信,如果需要对外暴露服务一般会提供等价的 REST 接口。通过 REST 接口比较方便前端 JavaScript 和后端交互。开源社区中的 grpc-gateway 项目就实现了将 gRPC 服务转为 REST 服务的能力。
grpc-gateway的工作原理如下图
grpc-gateway 的工作原理如下图:
![](../images/ch4-2-grpc-gateway.png)
*图 4-2 gRPC-Gateway工作流程*
*图 4-2 gRPC-Gateway 工作流程*
通过在Protobuf文件中添加路由相关的元信息通过自定义的代码插件生成路由相关的处理代码最终将REST请求转给更后端的gRPC服务处理。
通过在 Protobuf 文件中添加路由相关的元信息,通过自定义的代码插件生成路由相关的处理代码,最终将 REST 请求转给更后端的 gRPC 服务处理。
路由扩展元信息也是通过Protobuf的元数据扩展用法提供
路由扩展元信息也是通过 Protobuf 的元数据扩展用法提供:
```protobuf
syntax = "proto3";
@ -202,15 +202,15 @@ service RestService {
}
```
我们首先为gRPC定义了Get和Post方法然后通过元扩展语法在对应的方法后添加路由信息。其中“/get/{value}”路径对应的是Get方法`{value}`部分对应参数中的value成员结果通过json格式返回。Post方法对应“/post”路径body中包含json格式的请求信息。
我们首先为 gRPC 定义了 Get Post 方法,然后通过元扩展语法在对应的方法后添加路由信息。其中 “/get/{value}” 路径对应的是 Get 方法,`{value}` 部分对应参数中的 value 成员,结果通过 json 格式返回。Post 方法对应 “/post” 路径body 中包含 json 格式的请求信息。
然后通过以下命令安装protoc-gen-grpc-gateway插件
然后通过以下命令安装 protoc-gen-grpc-gateway 插件:
```
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
```
再通过插件生成grpc-gateway必须的路由处理代码
再通过插件生成 grpc-gateway 必须的路由处理代码:
```
$ protoc -I/usr/local/include -I. \
@ -220,9 +220,9 @@ $ protoc -I/usr/local/include -I. \
hello.proto
```
> windows:替换 `${GOPATH}``%GOPATH%` 即可.
> windows: 替换 `${GOPATH}``%GOPATH%` 即可.
插件会为RestService服务生成对应的RegisterRestServiceHandlerFromEndpoint函数
插件会为 RestService 服务生成对应的 RegisterRestServiceHandlerFromEndpoint 函数:
```go
func RegisterRestServiceHandlerFromEndpoint(
@ -233,7 +233,7 @@ func RegisterRestServiceHandlerFromEndpoint(
}
```
RegisterRestServiceHandlerFromEndpoint函数用于将定义了Rest接口的请求转发到真正的gRPC服务。注册路由处理函数之后就可以启动Web服务了
RegisterRestServiceHandlerFromEndpoint 函数用于将定义了 Rest 接口的请求转发到真正的 gRPC 服务。注册路由处理函数之后就可以启动 Web 服务了:
```go
func main() {
@ -255,7 +255,7 @@ func main() {
}
```
启动grpc服务 ,端口5000
启动 grpc 服务 , 端口 5000
```go
type RestServiceImpl struct{}
@ -275,9 +275,9 @@ func main() {
```
首先通过runtime.NewServeMux()函数创建路由处理器然后通过RegisterRestServiceHandlerFromEndpoint函数将RestService服务相关的REST接口中转到后面的gRPC服务。grpc-gateway提供的runtime.ServeMux类也实现了http.Handler接口因此可以和标准库中的相关函数配合使用。
首先通过 runtime.NewServeMux() 函数创建路由处理器,然后通过 RegisterRestServiceHandlerFromEndpoint 函数将 RestService 服务相关的 REST 接口中转到后面的 gRPC 服务。grpc-gateway 提供的 runtime.ServeMux 类也实现了 http.Handler 接口,因此可以和标准库中的相关函数配合使用。
当gRPC和REST服务全部启动之后就可以用curl请求REST服务了
gRPC REST 服务全部启动之后,就可以用 curl 请求 REST 服务了:
```
$ curl localhost:8080/get/gopher
@ -287,7 +287,7 @@ $ curl localhost:8080/post -X POST --data '{"value":"grpc"}'
{"value":"Post: grpc"}
```
在对外公布REST接口时我们一般还会提供一个Swagger格式的文件用于描述这个接口规范。
在对外公布 REST 接口时,我们一般还会提供一个 Swagger 格式的文件用于描述这个接口规范。
```
$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
@ -298,9 +298,9 @@ $ protoc -I. \
hello.proto
```
然后会生成一个hello.swagger.json文件。这样的话就可以通过swagger-ui这个项目在网页中提供REST接口的文档和测试等功能。
然后会生成一个 hello.swagger.json 文件。这样的话就可以通过 swagger-ui 这个项目,在网页中提供 REST 接口的文档和测试等功能。
## 4.6.3 Nginx
最新的Nginx对gRPC提供了深度支持。可以通过Nginx将后端多个gRPC服务聚合到一个Nginx服务。同时Nginx也提供了为同一种gRPC服务注册多个后端的功能这样可以轻松实现gRPC负载均衡的支持。Nginx的gRPC扩展是一个较大的主题感兴趣的读者可以自行参考相关文档。
最新的 Nginx gRPC 提供了深度支持。可以通过 Nginx 将后端多个 gRPC 服务聚合到一个 Nginx 服务。同时 Nginx 也提供了为同一种 gRPC 服务注册多个后端的功能,这样可以轻松实现 gRPC 负载均衡的支持。Nginx gRPC 扩展是一个较大的主题,感兴趣的读者可以自行参考相关文档。

View File

@ -1,12 +1,12 @@
# 4.7 pbgo: 基于Protobuf的框架
# 4.7 pbgo: 基于 Protobuf 的框架
pbgo是我们专门针对本节内容设计的较为完整的迷你框架它基于Protobuf的扩展语法通过插件自动生成rpc和rest相关代码。在本章第二节我们已经展示过如何定制一个Protobuf代码生成插件并生成了rpc部分的代码。在本节我们将重点讲述pbgo中和Protobuf扩展语法相关的rest部分的工作原理。
pbgo 是我们专门针对本节内容设计的较为完整的迷你框架,它基于 Protobuf 的扩展语法,通过插件自动生成 rpc rest 相关代码。在本章第二节我们已经展示过如何定制一个 Protobuf 代码生成插件,并生成了 rpc 部分的代码。在本节我们将重点讲述 pbgo 中和 Protobuf 扩展语法相关的 rest 部分的工作原理。
## 4.7.1 Protobuf扩展语法
## 4.7.1 Protobuf 扩展语法
目前Protobuf相关的很多开源项目都使用到了Protobuf的扩展语法。在前一节中提到的验证器就是通过给结构体成员增加扩展元信息实现验证。在grpc-gateway项目中则是通过为服务的每个方法增加Http相关的映射规则实现对Rest接口的支持。pbgo也是通过Protobuf的扩展语法来为rest接口增加元信息。
目前 Protobuf 相关的很多开源项目都使用到了 Protobuf 的扩展语法。在前一节中提到的验证器就是通过给结构体成员增加扩展元信息实现验证。在 grpc-gateway 项目中,则是通过为服务的每个方法增加 Http 相关的映射规则实现对 Rest 接口的支持。pbgo 也是通过 Protobuf 的扩展语法来为 rest 接口增加元信息。
pbgo的扩展语法在`github.com/chai2010/pbgo/pbgo.proto`文件定义:
pbgo 的扩展语法在 `github.com/chai2010/pbgo/pbgo.proto` 文件定义:
```protobuf
syntax = "proto3";
@ -29,11 +29,11 @@ message HttpRule {
}
```
pbgo.proto文件是pbgo框架的一个部分需要被其他的proto文件导入。Protobuf本身自有一套完整的包体系在这里包的路径就是pbgo。Go语言也有自己的一套包体系我们需要通过go_package的扩展语法定义Protobuf和Go语言之间包的映射关系。定义Protobuf和Go语言之间包的映射关系之后其他导入pbgo.ptoto包的Protobuf文件在生成Go语言时会生成pbgo.proto映射的Go语言包路径。
pbgo.proto 文件是 pbgo 框架的一个部分,需要被其他的 proto 文件导入。Protobuf 本身自有一套完整的包体系,在这里包的路径就是 pbgo。Go 语言也有自己的一套包体系,我们需要通过 go_package 的扩展语法定义 Protobuf Go 语言之间包的映射关系。定义 Protobuf Go 语言之间包的映射关系之后,其他导入 pbgo.ptoto 包的 Protobuf 文件在生成 Go 语言时,会生成 pbgo.proto 映射的 Go 语言包路径。
Protobuf扩展语法有五种类型分别是针对文件的扩展信息、针对message的扩展信息、针对message成员的扩展信息、针对service的扩展信息和针对service方法的扩展信息。在使用扩展前首先需要通过extend关键字定义扩展的类型和可以用于扩展的成员。扩展成员可以是基础类型也可以是一个结构体类型。pbgo中只定义了service的方法的扩展只定义了一个名为rest_api的扩展成员类型是HttpRule结构体。
Protobuf 扩展语法有五种类型,分别是针对文件的扩展信息、针对 message 的扩展信息、针对 message 成员的扩展信息、针对 service 的扩展信息和针对 service 方法的扩展信息。在使用扩展前首先需要通过 extend 关键字定义扩展的类型和可以用于扩展的成员。扩展成员可以是基础类型也可以是一个结构体类型。pbgo 中只定义了 service 的方法的扩展,只定义了一个名为 rest_api 的扩展成员,类型是 HttpRule 结构体。
定义好扩展之后我们就可以从其他的Protobuf文件中使用pbgo的扩展。创建一个hello.proto文件
定义好扩展之后,我们就可以从其他的 Protobuf 文件中使用 pbgo 的扩展。创建一个 hello.proto 文件:
```protobuf
syntax = "proto3";
@ -54,11 +54,11 @@ service HelloService {
}
```
首先我们通过导入`github.com/chai2010/pbgo/pbgo.proto`文件引入扩展定义然后在HelloService的Hello方法中使用了pbgo定义的扩展。Hello方法扩展的信息表示该方法对应一个REST接口只有一个GET方法对应"/hello/:value"路径。在REST方法的路径中采用了httprouter路由包的语法规则":value"表示路径中的该字段对应的是参数中同名的成员。
首先我们通过导入 `github.com/chai2010/pbgo/pbgo.proto` 文件引入扩展定义,然后在 HelloService Hello 方法中使用了 pbgo 定义的扩展。Hello 方法扩展的信息表示该方法对应一个 REST 接口,只有一个 GET 方法对应 "/hello/:value" 路径。在 REST 方法的路径中采用了 httprouter 路由包的语法规则,":value" 表示路径中的该字段对应的是参数中同名的成员。
## 4.7.2 插件中读取扩展信息
在本章的第二节我们已经简单讲述过Protobuf插件的工作原理并且展示了如何生成RPC必要的代码。插件是一个generator.Plugin接口
在本章的第二节我们已经简单讲述过 Protobuf 插件的工作原理,并且展示了如何生成 RPC 必要的代码。插件是一个 generator.Plugin 接口:
```go
type Plugin interface {
@ -77,9 +77,9 @@ type Plugin interface {
}
```
我们需要在Generate和GenerateImports函数中分别生成相关的代码。而Protobuf文件的全部信息都在*generator.FileDescriptor类型函数参数中描述因此我们需要从函数参数中提前扩展定义的元数据。
我们需要在 Generate GenerateImports 函数中分别生成相关的代码。而 Protobuf 文件的全部信息都在 * generator.FileDescriptor 类型函数参数中描述,因此我们需要从函数参数中提前扩展定义的元数据。
pbgo框架中的插件对象是pbgoPlugin在Generate方法中首先需要遍历Protobuf文件中定义的全部服务然后再遍历每个服务的每个方法。在得到方法结构之后再通过自定义的getServiceMethodOption方法提取rest扩展信息
pbgo 框架中的插件对象是 pbgoPlugin Generate 方法中首先需要遍历 Protobuf 文件中定义的全部服务,然后再遍历每个服务的每个方法。在得到方法结构之后再通过自定义的 getServiceMethodOption 方法提取 rest 扩展信息:
```go
func (p *pbgoPlugin) Generate(file *generator.FileDescriptor) {
@ -92,7 +92,7 @@ func (p *pbgoPlugin) Generate(file *generator.FileDescriptor) {
}
```
在讲述getServiceMethodOption方法之前我们先回顾下方法扩展的定义
在讲述 getServiceMethodOption 方法之前我们先回顾下方法扩展的定义:
```protobuf
extend google.protobuf.MethodOptions {
@ -100,9 +100,9 @@ extend google.protobuf.MethodOptions {
}
```
pbgo为服务的方法定义了一个rest_api名字的扩展在最终生成的Go语言代码中会包含一个pbgo.E_RestApi全局变量通过该全局变量可以获取用户定义的扩展信息。
pbgo 为服务的方法定义了一个 rest_api 名字的扩展,在最终生成的 Go 语言代码中会包含一个 pbgo.E_RestApi 全局变量,通过该全局变量可以获取用户定义的扩展信息。
下面是getServiceMethodOption方法的实现
下面是 getServiceMethodOption 方法的实现:
```go
func (p *pbgoPlugin) getServiceMethodOption(
@ -120,15 +120,15 @@ func (p *pbgoPlugin) getServiceMethodOption(
}
```
首先通过proto.HasExtension函数判断每个方法是否定义了扩展然后通过proto.GetExtension函数获取用户定义的扩展信息。在获取到扩展信息之后我们再将扩展转型为pbgo.HttpRule类型。
首先通过 proto.HasExtension 函数判断每个方法是否定义了扩展,然后通过 proto.GetExtension 函数获取用户定义的扩展信息。在获取到扩展信息之后,我们再将扩展转型为 pbgo.HttpRule 类型。
有了扩展信息之后我们就可以参考第二节中生成RPC代码的方式生成REST相关的代码。
有了扩展信息之后,我们就可以参考第二节中生成 RPC 代码的方式生成 REST 相关的代码。
## 4.7.3 生成REST代码
## 4.7.3 生成 REST 代码
pbgo框架同时也提供了一个插件用于生成REST代码。不过我们的目的是学习pbgo框架的设计过程因此我们先尝试手写Hello方法对应的REST代码然后插件再根据手写的代码构造模板自动生成代码。
pbgo 框架同时也提供了一个插件用于生成 REST 代码。不过我们的目的是学习 pbgo 框架的设计过程,因此我们先尝试手写 Hello 方法对应的 REST 代码,然后插件再根据手写的代码构造模板自动生成代码。
HelloService只有一个Hello方法Hello方法只定义了一个GET方式的REST接口
HelloService 只有一个 Hello 方法Hello 方法只定义了一个 GET 方式的 REST 接口:
```protobuf
message String {
@ -144,7 +144,7 @@ service HelloService {
}
```
为了方便最终的用户我们需要为HelloService构造一个路由。因此我们希望有个一个类似HelloServiceHandler的函数可以基于HelloServiceInterface服务的接口生成一个路由处理器
为了方便最终的用户,我们需要为 HelloService 构造一个路由。因此我们希望有个一个类似 HelloServiceHandler 的函数,可以基于 HelloServiceInterface 服务的接口生成一个路由处理器:
```go
type HelloServiceInterface interface {
@ -158,7 +158,7 @@ func HelloServiceHandler(svc HelloServiceInterface) http.Handler {
}
```
代码中选择的是开源中比较流行的httprouter路由引擎。其中_handle_HelloService_Hello_get函数用于将Hello方法注册到路由处理器
代码中选择的是开源中比较流行的 httprouter 路由引擎。其中_handle_HelloService_Hello_get 函数用于将 Hello 方法注册到路由处理器:
```go
func _handle_HelloService_Hello_get(
@ -188,13 +188,13 @@ func _handle_HelloService_Hello_get(
}
```
首先通过router.Handle方法注册路由函数。在路由函数内部首先通过`ps.ByName("value")`从URL中加载value参数然后通过pbgo.PopulateFieldFromPath辅助函数设置value参数对应的成员。当输入参数准备就绪之后就可以调用HelloService服务的Hello方法最终将Hello方法返回的结果用json编码返回。
首先通过 router.Handle 方法注册路由函数。在路由函数内部首先通过 `ps.ByName("value")` URL 中加载 value 参数,然后通过 pbgo.PopulateFieldFromPath 辅助函数设置 value 参数对应的成员。当输入参数准备就绪之后就可以调用 HelloService 服务的 Hello 方法,最终将 Hello 方法返回的结果用 json 编码返回。
在手工构造完成最终代码的结构之后,就可以在此基础上构造插件生成代码的模板。完整的插件代码和模板在`protoc-gen-pbgo/pbgo.go`文件,读者可以自行参考。
在手工构造完成最终代码的结构之后,就可以在此基础上构造插件生成代码的模板。完整的插件代码和模板在 `protoc-gen-pbgo/pbgo.go` 文件,读者可以自行参考。
## 4.7.4 启动REST服务
## 4.7.4 启动 REST 服务
虽然从头构造pbgo框架的过程比较繁琐但是使用pbgo构造REST服务却是异常简单。首先要构造一个满足HelloServiceInterface接口的服务对象
虽然从头构造 pbgo 框架的过程比较繁琐,但是使用 pbgo 构造 REST 服务却是异常简单。首先要构造一个满足 HelloServiceInterface 接口的服务对象:
```go
import (
@ -209,7 +209,7 @@ func (p *HelloService) Hello(request *hello_pb.String, reply *hello_pb.String) e
}
```
和RPC代码一样在Hello方法中简单返回结果。然后调用该服务对应的HelloServiceHandler函数生成路由处理器并启动服务
RPC 代码一样,在 Hello 方法中简单返回结果。然后调用该服务对应的 HelloServiceHandler 函数生成路由处理器,并启动服务:
```go
func main() {
@ -218,10 +218,10 @@ func main() {
}
```
然后在命令行测试REST服务
然后在命令行测试 REST 服务:
```
$ curl localhost:8080/hello/vgo
```
这样一个超级简单的pbgo框架就完成了
这样一个超级简单的 pbgo 框架就完成了!

View File

@ -1,11 +1,11 @@
# 4.8 grpcurl工具
# 4.8 grpcurl 工具
Protobuf本身具有反射功能可以在运行时获取对象的Proto文件。gRPC同样也提供了一个名为reflection的反射包用于为gRPC服务提供查询。gRPC官方提供了一个C++实现的grpc_cli工具可以用于查询gRPC列表或调用gRPC方法。但是C++版本的grpc_cli安装比较复杂我们推荐用纯Go语言实现的grpcurl工具。本节将简要介绍grpcurl工具的用法。
Protobuf 本身具有反射功能,可以在运行时获取对象的 Proto 文件。gRPC 同样也提供了一个名为 reflection 的反射包,用于为 gRPC 服务提供查询。gRPC 官方提供了一个 C++ 实现的 grpc_cli 工具,可以用于查询 gRPC 列表或调用 gRPC 方法。但是 C++ 版本的 grpc_cli 安装比较复杂,我们推荐用纯 Go 语言实现的 grpcurl 工具。本节将简要介绍 grpcurl 工具的用法。
## 4.8.1 启动反射服务
reflection包中只有一个Register函数用于将grpc.Server注册到反射服务中。reflection包文档给出了简单的使用方法
reflection 包中只有一个 Register 函数,用于将 grpc.Server 注册到反射服务中。reflection 包文档给出了简单的使用方法:
```go
import (
@ -23,18 +23,18 @@ func main() {
}
```
如果启动了gprc反射服务那么就可以通过reflection包提供的反射服务查询gRPC服务或调用gRPC方法。
如果启动了 gprc 反射服务,那么就可以通过 reflection 包提供的反射服务查询 gRPC 服务或调用 gRPC 方法。
## 4.8.2 查看服务列表
grpcurl是Go语言开源社区开发的工具需要手工安装
grpcurl Go 语言开源社区开发的工具,需要手工安装:
```
$ go get github.com/fullstorydev/grpcurl
$ go install github.com/fullstorydev/grpcurl/cmd/grpcurl
```
grpcurl中最常使用的是list命令用于获取服务或服务方法的列表。比如`grpcurl localhost:1234 list`命令将获取本地1234端口上的grpc服务的列表。在使用grpcurl时需要通过`-cert``-key`参数设置公钥和私钥文件链接启用了tls协议的服务。对于没有没用tls协议的grpc服务通过`-plaintext`参数忽略tls证书的验证过程。如果是Unix Socket协议则需要指定`-unix`参数。
grpcurl 中最常使用的是 list 命令,用于获取服务或服务方法的列表。比如 `grpcurl localhost:1234 list` 命令将获取本地 1234 端口上的 grpc 服务的列表。在使用 grpcurl 时,需要通过 `-cert` `-key` 参数设置公钥和私钥文件,链接启用了 tls 协议的服务。对于没有没用 tls 协议的 grpc 服务,通过 `-plaintext` 参数忽略 tls 证书的验证过程。如果是 Unix Socket 协议,则需要指定 `-unix` 参数。
如果没有配置好公钥和私钥文件,也没有忽略证书的验证过程,那么将会遇到类似以下的错误:
@ -44,14 +44,14 @@ Failed to dial target host "localhost:1234": tls: first record does not \
look like a TLS handshake
```
如果grpc服务正常但是服务没有启动reflection反射服务将会遇到以下错误
如果 grpc 服务正常,但是服务没有启动 reflection 反射服务,将会遇到以下错误:
```shell
$ grpcurl -plaintext localhost:1234 list
Failed to list services: server does not support the reflection API
```
假设grpc服务已经启动了reflection反射服务服务的Protobuf文件如下
假设 grpc 服务已经启动了 reflection 反射服务,服务的 Protobuf 文件如下:
```protobuf
syntax = "proto3";
@ -68,7 +68,7 @@ service HelloService {
}
```
grpcurl用list命令查看服务列表时将看到以下输出
grpcurl list 命令查看服务列表时将看到以下输出:
```shell
$ grpcurl -plaintext localhost:1234 list
@ -76,11 +76,11 @@ HelloService.HelloService
grpc.reflection.v1alpha.ServerReflection
```
其中HelloService.HelloService是在protobuf文件定义的服务。而ServerReflection服务则是reflection包注册的反射服务。通过ServerReflection服务可以查询包括本身在内的全部gRPC服务信息。
其中 HelloService.HelloService 是在 protobuf 文件定义的服务。而 ServerReflection 服务则是 reflection 包注册的反射服务。通过 ServerReflection 服务可以查询包括本身在内的全部 gRPC 服务信息。
## 4.8.3 服务的方法列表
继续使用list子命令还可以查看HelloService服务的方法列表
继续使用 list 子命令还可以查看 HelloService 服务的方法列表:
```shell
$ grpcurl -plaintext localhost:1234 list HelloService.HelloService
@ -88,9 +88,9 @@ Channel
Hello
```
从输出可以看到HelloService服务提供了Channel和Hello两个方法和Protobuf文件的定义是一致的。
从输出可以看到 HelloService 服务提供了 Channel Hello 两个方法,和 Protobuf 文件的定义是一致的。
如果还想了解方法的细节可以使用grpcurl提供的describe子命令查看更详细的描述信息
如果还想了解方法的细节,可以使用 grpcurl 提供的 describe 子命令查看更详细的描述信息:
```
$ grpcurl -plaintext localhost:1234 describe HelloService.HelloService
@ -128,7 +128,7 @@ HelloService.HelloService is a service:
## 4.8.4 获取类型信息
在获取到方法的参数和返回值类型之后还可以继续查看类型的信息。下面是用describe命令查看参数HelloService.String类型的信息
在获取到方法的参数和返回值类型之后,还可以继续查看类型的信息。下面是用 describe 命令查看参数 HelloService.String 类型的信息:
```shell
$ grpcurl -plaintext localhost:1234 describe HelloService.String
@ -153,7 +153,7 @@ HelloService.String is a message:
}
```
json信息对应HelloService.String类型在Protobuf中的定义如下
json 信息对应 HelloService.String 类型在 Protobuf 中的定义如下:
```protobuf
message String {
@ -161,25 +161,25 @@ message String {
}
```
输出的json数据只不过是Protobuf文件的另一种表示形式。
输出的 json 数据只不过是 Protobuf 文件的另一种表示形式。
## 4.8.5 调用方法
在获取gRPC服务的详细信息之后就可以json调用gRPC方法了。
在获取 gRPC 服务的详细信息之后就可以 json 调用 gRPC 方法了。
下面命令通过`-d`参数传入一个json字符串作为输入参数调用的是HelloService服务的Hello方法
下面命令通过 `-d` 参数传入一个 json 字符串作为输入参数,调用的是 HelloService 服务的 Hello 方法:
```shell
$ grpcurl -plaintext -d '{"value": "gopher"}' \
$ grpcurl -plaintext -d '{"value":"gopher"}' \
localhost:1234 HelloService.HelloService/Hello
{
"value": "hello:gopher"
}
```
如果`-d`参数是`@`则表示从标准输入读取json输入参数这一般用于比较输入复杂的json数据也可以用于测试流方法。
如果 `-d` 参数是 `@` 则表示从标准输入读取 json 输入参数,这一般用于比较输入复杂的 json 数据,也可以用于测试流方法。
下面命令是链接Channel流方法通过从标准输入读取输入流参数
下面命令是链接 Channel 流方法,通过从标准输入读取输入流参数:
```shell
$ grpcurl -plaintext -d @ localhost:1234 HelloService.HelloService/Channel
@ -194,4 +194,4 @@ $ grpcurl -plaintext -d @ localhost:1234 HelloService.HelloService/Channel
}
```
通过grpcurl工具我们可以在没有客户端代码的环境下测试gRPC服务。
通过 grpcurl 工具,我们可以在没有客户端代码的环境下测试 gRPC 服务。

View File

@ -1,5 +1,3 @@
## 4.9 补充说明
目前专门讲述RPC的图书比较少。目前Protobuf和gRPC的官网都提供了详细的参考资料和例子。本章重点讲述了Go标准库的RPC和基于Protobuf衍生的gRPC框架同时也简单展示了如何自己定制一个RPC框架。之所以聚焦在这几个有限的主题是因为这几个技术都是Go语言团队官方在进行维护和Go语言契合也最为默契。不过RPC依然是一个庞大的主题足以单独成书。目前开源世界也有很多富有特色的RPC框架还有针对分布式系统进行深度定制的RPC系统用户可以根据自己实际需求选择合适的工具。
目前专门讲述 RPC 的图书比较少。目前 Protobuf 和 gRPC 的官网都提供了详细的参考资料和例子。本章重点讲述了 Go 标准库的 RPC 和基于 Protobuf 衍生的 gRPC 框架,同时也简单展示了如何自己定制一个 RPC 框架。之所以聚焦在这几个有限的主题,是因为这几个技术都是 Go 语言团队官方在进行维护,和 Go 语言契合也最为默契。不过 RPC 依然是一个庞大的主题,足以单独成书。目前开源世界也有很多富有特色的 RPC 框架,还有针对分布式系统进行深度定制的 RPC 系统,用户可以根据自己实际需求选择合适的工具。

View File

@ -1,5 +1,5 @@
# 第4章 RPC和Protobuf
# 第 4 章 RPC Protobuf
*学习编程重要的是什么多练、多看、多实践跨语言学习掌握基础语法和语言的特性之后实战效率来的最快——khlipeng*
RPC是远程过程调用的缩写Remote Procedure Call通俗地说就是调用远处的一个函数。远处到底有多远呢可能是同一个文件内的不同函数也可能是同一个机器的另一个进程的函数还可能是远在火星好奇号上面的某个秘密方法。因为RPC涉及的函数可能非常之远远到它们之间说着完全不同的语言语言就成了两边的沟通障碍。而Protobuf因为支持多种不同的语言甚至不支持的语言也可以扩展支持其本身特性也非常方便描述服务的接口也就是方法列表因此非常适合作为RPC世界的接口交流语言。本章将讨论RPC的基本用法如何针对不同场景设计自己的RPC服务以及围绕Protobuf构造的更为庞大的RPC生态。
RPC 是远程过程调用的缩写Remote Procedure Call通俗地说就是调用远处的一个函数。远处到底有多远呢可能是同一个文件内的不同函数也可能是同一个机器的另一个进程的函数还可能是远在火星好奇号上面的某个秘密方法。因为 RPC 涉及的函数可能非常之远,远到它们之间说着完全不同的语言,语言就成了两边的沟通障碍。而 Protobuf 因为支持多种不同的语言(甚至不支持的语言也可以扩展支持),其本身特性也非常方便描述服务的接口(也就是方法列表),因此非常适合作为 RPC 世界的接口交流语言。本章将讨论 RPC 的基本用法,如何针对不同场景设计自己的 RPC 服务,以及围绕 Protobuf 构造的更为庞大的 RPC 生态。