Grcp&Protobuf

集中整合更新旧的文章。包含 Grpc 和 Protobuf 两部分内容。

Protoc 安装配置

Grpc 和 Protobuf 的使用都需要依赖 protoc 这个二进制命令程序完成。
需要去 github 下载预编译好的二进制文件放到系统的 PATH 中。

GitHub Release 地址: https://github.com/protocolbuffers/protobuf/releases
找到自身系统的版本解压缩,并把 bin 目录设置在系统的 PATH 中。include 文件夹需要和 bin 文件夹放同一级目录中。

图1

解决 GoLand 找不到 protoc 自带的 proto 文件,可以如图设置到 GoLand 中

图2

图中 xxxx/.env/include 就是 protoc 下载下来之后自带的那个 include 文件夹。

Grpc

grpc 使用 protobuf 协议通讯,基于 http2。

参考官网: https://grpc.io/docs/languages/go/quickstart/

依赖配置

Go 语言

在项目中执行下面的命令安装插件。

1
2
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

项目中的依赖添加:

1
$ go get -u google.golang.org/grpc

传统传输定义

参考本文:跳转

流式传输

proto 定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
service Greeter {  
rpc SendStream (stream SendStreamRequest) returns (SendStreamReply) {}
rpc ReceiveStream (ReceiveStreamRequest) returns (stream ReceiveStreamReply) {}
rpc Stream (stream StreamRequest) returns (stream StreamReply) {}
}

message StreamRequest {
string message = 1;
}

message StreamReply {
string echo = 1;
}

message ReceiveStreamRequest {
string name = 1;
}

message ReceiveStreamReply {
string message = 1;
}

message SendStreamRequest {
string name = 1;
}

message SendStreamReply {
string message = 1;
}

使用 stream 声明请求参数或者返回值是个流。以下为各种流的服务端实现和客户端实现

请求流

服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (s *server) SendStream(clientStream pb.Greeter_SendStreamServer) error {  
// 读取客户端流
var msg string
for {
clientRequest, err := clientStream.Recv()
if err != nil {
if err == io.EOF {
// 流传输结束拿到数据,关闭并回复客户端
if err := clientStream.SendAndClose(&pb.SendStreamReply{Message: msg}); err != nil {
return err
}
return nil
}
return err
}
// 处理客户端
msg += "你好: " + clientRequest.Name + " "
}
}
客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func SendStream(client proto.GreeterClient) {  
streamClient, err := client.SendStream(context.Background())
if err != nil {
log.Println(err)
return
}
names := []string{"张三", "李四", "王五"}
for _, name := range names {
if err := streamClient.Send(&proto.SendStreamRequest{Name: name}); err != nil {
log.Println(err)
return
}
log.Println("发送成功: " + name)
time.Sleep(time.Second * 2)
}

resp, err := streamClient.CloseAndRecv()
if err != nil {
log.Println(err)
return
}
log.Println(resp.Message)
}

响应流

服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (s *server) ReceiveStream(clientRequest *pb.ReceiveStreamRequest, serverRespStream pb.Greeter_ReceiveStreamServer) error {  
clientName := clientRequest.Name

data := []string{
"hello",
"你好",
}

// 循环发送数据
for _, word := range data {
err := serverRespStream.Send(&pb.ReceiveStreamReply{Message: word + ":" + clientName})
if err != nil {
return err
}
// 模拟耗时
time.Sleep(time.Second * 2)
}
return nil
}
客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func ReceiveStream(client proto.GreeterClient) {  
responseStream, err := client.ReceiveStream(context.Background(), &proto.ReceiveStreamRequest{Name: "张三"})
if err != nil {
log.Println(err)
return
}

for {
resp, err := responseStream.Recv()
if err != nil {
if err.Error() == "EOF" {
break
}
log.Println(err)
return
}
log.Println(resp.Message)
}

}

双向流

