1. 简介
在grpc中,客户端程序可以调用不同机器上应用程序的方法,就像它是本地对象一样。
在服务器端,服务器实现了这个接口并运行一个 gRPC 服务器来处理客户端调用。
在客户端,客户端有一个存根(在某些语言中简称为客户端),它提供与服务器相同的方法。
grpc支持各种各样的环境,只要grpc支持的语言。
例如:你可以用java作为grpc的服务端,用go、python、ruby等等任何一门语言作为客户端。
grpc使用Protocol Buffers,ProtocolBuffers用于序列化结构化数据。
Protocol Buffers、xml、Json 对比图
2. Protocol Buffers
例子
syntax = "proto3"; //指定使用的是proto3,如果不指定则使用proto2option go_package = "grpcTest/phone/phone";package phone;//go_package是指go的导入路径,package是proto文件的包说明符,两者没有任何关联。message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;}
消息编号
1到15范围内的编号占用一个字节,16到2047范围内的编号占用两个字节.
您可以指定的最小字段编号是 1,最大的是 229 - 1,即 536,870,911。 您也不能使用数字 19000 到 19999,它们是系统内部实现保留的.
字段定义规则
singular: 消息可以有零个或一个此字段(但不能超过一个)这是 proto3 语法的默认字段规则。
repeated: 此字段可以在格式良好的消息中重复任意次数(包括零次)重复值的顺序将被保留。
字段类型
| protobuf 数据类型 | 描述 | 打包 | Go Type |
|---|---|---|---|
| double | 64位浮点数 (默认值0.0) | N | float64 |
| float | 32位浮点数(默认值0.0) | N | float32 |
| int32 | 32位整数 (默认值0) | N |
int32 |
| int64 | 64位整数 (默认值0) | N |
int64 |
| uint32 | 无符号32位整数(默认值0) | N |
uint32 |
| uint64 | 无符号64位整数(默认值0) | N |
uint64 |
| sint32 | 32位整数,处理负数效率更高(默认值0) | N |
int32 |
| sint64 | 64位整数,处理负数效率更高(默认值0) | N |
int64 |
| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 228.(默认值0) | 4 |
uint32 |
| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 256.(默认值0) | 8 |
uint64 |
| sfixed32 | Always four bytes.(默认值0) | 4 | int32 |
| sfixed64 | Always eight bytes.(默认值0) | 8 | int64 |
| bool | 布尔类型 (默认值false) | 1字节 | bool |
| string | A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232. (默认值””) | N |
string |
| bytes | May contain any arbitrary sequence of bytes no longer than 232.(默认值空byte数组) | N | []byte |
| enum | 可以包含一个用户自定义的枚举类型uint32。默认值是第一个定义的枚举值,而且必须为0. | N(uint32) | enum |
| message | 可以包含一个用户自定义的消息类型 | N | object of class |
N 表示打包的字节并不是固定。而是根据数据的大小或者长度。
例如int32,如果数值比较小,在0~127时,使用一个字节打包。
关于枚举的打包方式和uint32相同。
关于message,类似于C语言中的结构包含另外一个结构作为数据成员一样。
关于 fixed32 和int32的区别。fixed32的打包效率比int32的效率高,但是使用的空间一般比int32多。因此一个属于时间效率高,一个属于空间效率高。根据项目的实际情况,一般选择fixed32,如果遇到对传输数据量要求比较苛刻的环境,可以选择int32.
引入其他包的.proto
import "myproject/other_protos.proto";
嵌套类型
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
生成代码命令
protoc -I /PATH/TO/.proto_DIR/ /PATH/TO/TARGET.proto --go_out=plugins=grpc:/PATH/TO/OUTPUT_DIR
#不指定-I表示从当前工作目录寻找.proto文件。-I一般是指向项目根目录。
# 题外话: --go-out 选项中有无plugins=grpc的区别:
1 protoc --go_out=. ./helloworld.proto // -–go_out=后直接跟路径(点号表示当前路径)。只生成序列化/反序列化代码文件,不需要rpc通讯。
2 protoc --go_out=plugins=grpc:. ./helloworld.proto // --go_out=后跟了grpc插件和目录。生成序列化反序列化代码 和 客户端/服务端通讯代码。
官方demo给出的命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative api/question.proto
如果指定了 paths=source_relative 标志,则输出文件与输入文件放置在相同的相对目录中。 例如,输入文件 protos/buzz.proto 会导致输出文件位于 protos/buzz.pb.go。
3. 服务定义
四种服务方法
1.Unary RPCs where the client sends a single request to the server and gets a single response back, just like a normal function call.
rpc SayHello(HelloRequest) returns (HelloResponse);
单个请求,单个响应。
2.Server streaming RPCs where the client sends a request to the server and gets a stream to read a sequence of messages back. The client reads from the returned stream until there are no more messages. gRPC guarantees message ordering within an individual RPC call.
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
单个请求,获取一系列消息。
3.Client streaming RPCs where the client writes a sequence of messages and sends them to the server, again using a provided stream. Once the client has finished writing the messages, it waits for the server to read them and return its response. Again gRPC guarantees message ordering within an individual RPC call.
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
客户端写入一系列消息,并获取一个响应。
4.Bidirectional streaming RPCs where both sides send a sequence of messages using a read-write stream. The two streams operate independently, so clients and servers can read and write in whatever order they like: for example, the server could wait to receive all the client messages before writing its responses, or it could alternately read a message then write a message, or some other combination of reads and writes. The order of messages in each stream is preserved.
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
客户端写入一系列消息,服务端也返回一系列消息,可以按照任意顺序读取和写入。
4. 生命周期
以单个请求单个响应为例。
1.一旦客户端调用了一个存根方法,服务器就会被通知该 RPC 已被调用,其中包含该调用的客户端元数据、方法名称和指定的截止日期。
2.服务端既可以在任何响应之前直接发送回初始的元数据,也可以等待客户端的请求信息,到底哪个先发生,取决于具体的应用。
3.一旦服务端获得客户端的请求信息,就会做所需的任何工作来创建或组装对应的响应。如果成功的话,这个响应会和包含状态码以及可选的状态信息等状态明细及可选的追踪信息返回给客户端 。
4.如果响应状态为 OK,则客户端得到响应,从而完成客户端的调用。
截止时间
gRPC 允许客户端在调用一个远程方法前指定一个最后期限值。这个值指定了在客户端可以等待服务端多长时间来应答,超过这个时间值 RPC 将结束并返回DEADLINE_EXCEEDED错误。在服务端可以查询这个期限值来看是否一个特定的方法已经过期,或者还剩多长时间来完成这个方法。 各语言来指定一个截止时间的方式是不同的 - 比如在 Python 里一个截止时间值总是必须的,但并不是所有语言都有一个默认的截止时间。
5. grpc使用流程
6. protoc命令
protoc可以解析proto_files文件,并可以生成对应语言的代码。
protoc --proto_path=./proto_vendor/:. --twirp_out=. --go_out=paths=source_relative:. ./rpc/common/common.proto
上面的指令可以拆解为三部分,分别对应protoc的三个重要参数,我们来看看protoc提供了哪些参数:
| 命令 | 含义 |
|---|---|
| -IPATH, —proto_path=PATH | 指定导入文件的搜索路径,如果不指定,则为当前路径。 |
| —cpp_out=OUT_DIR | Generate C++ header and source. |
| —csharp_out=OUT_DIR | Generate C# source file. |
| —java_out=OUT_DIR | Generate Java source file. |
| —js_out=OUT_DIR | Generate JavaScript source. |
| —objc_out=OUT_DIR | Generate Objective C header and source. |
| —php_out=OUT_DIR | Generate PHP source file. |
| —python_out=OUT_DIR | Generate Python source file. |
| —ruby_out=OUT_DIR | Generate Ruby source file |
| @ |
proto文件的具体位置 |
1.搜索路径参数
第一个比较重要的参数就是搜索路径参数,即上述展示的-IPATH, —proto_path=PATH。它表示的是我们要在哪个路径下搜索要导入的.proto文件,这个参数既可以用-I指定,也可以使用—proto_path=指定。
如果不指定该参数,则默认在当前路径下进行搜索;另外,该参数也可以指定多次,这也意味着我们可以指定多个路径进行搜索。
比如下面这三条指令,表达的含义都是一样的。
# Makefile
GOPATH:=$(shell go env GOPATH)
# proto1 相关指令
.PHONY: proto1
path1:
protoc --go_out=. proto1/greeter/greeter.proto
path2:
protoc -I. --go_out=. proto1/greeter/greeter.proto # 点号表示当前路径,注意-I参数没有等于号
path3:
protoc --proto_path=. --go_out=. proto1/greeter/greeter.proto
2.—go_out详细解读
想必大家在使用的时候,应该遇到过这些写法:—go_out=paths=import:.、—go_out=paths=source_relative:.,或者—go_out=plugins=grpc:.。
这样写表达的是啥意思呢?
所以我们需要知道,—go_out参数是用来指定 protoc-gen-go 插件的工作方式和Go代码的生成位置,而上面的写法正是表明该插件的工作方式。
—go_out主要的两个参数为plugins 和 paths,分别表示生成Go代码所使用的插件,以及生成的Go代码的位置。—go_out的写法是,参数之间用逗号隔开,最后加上冒号来指定代码的生成位置,比如—go_out=plugins=grpc,paths=import:.。
paths参数有两个选项,分别是 import 和 source_relative,默认为 import,表示按照生成的Go代码的包的全路径去创建目录层级,source_relative 表示按照 proto源文件的目录层级去创建Go代码的目录层级,如果目录已存在则不用创建。
举例说明:
option go_package="proto1/pb_go";
# 指令1:paths为import,pb文件最终在 pb_go 目录下
$ protoc --proto_path=. --go_out=. proto1/greeter/greeter_v2.proto
$ protoc --proto_path=. --go_out=paths=import:. proto1/greeter/greeter_v2.proto
# 指令2:paths为source_relative,pb文件最终在 proto1/greeter 目录下
$ protoc --proto_path=. --go_out=paths=source_relative:. proto1/greeter/greeter_v2.proto
3. package
package参数针对的是protobuf,是proto文件的命名空间,它的作用是为了避免我们定义的接口,或者message出现冲突。
举一个小栗子,假设我有A.proto和B.proto两份文件,如下
# A.proto
message UserInfo {
uint32 uid = 1;
string name = 2;
}
# B.proto
message UserInfo {
uint32 uid = 1;
string name = 2;
uint32 age = 3;
string work = 4;
}
如上,两份文件同时有一个UserInfo的message,这时候如果我需要在A文件引用B文件,如果没有指定package,就无法区分是要调A的UserInfo还是调B的。
4. xx_package
这里以go_package进行举例说明,该参数主要声明Go代码的存放位置,也可以说它解决的是包名问题(因为proto文件编译后会生成一份.pb.go文件,既然是go文件,就有包名问题)
.pb.go常规的存放路径一般是放在同名proto文件下,但也有些人不想这么做,比如他想把所有.pb.go文件都存放在一个特定文件夹下,比如上述的 pb_go,那么他有两种办法:
第一种:
# 修改 --go_out,go_package 保持不变
$ protoc --proto_path=. --go_out=./proto1/pb_go proto1/greeter/greeter.proto
这样生成的pb文件在 pb_go/proto1/greeter 目录下,文件目录有点冗余,不过pb文件的包名仍然是 greeter
第二种:
# 修改 go_package, go_out 保持不变
option go_package="proto1/pb_go";
$ protoc --proto_path=. --go_out=. proto1/greeter/greeter_v2.proto
这样生成的pb文件在 pb_go 目录下,pb文件的包名为 pb_go
另外,xx_package其实有两种声明方式,第一种是在文件中声明,你已经见过了;第二种是在命令行中声明,声明格式如下:M${PROTO_FILE}=${GO_IMPORT_PATH}。
目前,官方文档比较推荐在文件中声明,目的是可以缩短protoc指令的长度,我个人也比较推荐这种方式。
好了,相信通过以上几个例子,你已经能大致弄懂package和xx_package的区别了,再次强调下,官方文档中已经说明,package和xx_package两者没有关联,属于不同范畴。

