Go - protobuf与gRPC入门使用

Go - protobuf与gRPC入门使用

一、Protobuf介绍

Protobuf 是一种语言中立、平台无关、可扩展的序列化数据的格式,可用于通信协议,数据存储等。

1.1 Protobuf优点

ProtoBuf 序列化数据方面它是灵活的、高效的。相比于 XML 来说,ProtoBuf 更加小巧、更加快速、更加简单。

一旦定义了要处理的数据的数据结构之后,就可以利用 ProtoBuf 的代码生成工具生成相关的代码。甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 ProtoBuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。

ProtoBuf 很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式

如果想看详细的介绍可以查看:Protobuf 官方文档

1.2 Protobuf的安装

windows:下载完成后配置环境变量即可好环境变量即可

打开cmd,输入protoc --version,如果输出版本,说明安装成功。

  • 安装Go的protobuf 库
go 复制代码
go get -u github.com/golang/protobuf/protoc-gen-go

注意,protoc-gen-go 将自动安装到 $GOPATH/bin 目录下,也需要将这个目录加入到环境变量。

  • 安装Go的gRPC的
go 复制代码
go get google.golang.org/grpc
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc

二、Protobuf简单语法

这里以一个简单的示例演示简单语法使用

2.1 编写一个pb文件

我们这里创建个person.proto 文件,将以下的经典的示例写法写入

go 复制代码
    syntax = "proto3";
    ​
    option go_package = "/person";

    package pb;
    ​
    message Person {
        string name = 1;
        int32  age = 2;
        repeated string hobbies = 3;
    }

这个.proto文件中定义了一个名为 Person 的消息类型,包含了 name、age 和 hobbies 三个字段。

  • name 和 age 都是普通的单值类型字段,hobbies 是一个字符串数组类型字段。

  • 每个字段都有一个唯一的标签号,用于标识这个字段在二进制编码中的位置和类型。

2.2 使用protoc生成GO代码

在此文件的目录下,运行protoc --go_out=. *.proto命令,即可生成GO代码。

运行后,我们可以看到该目录下多出了一个person文件夹,里面包含 Go 文件 person.pb.go。这个文件内部定义了一个结构体 Person