服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (s *server) Stream(stream pb.Greeter_StreamServer) error {  
for {
clientRequest, err := stream.Recv()
if err != nil {
if err == io.EOF {
break
}
return err
}

if err := stream.Send(&pb.StreamReply{Echo: "Echo: " + clientRequest.Message}); err != nil {
return err
}
}
return nil
}
客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func Stream(client proto.GreeterClient) {  
stream, err := client.Stream(context.Background())
if err != nil {
log.Println(err)
return
}
if err := stream.Send(&proto.StreamRequest{Message: "张三"}); err != nil {
log.Println(err)
return
}
replay, err := stream.Recv()
if err != nil {
log.Println(err)
return
}

log.Println(replay.Echo)

if err := stream.Send(&proto.StreamRequest{Message: "李四"}); err != nil {
log.Println(err)
return
}
replay, err = stream.Recv()
if err != nil {
log.Println(err)
return
}

log.Println(replay.Echo)

}

元数据

Grpc 类似 HTTP 可已在 HEADER 中存放一些键值对。

  • 键值的 key 不区分大小写,会自动全部转为小写。
  • 无法使用 grpc- 开头的 key 名称,这是保留字段。
  • key 永远都是字符串,但是值可以是字符串也可以是二进制,二进制的值 key 需要以-bin 结尾。发送前会使用 base64 编码,收到后解码。
  • 只有服务端可以发送 trailer 数据。

客户端收发元数据

发送元数据:设置属性到 ctx 上,传入调用的 grcp 函数即可。

1
2
3
4
5
6
7
8
9
10
ctx := context.Background()  
// 创建新的元数据,同key会被覆盖
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("k2", "v2"))
// 附加元数据,同名的会被合并,不存在的会被创建
ctx = metadata.AppendToOutgoingContext(ctx, "k1", "v1")

// 普通方法
client.SayHello(ctx, &proto.HelloRequest{Name: "张三"})
// 流式传输
client.SendStream(ctx)

接受数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 普通方法
// 创建用户结束服务端发送的 header 和 trailer
// 直接以 grpc.CallOption 的方式绑定到请求上
var header, trailer metadata.MD

resp, err := client.SayHello(ctx, &proto.HelloRequest{Name: "张三"}, grpc.Header(&header), grpc.Trailer(&trailer))
if err != nil {
log.Println(err)
return
}

// 流式传输接受
// 在流中获取 header 和 trailer
log.Println(streamClient.Header())
log.Println(streamClient.Trailer())

服务端收发元数据

发送元数据

1
2
3
4
5
6
// 普通方法
grpc.SendHeader(ctx, metadata.Pairs("k1", "v1"))
grpc.SetTrailer(ctx, metadata.Pairs("k2", "v2"))
// 流式方法
stream.SendHeader(metadata.Pairs("k1", "v1"))
stream.SetTrailer(metadata.Pairs("k2", "v2"))

接受元数据

1
2
3
4
// 普通方法
header, err := metadata.FromOutgoingContext(ctx)
// 流式方法
header, err := metadata.FromIncomingContext(stream.Context())

拦截器(中间件)

社区中的开源拦截器: https://github.com/grpc-ecosystem/go-grpc-middleware

拦截器分为普通拦截器,作用于普通的 rpc 请求上。流拦截器,作用于流式请求上的数据过滤。
服务端和客户端的拦截器使用都是大同小异的。

拦截器分为:一元拦截器和流拦截器

函数签名示例

1
2
3
4
5
6
7
// 服务端
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

// 客户端
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
type StreamClientInterceptor func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)

拦截器使用

多个一元中间件使用 grpc.WithChainUnaryInterceptor 或者 grpc.ChainUnaryInterceptor,该方式遵循洋葱模型。

1
2
3
4
5
6
7
8
9
10
11
// 客户端使用中间件
conn, err := grpc.Dial("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(myInterceptor),
grpc.WithStreamInterceptor(myStreamInterceptor))
if err != nil {
panic(err)
}
// 服务端使用中间件
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(myUnaryServerInterceptor),
grpc.StreamInterceptor(myStreamServerInterceptor))

中间件函数示例

一元拦截器(客户端)

