目录
上篇文章介绍了如何安装protobuf环境,文章链接如下
本节介绍protobuf在gRPC中具体如何使用,并编写测试用例
一、Protobuf是如何工作的
.proto文件是protobuf一个重要的文件,它定义了需要序列化数据的结构,当protobuf编译器(protoc)来运行.proto文件时候,编译器将生成所选择的语言的代码,比如你选择go语言,那么就会将.proto转换成对应的go语言代码,对于go来说,编译器会为每个消息类型生成一个pd.go文件,而C++会生成一个.h文件和一个.cc文件。
使用protobuf的3个步骤是:
1. 在.proto文件中定义消息格式。
2. 用protobuf编译器编译.proto文件。
3. 用C++/Java/go等对应的protobuf API来写或者读消息。
二、Protobuf代码测试
在开始代码编写与测试之前,把官网的链接分享给大家,这个看完可以避坑,尤其是版本,示例代码,proto文件格式等。
工具安装及demo测试:Quick start | Go | gRPC
1.定义proto文件
编写proto文件需要按照.proto文件格式和规则定义,初学者我们需要参考官网文档,链接如下:
Go Generated Code Guide | Protocol Buffers Documentation
syntax="proto3";
option go_package="./;student"; //关于最后生成的go文件是处在哪个目录哪个包中,.代表在当前目录生成,student代表了生成的go文件的包名是student
service DemoService {
rpc Sender(StudentRequest) returns (StudentResponse){}
}
message StudentRequest {
string Id = 1;
}
message StudentResponse {
string result =1;
}
message Student {
int64 Id = 1; //id
string Name =2; //姓名
string No =3; //学号
}
2.生成代码
进入proto文件所在目录,cd ~/sourceCode/go/goproject01/src/day34/grpc/proto
<1>执行protoc --go_out=. student.proto
protoc --go_out=. student.proto
执行后发现proto目录生成了一个文件:student.pb.go
<2>执行protoc --go-grpc_out=. student.proto,发现命令执行报错如下
cd ~/sourceCode/go/goproject01/src/day34/grpc/proto
protoc --go-grpc_out=. student.proto
protoc-gen-go-grpc: program not found or is not executable
Please specify a program using absolute path or make sure the program is available in your PATH system variable
--go-grpc_out: protoc-gen-go-grpc: Plugin failed with status code 1.
执行报错,发现没有安装protoc-gen-go-grpc,需要安装一下
先执行go get
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc
go: downloading google.golang.org/grpc v1.59.0
go: downloading google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
go: downloading google.golang.org/protobuf v1.28.1
go: added google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
go: added google.golang.org/protobuf v1.28.1
再执行go install
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
执行完在$GOBIN目录下生成protoc-gen-go-grpc,源码对应在pkg下
再次执行protoc --go-grpc_out=. student.proto
protoc --go-grpc_out=. student.proto
执行后会在当前目录生成一文件:student_grpc.pb.go
<3>执行go mod tidy
打开文件发现依赖的包没有导入,会报错,需要执行一下最小化导入包依赖
go mod tidy
go: finding module for package google.golang.org/grpc
go: finding module for package google.golang.org/grpc/status
go: finding module for package google.golang.org/grpc/codes
go: found google.golang.org/grpc in google.golang.org/grpc v1.59.0
go: found google.golang.org/grpc/codes in google.golang.org/grpc v1.59.0
go: found google.golang.org/grpc/status in google.golang.org/grpc v1.59.0
go: downloading google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d
go: downloading golang.org/x/text v0.12.0
执行后生成的代码编译通过,不再报错。
你也可以一次性生成student.pb.go 和 student_grpc.pb.go,使用如下命令:
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=require_unimplemented_servers=false:. src/main/protobuf/*.proto //可以同时操作多个.proto文件
- --go_out:proto文件message定义的go程序生成文件路径
- --go_opt=paths=source_relative:指定生成的文件位置时,其生成规则是
.proto
文件相对于-I
参数的相对位置,即是生成的.pb.go
文件相对--go_out
参数的相对位置 - --go-grpc_out:proto文件service接口定义的go程序生成文件路径
- 最后边就是目标proto文件路径
require_unimplemented_servers=false选项不建议使用,这个在接下来的Server源码有介绍,这里提前贴一下
grpc生成源码后多了一个方法mustEmbedUnimplementedDemoServiceServer
这个方法首字母小写不允许重载,自定义实现却没法实现该方法,解决方法如下
1,生成代码时候使用选项:
protoc --go_out=. **--go-grpc_opt=require_unimplemented_servers=false** --go-grpc_out=. proto/*.proto
This works, but your binary will fail to compile if you add methods to your service(s) and regenerate/recompile.
That is why we have the embedding requirement by default. We recommend against using this option.
We recommend against using this option(不推荐使用此选项)2,使用内嵌的结构体定义
// server is used to implement helloworld.GreeterServer.
type server struct{
// Embed the unimplemented server
helloworld.UnimplementedGreeterServer
}
推荐使用命令:
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. src/main/protobuf/*.proto //可以同时操作多个.proto文件
3.编写Server端程序
在server包下创建server.go文件
package main
import (
"context"
"encoding/json"
"errors"
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
student "goproject01/day34/grpc/proto"
"log"
"net"
"strconv"
"time"
)
// grpc生成源码后多了一个方法mustEmbedUnimplementedDemoServiceServer
// 这个方法首字母小写不允许重载,自定义实现却没法实现该方法,解决方法如下
/**
1,生成代码时候使用选项:
protoc --go_out=. **--go-grpc_opt=require_unimplemented_servers=false** --go-grpc_out=. proto/*.proto
This works, but your binary will fail to compile if you add methods to your service(s) and regenerate/recompile.
That is why we have the embedding requirement by default. We recommend against using this option.
We recommend against using this option(不推荐使用此选项)
2,使用内嵌的结构体定义
// server is used to implement helloworld.GreeterServer.
type server struct{
// Embed the unimplemented server
helloworld.UnimplementedGreeterServer
}
*/
type MyDemeServiceImpl struct {
student.UnimplementedDemoServiceServer
}
func (ds *MyDemeServiceImpl) Sender(ctx context.Context, in *student.StudentRequest) (*student.StudentResponse, error) {
return handSendMessage(ctx, in)
}
func main() {
//绑定9091端口
listener, err := net.Listen("tcp", ":10005")
if err != nil {
log.Fatalf("bingding port:9091 error:%v", err)
}
//注册服务
//这个连接最大的空闲时间,超过就释放,解决proxy等到网络问题(不通知grpc的client和server)
/**
func NewGrpcServer(opts ...grpc.ServerOption) *grpc.Server {
var options []grpc.ServerOption
options = append(options,
grpc.KeepaliveParams(keepalive.ServerParameters{
Time: 10 * time.Second, // wait time before ping if no activity
Timeout: 20 * time.Second, // ping timeout
}),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 60 * time.Second, // min time a client should wait before sending a ping
PermitWithoutStream: true,
}),
grpc.MaxRecvMsgSize(Max_Message_Size),
grpc.MaxSendMsgSize(Max_Message_Size),
)
for _, opt := range opts {
if opt != nil {
options = append(options, opt)
}
}
return grpc.NewServer(options...)
}
*/
option1 := grpc.KeepaliveParams(keepalive.ServerParameters{MaxConnectionIdle: 5 * time.Minute})
option2 := grpc.MaxSendMsgSize(409600) //400kB
option3 := grpc.MaxRecvMsgSize(409600)
grpcServer := grpc.NewServer(option1, option2, option3)
//impliServer := student.UnimplementedDemoServiceServer{}
var impliServer = &MyDemeServiceImpl{}
student.RegisterDemoServiceServer(grpcServer, impliServer)
log.Printf("server listening at %v", listener.Addr())
/*
错误的写成http了,导致排查半天
err = http.Serve(listener, nil)
if err != nil {
log.Fatalf("http serve fail:%v", err)
}*/
if err := grpcServer.Serve(listener); err != nil {
panic("error building server: " + err.Error())
}
}
func handSendMessage(ctx context.Context, req *student.StudentRequest) (*student.StudentResponse, error) {
log.Println("receive param=", req.GetId())
//模拟根据id查询student对象并构建一个student实例
sid := req.GetId()
if sid == "" {
log.Println("request param id is null")
return nil, errors.New("request param id is null")
}
resp := &student.StudentResponse{}
sidInt64, err := strconv.ParseInt(sid, 10, 64)
if err != nil {
log.Printf("sid:%s covert to int64 error", sid)
return nil, errors.New("sid covert to int64 error")
}
//通过proto进行序列化对象,和原始json以及easyJson使用方法类似
s := &student.Student{Name: "xiaoliu", No: "10001", Id: sidInt64}
//bytes, errs := proto.Marshal(s) //需要一个指针类型对象
bytes, errs := json.Marshal(s)
if errs != nil {
log.Println("student obj convert to json error")
return nil, errors.New("student obj convert to json error")
}
resp.Result = bytes
log.Println("返回客户端序列化字符串:", string(bytes))
return resp, nil
}
4.编写客户端程序
package main
import (
"context"
"flag"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
student "goproject01/day34/grpc/proto"
"log"
"time"
)
const (
defaultName = "world"
defaultId = "10001"
)
var (
address = flag.String("address", "localhost:10005", "the address connect to ")
name = flag.String("name", defaultName, " name to great")
id = flag.String("id", defaultId, "id send to server")
)
func main() {
flag.Parse()
connection, err := grpc.Dial(*address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("connect localhost:9091 fail:%v\n", err)
}
defer connection.Close()
client := student.NewDemoServiceClient(connection)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
resp, errs := client.Sender(ctx, &student.StudentRequest{Id: *id})
if errs != nil {
log.Fatalf("client call server Sender method fail:%v\n", errs)
}
//获取StudentResponse result的内容
rst := string(resp.GetResult())
log.Println("rpc returns result:", rst)
}
5.代码测试
<1>启动服务端程序
go run server.go
//启动后打开服务端端口,等待客户端连接日志
2023/12/04 18:24:17 server listening at [::]:10005
//启动后接收客户端的参数打印
2023/12/04 18:24:25 receive param= 10001
2023/12/04 18:24:25 返回客户端序列化字符串: {"Id":10001,"Name":"xiaoliu","No":"10001"}
<2>运行客户端程序
go run client.go
首次执行发现报错如下:
rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: http2: frame too large"
错误解决:自己误把grpc协议写为http,修改代码即可:
/*
错误的写成http了,导致排查半天
err = http.Serve(listener, nil)
if err != nil {
log.Fatalf("http serve fail:%v", err)
}*/
if err := grpcServer.Serve(listener); err != nil {
panic("error building server: " + err.Error())
}
再次执行报错如下:
2023/12/04 18:09:55 client call server Sender method fail:rpc error: code = Internal desc = grpc: error while marshaling: string field contains invalid UTF-8
错误解决:需要修改student.proto文件中StudentResponse的result字段为bytes类型,用来支持utf-8字符。将student.proto文件修改如下:上面的server.go,client.go最终以这个proto文件为准。
syntax="proto3";
option go_package="./;student"; //关于最后生成的go文件是处在哪个目录哪个包中,.代表在当前目录生成,student代表了生成的go文件的包名是student
service DemoService {
rpc Sender(StudentRequest) returns (StudentResponse){}
}
message StudentRequest {
string Id = 1;
}
message StudentResponse {
bytes result =1; //涉及到utf-8编码的字符需要使用bytes类型
}
message Student {
int64 Id = 1;
string Name =2;
string No =3;
}
修改后运行客户端程序:
调用服务端获取序列化的结果如下
go run client.go
2023/12/04 18:24:25 rpc returns result: {"Id":10001,"Name":"xiaoliu","No":"10001"}
6.proto文件定义解读
这里定义一个Teacher.proto文件如下
syntax = "proto3";
package main;
// this is a comment
message Teacher {
string name = 1;
bool male = 2;
repeated int32 classNO = 3;
}
对应的程序文件Teacher.pb.go中定义的结构体为
type Student Teacher {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Male bool `protobuf:"varint,2,opt,name=male,proto3" json:"male,omitempty"`
classNO []int32 `protobuf:"varint,3,rep,packed,name=classNO,proto3" json:"classNO,omitempty"`
...
}
这里解读一下student.proto文件的定义
- protobuf 有2个版本,默认版本是 proto2,如果需要 proto3,则需要在非空非注释第一行使用
syntax = "proto3"
标明版本。 package
,即包名声明符是可选的,用来防止不同的消息类型有命名冲突。- 消息类型 使用
message
关键字定义,Student 是类型名,name, male, classNO 是该类型的 3 个字段,类型分别为 string, bool 和 []int32。字段可以是标量类型,也可以是合成类型。 - 每个字段的修饰符默认是 singular,一般省略不写,
repeated
表示字段可重复,即用来表示 Go 语言中的数组类型。 - 每个字符
=
后面的数字称为标识符,每个字段都需要提供一个唯一的标识符。标识符用来在消息的二进制格式中识别各个字段,一旦使用就不能够再改变,标识符的取值范围为 [1, 2^29 - 1] 。 - .proto 文件可以写注释,单行注释
//
,多行注释/* ... */
- 一个 .proto 文件中可以写多个消息类型,即对应多个结构体(struct)。
- 保留字段(Reserved Field)
更新消息类型时,可能会将某些字段/标识符删除。这些被删掉的字段/标识符可能被重新使用,如果加载老版本的数据时,可能会造成数据冲突,在升级时,可以将这些字段/标识符保留(reserved),这样就不会被重新使用了。
protobuf判断消息字段是否重复,是根据字段的序号判断的,因为protobuf需要向后兼容,之前历史版本字段的删减需要保留原字段的编号,比如Foo之前的字段序号2,15,9,10,11被删除,但是字段序号需要保留,定义如下。也可以标明要保留的字段名称。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
7. gRPC官网文档
这里go官网提供使用gRPC开发步骤
<1>hellworld测试:examples/helloworld
<2>route_guide测试:examples/route_guide
以上两个测试程序官网都有对应的文档,按照步骤测试执行即可。
三. gRPC四种通信方式
gRPC 允许你定义四类服务方法:
1. 简单RPC(Simple RPC):即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。
rpc SayHello(HelloRequest) returns (HelloResponse){
}
2. 服务端流式RPC(Server-side streaming RPC):一个请求对象,服务端可以传回多个结果对象。即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
3. 客户端流式RPC(Client-side streaming RPC):客户端传入多个请求对象,服务端返回一个响应结果。即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
4. 双向流式RPC(Bidirectional streaming RPC):结合客户端流式rpc和服务端流式rpc,可以传入多个对象,返回多个响应对象。即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}
四. protobuf定义的数据类型
<1>标量类型(Scalar)
proto类型 | go类型 | 备注 | proto类型 | go类型 | 备注 |
---|---|---|---|---|---|
double | float64 | float | float32 | ||
int32 | int32 | int64 | int64 | ||
uint32 | uint32 | uint64 | uint64 | ||
sint32 | int32 | 适合负数 | sint64 | int64 | 适合负数 |
fixed32 | uint32 | 固长编码,适合大于2^28的值 | fixed64 | uint64 | 固长编码,适合大于2^56的值 |
sfixed32 | int32 | 固长编码 | sfixed64 | int64 | 固长编码 |
bool | bool | string | string | UTF8 编码,长度不超过 2^32 | |
bytes | []byte | 任意字节序列,长度不超过 2^32 |
标量类型如果没有被赋值,则不会被序列化,解析时,会赋予默认值。
- strings:空字符串
- bytes:空序列
- bools:false
- 数值类型:0
<2>枚举(Enumerations)
枚举类型适用于提供一组预定义的值,选择其中一个。例如我们将性别定义为枚举类型。
message Student {
string name = 1;
enum Gender {
FEMALE = 0;
MALE = 1;
}
Gender gender = 2;
repeated int32 scores = 3;
}
- 枚举类型的第一个选项的标识符必须是0,这也是枚举类型的默认值。
- 别名(Alias),允许为不同的枚举值赋予相同的标识符,称之为别名,需要打开
allow_alias
选项,如STARTED,RUNNING标识符都是1。
message EnumAllowAlias {
enum Status {
option allow_alias = true;
UNKOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
<3>使用其他消息类型
Result
是另一个消息类型,在 SearchReponse 作为一个消息字段类型使用。下面results实际上就是程序需要返回一个Result数组: []Result,每个数组元素看作一个Result对象,有三个属性。
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
嵌套写也是支持的:和上面写法等价
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果定义在其他文件中,可以导入其他消息类型来使用:
import "myproject/other_protos.proto";
<4>任意类型(Any)
Any 可以表示不在 .proto 中定义任意的内置类型。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2; //details可以任意类型
}
<5>oneof
oneof定义的关键字段必须唯一,且序号不能重复
message Stock {
// Stock-specific data
}
message Currency {
// Currency-specific data
}
message ChangeNotification {
int32 id = 1;
oneof instrument {
Stock stock = 4;
Currency currency = 5;
}
}
<6>map
map类型字段定义方式和Java map的泛型定义类似
message MapRequest {
map<string, int32> points = 1;
}
参考资料
proto文件定义:Go Generated Code Guide | Protocol Buffers Documentation
gRPC介绍:Basics tutorial | Go | gRPC
Server到Client数据发送过程解析:gRPC 源码分析(四): gRPC server 中 frame 的处理 - 掘金
HTTP/2:RFC7540:RFC 7540/7541: Hypertext Transfer Protocol Version 2 (HTTP/2)