go 复制代码
    type Person struct {
       state         protoimpl.MessageState
       sizeCache     protoimpl.SizeCache
       unknownFields protoimpl.UnknownFields
    ​
       Name    string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
       Age     int32    `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
       Hobbies []string `protobuf:"bytes,3,rep,name=hobbies,proto3" json:"hobbies,omitempty"`
    }

除了结构体外,还有很多方法,这些方法提供了对 Protocol Buffers 消息进行编码、解码和操作的基础设施.

有以下几个主要的方法。

  • func ( *Person) Reset(): 将 Person 消息重置为默认值。
  • func (*Person) String() string: 返回一个字符串,包含 Person 消息的文本表示形式。
  • func (*Person) ProtoMessage(): 使 Person 结构体实现 proto.Message 接口,这是在序列化和反序列化 Protobuf 消息时所需的。
  • func (*Person) Descriptor() ([]byte, []int): 返回关于 Person 消息类型的描述符信息。
  • func ( *Person) GetName() string: 返回 Person 消息中 Name 字段的值。
  • func (*Person) GetAge() int32: 返回 Person 消息中 Age 字段的值。

列出一个常用的protoc的命令:

protoc --proto_path=client/pb/. --go_out=plugins=grpc:./client/code client/pb/person.proto

  • --proto_path: 当前命令扫描的pb文件目录

  • --go_out=:输出编译的go文件

  • client/pb/person.proto:表示当前编译的pb文件

2.3 序列化与反序列化

序列化和反序列化函数在github.com/golang/protobuf/proto包中

go 复制代码
    func TestPersonSerialization(t *testing.T) {
        // 创建一个 Person 消息实例并设置其字段
        p:=&Person{Name: "yzy",Age: 23,Hobbies: []string{"music","sport"}}
    ​
        // 将消息序列化为二进制格式
        data, err := proto.Marshal(p)
        if err != nil {
            t.Fatal("marshaling error: ", err)
        }
    ​
        // 反序列化消息
        p2 := &Person{}
        err = proto.Unmarshal(data, p2)
        if err != nil {
            t.Fatal("unmarshaling error: ", err)
        }
    ​
        // 比较原始消息和反序列化后的消息
        if p.String()!=p2.String() {
            t.Fatalf("original message %v != unmarshaled message %v", p, p2)
        }
    }
    ​

如果想链接跟多字段的使用可以参考:Go Protobuf 的其它字段类型)

2.4 公共包处理方式

如:有一个公共pb文件common.proto , 对于公共文件包可以新起一个Github项目单独生成PB代码,在实际项目中引用该公共的PB文件。

  • 创建一个空项目first
  • 在项目目录创建common.proto
go 复制代码
        syntax = "proto3";
        package pb;
        ​
        option go_package = "./common";
        ​
        import "google/protobuf/descriptor.proto";
        ​
        extend google.protobuf.ServiceOptions {
          optional uint32 service_option_id = 10001;
        }
        ​
        extend google.protobuf.MethodOptions {
          optional uint32 method_option_id = 10002;
        }
        ​
        extend google.protobuf.MethodOptions {
          optional string http_option_path = 10003;
        }
        ​
        message Dto {
          string name = 1;
        }
  • 使用命令生成公共文件pb的代码

    protoc --go_out=. *.proto

go 复制代码
     first
      └─common
      |       common.pb.go
      │ common.proto
      │ go.mod
  • 上传公共项目至Github上

  • 业务引用使用该公共PB

注意: 引用公共pb后需要讲其go_packge的改为github对应的地址 , 并且在使用protoc编译时不需要公共proto文件。生成的代码中会自动引入Github的公共项目。

2.5 protobuf拓展协议

2.5.1 Extend拓展

扩展是在其消息体之外定义的字段,通常位于基础 .proto 与 消息体.proto文件文件中进行分开。

为什么使用扩展?

让消息体的.proto文件将具有较少的导入/依赖项,缩短构建时间,打破循环依赖,促进松散耦合。

允许系统以最小的依赖性和协调将数据附加到容器消息。

您的用例需要对大量扩展进行非常低的协调,请考虑改用 Any消息类型

拓展示例

  • 创建基础person.proto文件 与 student.proto文件
go 复制代码
        // person.proto
        ​
        syntax = "proto2";
        package person;
        option go_package = "./person";
        ​
        message Person {
          optional string id_code = 1;
          optional string name = 2;
          extensions 100 to 190;   // 定义的拓展范围
        }
        ​
go 复制代码
        // student.proto
        ​
        syntax = "proto2";
        package student;
        ​
        option go_package = "./student";
        ​
        import "person.proto";
        ​
        // 拓展person
        extend person.Person {
          optional int32 age = 100;
        }
  • 使用命令编译文件

    protoc --go_out=. *.proto

  • 设置/获取拓展字段值

go 复制代码
        func main() {
            p1 := &person.Person{}
            proto.SetExtension(p1, student.E_Age, int32(10))
        ​
            extension := proto.GetExtension(p1, student.E_Age)
            fmt.Println(extension.(int32))
        }

2.5.2 字段选项

Protocol Buffers 还允许定义和使用自己的选项进行描述PB ,主要拓展的是google/protobuf/descriptor.proto的选项

  • google.protobuf.MessageOptions 消息描述选项
  • google.protobuf.OneofOptions 可选字段选项
  • google.protobuf.EnumOptions 枚举选项
  • google.protobuf.EnumValueOptions 枚举值选项
  • google.protobuf.FileOptions 字段选项
  • google.protobuf.serviceOptions 服务选项
  • google.protobuf.Methodoptions 方法选项

该拓展可以用于自定义一些PB的服务,方法,字段,枚举等相关的自定义的描述。

如服务id、方法id、方法路由、字段的validate检验规则的定义等。

选项示例

  • proto文件中定义拓展选项
go 复制代码
        syntax = "proto2";
        package student;
        ​
        option go_package = "./student";
        ​
        import "google/protobuf/descriptor.proto";
        import "person.proto";
        ​
        extend person.Person {
          optional int32 age = 100;
        }
        ​
        extend google.protobuf.ServiceOptions {
          optional uint32 service_option_id = 10001;
        }
        ​
        extend google.protobuf.MethodOptions {
          optional uint32 method_option_id = 10002;
        }
        ​
        extend google.protobuf.MethodOptions {
          optional string http_option_path = 10003;
        }
        ​
        ​
        message PersonRequest {
          optional string IdNum = 1;
        }
        ​
        message PersonResponse {
          optional person.Person person = 1;
        }
        ​
        service StudentService {
        ​
          option (service_option_id) = 12345;
        ​
          rpc GetPersonInfo (PersonRequest) returns (PersonResponse) {
            option (method_option_id) = 1;
            option (http_option_path) = "get/person-info";
          };
        }
  • 代码中读取解析拓展选项字段
go 复制代码
        func main() {
            studentProto := student.File_student_proto.Services()
            fmt.Println(proto.GetExtension(studentProto.ByName("StudentService").Options(), student.E_ServiceOptionId))
        ​
            methods := studentProto.ByName("StudentService").Methods()
            for i := 0; i < methods.Len(); i++ {
                method := methods.Get(i)
                methodId := proto.GetExtension(method.Options(), student.E_MethodOptionId)
                path := proto.GetExtension(method.Options(), student.E_HttpOptionPath)
                fmt.Println(fmt.Sprintf("%v %v %v", method.Name(), methodId, path))
            }
        }

三、gRPC的使用

gRPC是什么可以用官网的一句话来概括, A high-performance, open-source universal RPC framework 一个高性能、开源的通用 RPC 框架。

如果你想了解更多详情可以参考:grpc-go

3.1 服务定义

创建person.proto文件且编写如下内容:

go 复制代码
    syntax = "proto3";
    package pb;
    ​
    option go_package = "/person";
    ​
    message Person {
      string name = 1;
      int32  age = 2;
      repeated string hobbies = 3;
    }
    ​
    message PersonRequest {
      string IdNum = 1;
    }
    ​
    message PersonResponse {
      Person person = 1;
    }
    ​
    service PersonService {
      rpc GetPersonInfo (PersonRequest) returns (PersonResponse);
    }

在文件下执行 protoc --go_out=plugins=grpc:. *.proto

该命令会生成对应的grpc客户端请求的接口 、服务端需要实现的方法示例接口

3.2 服务端与客户端

3.2.1 服务端代码

  • 当上面输入命令后会生person.pb.go文件,需要自行待实现的接口
go 复制代码
        type PersonServiceServer interface {
           GetPersonInfo(context.Context, *PersonRequest) (*PersonResponse, error)
        }
  • 实现服务端接口

    创建service/person.go文件,并实现上面的自动生成的接口中的方法

go 复制代码
        type PersonServiceImpl struct{}
        ​
        func (p *PersonServiceImpl) GetPersonInfo(ctx context.Context, request *person.PersonRequest) (*person.PersonResponse, error) {
            response := &person.PersonResponse{
                Person: &person.Person{
                    Name:    "test",
                    Age:     10,
                    Hobbies: []string{"打篮球", "看书"},
                },
            }
            return response, nil
        }
  • 注册服务端代码

    创建service/main.go文件,进行启动服务,并注册方法实例

go 复制代码
        func main() {
            lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 8081))
            if err != nil {
                log.Fatalf("failed to listen: %v", err)
            }
            // 创建一个grpc Server服务对象
            s := grpc.NewServer()
            // 注册服务
            person.RegisterPersonServiceServer(s, &PersonServiceImpl{})
            // 启动RPC并监听
            log.Printf("server listening at %v", lis.Addr())
            if err := s.Serve(lis); err != nil {
                log.Fatalf("failed to serve: %v", err)
            }
        }

3.2.2 客户端代码

  • 创建client/main.go文件进行调用服务端的接口
go 复制代码
        func main() {
            conn, err := grpc.Dial("127.0.0.1:8081", grpc.WithInsecure(), grpc.WithBlock())
            if err != nil {
                log.Fatalf("did not connect: %v", err)
            }
            defer conn.Close()
        ​
            // 调用接口
            info, err := person.NewPersonServiceClient(conn).GetPersonInfo(ctx, &person.PersonRequest{
                IdNum: "15",
            })
            if err != nil {
                fmt.Println(err)
                return
            }
        ​
            fmt.Println(info)
        }

3.2.3 添加头信息

grpc可以通过MD,metadata进行定义header头部信息

  • 客户端请求添加header

    修改文件client/main.go

go 复制代码
        func main() {
            conn, err := grpc.Dial("127.0.0.1:8081", grpc.WithInsecure(), grpc.WithBlock())
            if err != nil {
                log.Fatalf("did not connect: %v", err)
            }
            defer conn.Close()
            
            // 添加头部信息
            ctx := metadata.NewOutgoingContext(context.Background(), map[string][]string{
                "operator_id":   {"110"},
                "operator_name": {"admin"},
            })
            
            
            info, err := person.NewPersonServiceClient(conn).GetPersonInfo(ctx, &person.PersonRequest{IdNum: "15"})
            if err != nil {
                fmt.Println(err)
                return
            }
            fmt.Println(info)
        }
  • 服务端获取请求header

    修改文件service/person.go

go 复制代码
        type PersonServiceImpl struct{}
        ​
        func (p *PersonServiceImpl) GetPersonInfo(ctx context.Context, request *person.PersonRequest) (*person.PersonResponse, error) {
        ​
            // 从上下文中读取header
            incomingContext, ok := metadata.FromIncomingContext(ctx)
            fmt.Println(ok, incomingContext["operator_id"], incomingContext["operator_name"])
            
            response := &person.PersonResponse{
                Person: &person.Person{
                    Name:    "test",
                    Age:     10,
                    Hobbies: []string{"打篮球", "看书"},
                },
            }
            return response, nil
        }

四、链接

Protobuf 官方文档

grpc.io/docs/

相关推荐
慕城南风1 天前
Go语言中的defer,panic,recover 与错误处理
golang·go
桃园码工2 天前
1-Gin介绍与环境搭建 --[Gin 框架入门精讲与实战案例]
go·gin·环境搭建
云中谷2 天前
Golang 神器!go-decorator 一行注释搞定装饰器,v0.22版本发布
go·敏捷开发
苏三有春3 天前
五分钟学会如何在GitHub上自动化部署个人博客(hugo框架 + stack主题)
git·go·github
我是前端小学生3 天前
Go语言中的方法和函数
go
探索云原生4 天前
在 K8S 中创建 Pod 是如何使用到 GPU 的: nvidia device plugin 源码分析
ai·云原生·kubernetes·go·gpu
自在的LEE4 天前
当 Go 遇上 Windows:15.625ms 的时间更新困局
后端·kubernetes·go
Gvto5 天前
使用FakeSMTP创建本地SMTP服务器接收邮件具体实现。
go·smtp·mailtrap
白泽来了5 天前
【Go进阶】手写 Go websocket 库(一)|WebSocket 通信协议
开源·go
witton5 天前
将VSCode配置成Goland的视觉效果
ide·vscode·编辑器·go·字体·c/c++·goland