参数解释:

  • ctx:是一个上下文(context)对象,包含了当前请求的上下文信息,如请求的截止时间、请求的元数据等。
  • req:是请求的消息体,是一个 interface{} 类型的变量。该变量可以是任何类型,具体类型需要根据服务端定义的 protobuf 消息体结构而定。
  • info:是一个 *UnaryServerInfo 类型的指针,包含了请求的一些元信息,如请求的方法名称、请求的元数据等。
  • handler:是一个 UnaryHandler 类型的函数,是一元 RPC 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
1
2
3
4
5
6
7
8
9
10
11
func myInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {  
// 预处理

// 调用 rpc 方法
if err := invoker(ctx, method, req, reply, cc, opts...); err != nil {
return err
}

// 调用之后
return nil
}

一元拦截器(服务端)

参数解释:

  • ctx:是一个上下文(context)对象,包含了当前请求的上下文信息,如请求的截止时间、请求的元数据等。
  • req:是请求的消息体,是一个 interface{} 类型的变量。该变量可以是任何类型,具体类型需要根据服务端定义的 protobuf 消息体结构而定。
  • info:是一个 *UnaryServerInfo 类型的指针,包含了请求的一些元信息,如请求的方法名称、请求的元数据等。
  • handler:是一个 UnaryHandler 类型的函数,是一元 RPC 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
1
2
3
4
5
6
7
8
9
func myUnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {  
// 请求处理前
// 调用 rpc 方法
resp, err = handler(ctx, req)

// 请求处理后

return resp, nil
}

流处理拦截器(客户端)

客户端的流拦截器是重写 grpc.ClientStream 接口的方法。

参数解释:

  • ctx:是一个上下文(context)对象,包含了当前请求的上下文信息,如请求的截止时间、请求的元数据等。
  • desc:是一个 *StreamDesc 类型的指针,表示请求的流描述符。
  • cc:是一个 *ClientConn 类型的指针,表示客户端的连接对象。
  • method:是一个字符串类型的变量,表示调用的方法名称。
  • streamer:是一个 ClientStream 类型的对象,表示客户端发送请求的流对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type wrapperClientStream struct {  
grpc.ClientStream
}

func newWrapperClientStream(s grpc.ClientStream) grpc.ClientStream {
return &wrapperClientStream{s}
}

func (w *wrapperClientStream) SendMsg(m interface{}) error {
log.Printf("发送消息: (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
return w.ClientStream.SendMsg(m)
}

func (w *wrapperClientStream) RecvMsg(m interface{}) error {
log.Printf("收到消息: (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
return w.ClientStream.RecvMsg(m)
}

func myStreamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
// 请求处理前

// 返回我们包装的流对象
s, err := streamer(ctx, desc, cc, method, opts...)
if err != nil {
return nil, err
}
return newWrapperClientStream(s), nil
}

流处理拦截器(服务端)

服务端的流拦截器是重写 grpc.ClientStream 接口的方法。

参数解释:

  • srv:是一个 interface{} 类型的变量,表示实现服务端接口的对象。
  • stream:是一个 ServerStream 类型的对象,表示服务端接收到的请求的流对象。
  • info:是一个 *StreamServerInfo 类型的指针,表示请求的流信息。
  • handler:是一个 StreamHandler 类型的函数,是流 RPC 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type wrapperServerStream struct {  
grpc.ServerStream
}

func newWrapperClientStream(s grpc.ServerStream) grpc.ServerStream {
return &wrapperServerStream{s}
}

func (w *wrapperServerStream) SendMsg(m interface{}) error {
log.Printf("发送消息: (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
return w.ServerStream.SendMsg(m)
}

func (w *wrapperServerStream) RecvMsg(m interface{}) error {
log.Printf("收到消息: (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
return w.ServerStream.RecvMsg(m)
}

func myStreamServerInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// 请求处理前

if err := handler(srv, newWrapperClientStream(ss)); err != nil {
return err
}
return nil
}

Grpc Gateway

自动生成 HTTP 网关,调用 RPC 服务。
本章节内容不属于 grpc 的功能,该功能由其插件实现。这里简单介绍使用,具体参考插件文档。
插件地址: https://github.com/grpc-ecosystem/grpc-gateway

功能图如下:

图4

可以看到插件的目的就是同时生成 http 服务端和 grpc 的客户端,做了一层请求的转换。

前置准备

使用前置要求:protoc 二进制命令的所在目录的同级存在一个 include 文件夹。且该文件夹内存在 protobuf 自带的 proto 文件和 google.api 的以下 proto 文。

google.api 的 proto 文件下载地址: https://github.com/googleapis/googleapis/tree/master/google/api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bin
└── protoc

include
└── google
├── api
│   ├── annotations.proto
│   └── http.proto
└── protobuf
├── any.proto
├── api.proto
├── compiler
│   └── plugin.proto
├── descriptor.proto
├── duration.proto
├── empty.proto
├── field_mask.proto
├── source_context.proto
├── struct.proto
├── timestamp.proto
├── type.proto
└── wrappers.proto

如果 proto 没有放到上述位置,可以在编译 proto 文件时使用 -I 参数指定搜索 proto 的路径。

根据插件文档要求安装二进制插件,其中 grpc 和 protobuf 的插件之前都安装过了,所以只需要安装下面的两个插件即可:

1
2
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest

修改 proto 文件

理论上只需要修改服务端的 proto 文件即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 导入 google 的 proto
import "google/api/annotations.proto";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
// 这里添加 google.api.http 注释
// 其中 post 表示请求方式
// post 后面的路径就是请求的路由
// body “*” 表示全部的body都转换到 grpc 中
option (google.api.http) = {
post: "/say_hello"
body: "*"
};
}
}

