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 语言

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

$ 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

项目中的依赖添加:

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

传统传输定义

参考本文:跳转

流式传输

proto 定义

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 声明请求参数或者返回值是个流。以下为各种流的服务端实现和客户端实现

请求流

服务端
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 + "    "  
   }  
}
客户端
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)  
}

响应流

服务端
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  
}
客户端
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)  
   }  
  
}

双向流

服务端
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  
}
客户端
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 函数即可。

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)

接受数据

// 普通方法
// 创建用户结束服务端发送的 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())

服务端收发元数据

发送元数据

// 普通方法
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"))

接受元数据

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

拦截器(中间件)

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

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

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

函数签名示例

// 服务端
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,该方式遵循洋葱模型。

// 客户端使用中间件
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 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
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 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
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 类型的对象,表示客户端发送请求的流对象。
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 请求处理函数,负责对请求进行处理并返回响应结果。该函数也是一个拦截器,可以被其他拦截器所调用。
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

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 的插件之前都安装过了,所以只需要安装下面的两个插件即可:

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 文件即可。

// 导入 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 编译指令需要增加插件的参数:

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 服务

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

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 开始编号。

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

消息的定义

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

// 整数, 类型对应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;
}

服务的定义

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

保留字段

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

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

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

oneof

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

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

枚举

  1. 必须有一个零值,这样我们就可以使用0作为数值默认值。
  2. 零值必须是第一个元素,以便与 proto2语义兼容,其中第一个枚举值总是默认值。
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;
}

编译指令

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 中指定的值。

// 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 文件内容

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 文件内容

syntax = "proto3";  
  
option go_package="demo/pb2;pb2";  
  
message MsgB {  
  int32 a = 1;  
}

文件结构如下:

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 文件夹。

# 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 写成一样即可。

序列化和反序列化

在项目中添加依赖:

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

序列化

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

反序列化

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 存储库中。

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 文件夹的父级执行。

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

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

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

实现服务端

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 的值。

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;  
}

实现客户端

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()  
}