重新编译 proto

因为插件是作用于 服务端 的,所以服务端的 proto 编译指令需要增加插件的参数:

1
2
3
4
5
6
7
protoc  --go_out=. \  
--go-grpc_out=. \
--go_opt=paths=source_relative \
--go-grpc_opt=paths=source_relative \
--grpc-gateway_out=. \
--grpc-gateway_opt=paths=source_relative \
./server/pb/*.proto

上述命令新增了 --grpc-gateway_out--grpc-gateway_opt=paths。意思和之前的 grpc 、protobuf 是相同的。

编译完成之后,会生成一个新的后缀为 .pb.gw.go 的文件。
如果新文件内引入的包有不存在的,在项目中执行 go mod tidy 补全依赖即可。

创建 HTTP 服务

修改服务端的启动部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func main() {  
listen, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(myUnaryServerInterceptor),
grpc.StreamInterceptor(myStreamServerInterceptor))
pb.RegisterGreeterServer(grpcServer, &server{})

// 服务端异步启动
go func() {
fmt.Println("grpc server on 8080")
err = grpcServer.Serve(listen)
if err != nil {
panic(err)
}
}()

// 创建一个 grpc 的客户端连接
conn, err := grpc.DialContext(context.Background(), "localhost:8080",
// WithBlock: 设置客户端必须连接上服务端才会继续执行之后的代码
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
fmt.Println(err)
return
}

// runtime 使用的是 github.com/grpc-ecosystem/grpc-gateway/v2/runtime gwmux := runtime.NewServeMux()
// 注册
if err := pb.RegisterGreeterHandler(context.Background(), gwmux, conn); err != nil {
fmt.Println(err.Error())
return
}

// 创建服务
gwServer := &http.Server{
Addr: ":8090",
Handler: gwmux,
}

// 启动 http fmt.Println("http server on 8090")
if err := gwServer.ListenAndServe(); err != nil {
fmt.Println(err.Error())
return
}
}

启动之后,尝试访问设置的地址验证。

Protobuf

是一种 IDL(Interface description language) 语言。

protobuf 的消息必须指定一个编号,且编号不能重复,其中 19000-19999 不能使用为保留字段。
1-15 占用一个字节,所以经常使用的字段使用 1-15。最好预留下。
编号不需要按顺序设置,也不需要从 1 开始编号。

编号一经分配强烈不建议在进行修改,字段删除后一定要设置为保留字段。

消息的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
syntax = "proto3";

option go_package = "server/pb";

package pb;

message people {
string name = 1;
int32 age = 2;
}

message human {
repeated people peoples = 1;
}
  • syntax = "proto3";:该行必须为文件的第一行且之前不允许出现空格。表示声明协议文件的语法为 proto3 版本,默认为 2 版本。
  • option go_package = "server/pb";:go 语言特有,指定了编译后源码存放的位置,相对 –go_out= 和 –go-grpc_out= 来说。
  • package pb;:类似命名空间。分割相同名称的消息或者服务名称
  • message:定义消息

支持的数据类型

各个类型和各个语言的对应关系参考官网:Scalar Value Types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 整数, 类型对应go中同名类型
int32 number = 1;
int64 number = 1;
uint32 number = 1;
uint64 number = 1;

// 浮点数, float 对应 go float32, double 对应 float64
float number = 1;
double number = 1;

// 字符串
string str = 1; // 对应 string utf8

// 字节
bytes data = 1; // 对应 []byte

// 字典(map)
map<string, string> data = 3;

// 切片(列表, 数组...), 使用 repeated 关键字标识后面的类型是一个数组类型
repeated string strSlice = 1; // 对应 []string
repeated int32 intSlice = 1; // 对应 []int32
repeated map<string, string> mapSlice = 3; // 对应 []map[string]string

// 嵌套消息 对应 go 结构体嵌套
message people {
string name = 1;
int32 age = 2;
}

message human {
repeated people peoples = 1;
}

服务的定义

1
2
3
4
service Greeter {  
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc Add (AddRequest) returns (AddReply) {}
}
  • service:定义服务
  • rpc:定义函数,函数的参数和返回值必须有且必须是 protobuf 中的消息

保留字段

当我们修改了 proto 文件,比如删除了某个字段,那么我们可以重新使用该字段的编号,如果其他不知情的人使用了旧版本的 proto 文件,就可能造成数据损坏或者数据泄露。所以为了防止这种情况发生,我们需要保留该编号让其他人不能在使用。

使用 reserved 关键字来指定,下面为关键字的用法。

1
2
3
4
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}

oneof

定义一个消息内的字段,多个字段只有最后一个设置的字段可以生效,之前其他字段设置的值会被删除。简而言之就是多个字段只能赋值一个字段。

1
2
3
4
5
6
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}

枚举

  1. 必须有一个零值,这样我们就可以使用0作为数值默认值。
  2. 零值必须是第一个元素,以便与 proto2语义兼容,其中第一个枚举值总是默认值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}

编译指令

1
protoc --go_out=. --go_opt=paths=source_relative ./xxx/xxx.proto
  • --go_out=.:指定生成的源码文件存放位置,一个点表示当前执行 protoc 命令所在的位置。
  • --go_opt=paths=source_relative: 使用相对位置
  • -I /xxx:指定搜索 proto 文件的文件夹路径,比如指定 google 的 proto 文件所在文件夹的位置。

导入其他 proto

proto 文件支持拆分和导入

使用 import 关键字带上其他 proto 文件的路径即可。
路径基于 --proto_path 的值,为相对路径。如果没有指定该参数应当参考 go_package 中指定的值。

1
2
3
// pb 目录下存在 book 目录,且内部有两个 proto 文件,price 依赖 book
// protoc --proto_path=pb --go_out=. --go_opt=paths=source_relative book/price.proto book/book.proto
import "book/book.proto"

生成到不同目录

a.proto 文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
syntax = "proto3";  

option go_package="demo/pb;pb";

import "b.proto";

message MsgA {
int32 a =1 ;
}

service TA {
rpc UpdateA(MsgA) returns (MsgB);
}

service TB {
rpc UpdateA(MsgA) returns (MsgB);
}

b.proto 文件内容

1
2
3
4
5
6
7
syntax = "proto3";  

option go_package="demo/pb2;pb2";

message MsgB {
int32 a = 1;
}

文件结构如下:

1
2
3
4
5
6
7
8
9
10
demo
├── a
│ └── a.proto
├── b
│ └── b.proto
├── pb
│ ├── a.pb.go
│ └── a_grpc.pb.go
└── pb2
└── b.pb.go

目标是对两个 proto 文件生成到 go 的两个包中。a 依赖 b,以下是生成命令,执行前手动建立 pb 文件夹和 pb 2 文件夹。

1
2
3
4
5
# a 文件操作
protoc -I=./a -I=./b --go_out=./pb --go-grpc_out=./pb --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./a/a.proto

# b 文件操作
protoc -I=./a -I=./b --go_out=./pb2 --go-grpc_out=./pb2 --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./b/b.proto

参数注释:

  1. -I :指定搜索 proto 文件的路径,这里写到目录即可,被编译的 proto 文件 import 路径可以直接写文件名,如果还有子文件夹则需要带上子文件夹的名称。本参数可以同时指定多个。
  2. -go_out :指定编译后的 protobuf 消息 pb 文件的保存位置。
  3. --go-grpc_out :指定编译后的 grpc 服务 pb 文件的保存位置。
  4. --go_opt=paths=source_relative :让 protoc 自动识别 go_package 参数分号前的配置,现在你可以吧分号前的路径,从你项目根目录开始写了。此参数针对 protobuf 消息 pb 文件生效。
  5. --go-grpc_opt=paths=source_relative :同上,不过此参数针对 grpc 服务的 pb 文件生效。
  6. 末尾跟上你要编译的 proto 文件路径。

如果想生成到同一个包: go_package 写成一样即可。

序列化和反序列化

在项目中添加依赖:

1
go get -u github.com/golang/protobuf/proto

序列化

1
2
3
4
5
6
7
8
func main() {
pbData := new(pb文件中的结构体)
msgbyte, err := proto.Marshal(pbData)
if err != nil {
log.Println("序列化错误消息失败:" + err.Error())
return
}
}

反序列化

1
2
3
4
5
6
7
8
func main() {
pbData := new(pb文件中的结构体)
err := proto.Unmarshal(data, pbData)
if err != nil {
msg := "消息反序列化失败:" + err.Error()
fmt.Println(msg)
}
}

Hello World

编写一个 HelloWorld 示例。

完整项目结构如下:

图3

定义服务端 protobuf

定义一个简单的 proto 协议文件,该协议文件客户端和服务端共同使用,实际开发中可以把该文件单独放在一个 git 存储库中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto3";  

// 编译后的源码存放位置
option go_package = "server/pb";

// 命名空间
package pb;

// 定义服务名称
service Greeter {
// 定义服务中的方法名称和入参出参
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 定义消息
message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

编译 proto 协议文件

编译 proto 文件为 go 语言的文件。

本示例 protoc 命令需要在 server 和 client 文件夹的父级执行。

1
protoc --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./server/pb/hello.proto

为了方便写成 Makefile 方便调用执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
build-all: build-client build-server  

build-client:
protoc --go_out=. \
--go-grpc_out=. \
--go_opt=paths=source_relative \
--go-grpc_opt=paths=source_relative \
./client/proto/*.proto

build-server:
protoc --go_out=. \
--go-grpc_out=. \
--go_opt=paths=source_relative \
--go-grpc_opt=paths=source_relative \
./server/pb/*.proto

实现服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main  

import (
"context"
"google.golang.org/grpc"
"net"
"server/pb"
)

type server struct {
pb.UnimplementedGreeterServer
}

func (s *server) SayHello(_ context.Context, request *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{
Message: "Hello" + request.Name,
}, nil
}

func main() {
listen, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
grpcServer := grpc.NewServer()
pb.RegisterGreeterServer(grpcServer, &server{})
err = grpcServer.Serve(listen)
if err != nil {
panic(err)
}
}

定义客户端 proto

实际上客户端和服务端使用的是同一份 proto 文件。这里是为了模仿两个项目简单修改了 go_package 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
syntax = "proto3";  

option go_package = "client/proto";

package pb;

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

实现客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main  

import (
"client/proto"
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"time"
)

func main() {

conn, err := grpc.Dial("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}

defer func() {
_ = conn.Close()
}()

client := proto.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
resp, err := client.SayHello(ctx, &proto.HelloRequest{Name: "张三"})
if err != nil {
log.Println(err)
return
}
log.Println(resp.Message)
cancel()
}