Go 的 gRPC 和 Protocol Buffers---Quick Start

本文最后更新于 a year ago,文中所描述的信息可能已发生改变。

Protocol Buffers

Protocol Buffers (Protobuf) 是一种语言无关、平台无关、可扩展的序列化结构化数据的方法, 可以像 XML, json 等一样用于程序间数据通信. 而相较于上列二者, Protobuf 有更高的效率.

使用 Protobuf 需要在 .proto 文件中定义数据结构, 并安装其编译器来生成对应语言的模型. 本文以 Go 为例.

安装

TIP提示

本文环境:

  • Debian 12 amd64
  • Go 1.21.3
  • Protoc 24.4

安装 Protobuf 编译器最简单的方法是在 Github Release 页面下载对应平台的二进制文件, 并加入 PATH 环境变量.

bash
wget https://github.com/protocolbuffers/protobuf/releases/download/v24.4/protoc-24.4-linux-x86_64.zip
mkdir protoc && unzip protoc-24.4-linux-x86_64.zip -d protoc
export PATH=$PATH:/path/to/protoc/bin # 将此行加入终端配置文件中

为了在 Go 中使用 Protobuf 和默认使用 Protobuf 的 gRPC, 还需要安装 protoc-gen-goprotoc-gen-go-grpc

bash
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH="$PATH:$(go env GOPATH)/bin" # 把 GOBIN 加入 PATH

语法和规范

Protobuf 的主要语法很简单也很 Go, 下例:

proto
syntax = "proto3"; // 指定版本

option go_package = "example/proto"; // 指定 Go 代码生成的包名

package artwork; // 包名, 用于区分不同的 proto 文件. 注意此包名并不是 Go 中的包名

// 定义消息类型
message ArtworkInfo {
    uint64 artworkID = 1; 
    string title = 2;
    SourceName source = 3; // 引用枚举类型
    repeated string tags = 4; // repeated 表示为数组
    bool r18 = 5;
    repeated PictureInfo pictures = 6; // 引用嵌套消息类型

    // 定义枚举类型
    enum SourceName {
        Pixiv = 0;
        Twitter = 1;
        // ...
    }

    // 定义嵌套消息类型
    message PictureInfo {
        uint64 pictureID = 1;
        uint64 width = 2;
        uint64 height = 3;
    }
}

可以看到 Proto 的消息定义和 Go 的结构体定义很像, 简要总结:

  • 消息类型定义: message 关键字 + 消息名, 在大括号内定义字段
  • 字段定义: 字段类型, 字段名, 编号. 编号是不可省略且在同一消息定义下不可重复的. 可以理解成字段的唯一ID.

在规范上, Protobuf 有如下约定:

  • 包名应为小写, 且使用和目录结构一致的点分法, 如 example.package
  • 使用驼峰命名法命名消息类型和服务, 如 ExampleMessage, ExampleService
  • 使用蛇形命名法命名字段, 如 example_field
  • 使用双斜杠注释, 如 // 注释内容

关于 Protobuf 的数据类型和各语言中的对应, 可以参考 官方文档#scalar

编译生成 Go 代码

.proto 文件所在目录下执行:

bash
protoc --go_out=. example.proto

protoc 会在当前目录下生成 proto/example.pb.go 文件, 其中包含了 example.proto 中定义的消息类型, 并且提供了一些方便的方法.

由 protoc 生成的文件不应直接修改, 如需更改, 应修改 .proto 文件后重新编译生成.

Protobuf 常与 gRPC 结合使用, 作为服务之间远程调用的数据传输格式.

gRPC

gRPC 是一个基于 HTTP/2 的"高性能、开源和通用的 RPC 框架". gRPC 默认使用 Protobuf 作为接口定义语言和数据传输格式.

gRPC、 Protobuf 和 Go 都是由 Google 开发的, 这三者结合使用具有套装效果.

安装

在项目目录下执行:

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

定义 gRPC 服务和方法

定义一个服务, 并指定其可以被远程调用的方法, 以及方法的参数和返回值类型. 这即是 gRPc 定义服务的思想.

在上文的 example.proto 中定义一个服务:

proto
syntax = "proto3"; // 指定版本

option go_package = "example/proto"; // 指定 Go 代码生成的包名

package artwork; // 包名, 用于区分不同的 proto 文件. 注意此包名并不是 Go 中的包名

// 定义消息类型
message ArtworkInfo {
    uint64 artworkID = 1; 
    string title = 2;
    SourceName source = 3; // 引用枚举类型
    repeated string tags = 4; // repeated 表示为数组
    bool r18 = 5;
    repeated PictureInfo pictures = 6; // 引用嵌套消息类型

    // 定义枚举类型
    enum SourceName {
        Pixiv = 0;
        Twitter = 1;
        // ...
    }

    // 定义嵌套消息类型
    message PictureInfo {
        uint64 pictureID = 1;
        uint64 width = 2;
        uint64 height = 3;
    }
}

message GetArtworkInfoRequest { 
    uint64 artworkID = 1;
} 

// 定义服务
service ArtworkService { 
    // 定义方法
    rpc GetArtworkInfo (GetArtworkInfoRequest) returns (ArtworkInfo);
} 

gRPC 方法

gRPC 有四种方法类型:

  1. 一元方法
  2. 服务端流式方法
  3. 客户端流式方法
  4. 双向流式方法
一元方法

一元方法是最简单常用的方法. 客户端将请求消息作为参数发送给服务, 服务返回一个响应消息. 一元方法调用完成.

在实际代码中, 一元方法的调用就像是调用一个普通的函数.

上文的 GetArtworkInfo 方法就是一个一元方法.

服务端流式方法

服务端流式方法是指客户端发送一个请求消息, 服务端返回一个流式响应消息. 客户端从返回的流中读取响应消息, 直到没有消息为止.

定义流式方法, 需要在参数或返回值前加上 stream 关键字.

例如, 定义一个 GetArtworkInfoList 方法, 用于获取多个作品的信息:

proto
syntax = "proto3"; // 指定版本

option go_package = "example/proto"; // 指定 Go 代码生成的包名

package artwork; // 包名, 用于区分不同的 proto 文件. 注意此包名并不是 Go 中的包名

// 定义消息类型
message ArtworkInfo {
    uint64 artworkID = 1; 
    string title = 2;
    SourceName source = 3; // 引用枚举类型
    repeated string tags = 4; // repeated 表示为数组
    bool r18 = 5;
    repeated PictureInfo pictures = 6; // 引用嵌套消息类型

    // 定义枚举类型
    enum SourceName {
        Pixiv = 0;
        Twitter = 1;
        // ...
    }

    // 定义嵌套消息类型
    message PictureInfo {
        uint64 pictureID = 1;
        uint64 width = 2;
        uint64 height = 3;
    }
}

message GetArtworkInfoRequest {
    uint64 artworkID = 1;
}

// 定义服务
service ArtworkService {
    // 定义方法
    rpc GetArtworkInfo (GetArtworkInfoRequest) returns (ArtworkInfo);
    rpc GetArtworkInfoList (GetArtworkInfoRequest) returns (stream ArtworkInfo); // 定义流式方法 //[!code ++]
}
客户端流式方法

与服务端流式方法相反, 客户端向数据流中写入并发送一系列消息, 服务端从流中读取这些消息, 直到没有消息为止, 然后返回一个响应消息.

在方法参数前加上 stream 关键字即可定义客户端流式方法.

双向流式方法

双向流式方法是指客户端和服务端都可以通过一个读写数据流来发送一系列消息. 这两个流独立运行, 因此客户端和服务端可以以任意顺序读写: 例如, 服务端可以在写入响应前等待接收所有客户端消息, 或者可以先读取一个消息, 然后再写入一个消息, 或者读取或写入多个消息, 甚至可以同时读写.

在方法参数和返回值前加上 stream 关键字即可定义双向流式方法.

生成 Go 代码

上文所述的内容都还是在 proto 文件中完成, 要想在 Go 中使用, 还需要生成 Go 代码.

在项目目录下执行:

bash
protoc --go_out=. --go-grpc_out=. example.proto

protoc 会在当前目录下生成 proto/example.pb.goproto/example_grpc.pb.go 两个文件, 前者包含了 example.proto 中定义的消息类型, 后者包含了 example.proto 中定义的服务和方法.

服务端实现

实现上文定义的 ArtworkService 服务, 需要做两部分工作:

  1. 实现 ArtworkService 服务中定义的所有方法. 即实现 example_grpc.pb.go 中的 ArtworkServiceServer 接口.
  2. 运行一个 gRPC 服务器, 监听来自客户端的请求并返回响应.

实现服务方法

打开 example_grpc.pb.go 文件, 可以看到 ArtworkServiceServer 接口的定义:

go
// ArtworkServiceServer is the server API for ArtworkService service.
// All implementations must embed UnimplementedArtworkServiceServer
// for forward compatibility
type ArtworkServiceServer interface {
 GetArtworkInfo(context.Context, *GetArtworkInfoRequest) (*ArtworkInfo, error)
 GetArtworkInfoList(*GetArtworkInfoRequest, ArtworkService_GetArtworkInfoListServer) error
 mustEmbedUnimplementedArtworkServiceServer()
}

在项目中导入 example/proto 包, 并实现 ArtworkServiceServer 接口:

go
package main

import (
    "context"
    "log"
    "net"

    "example/proto"
    "google.golang.org/grpc"
)

type ArtworkServiceServer struct {
    proto.UnimplementedArtworkServiceServer // 为了实现向前兼容, 需要嵌入此结构体
}

func (s *ArtworkServiceServer) GetArtworkInfo(ctx context.Context, req *proto.GetArtworkInfoRequest) (*proto.ArtworkInfo, error) {
    // 实现
    return &proto.ArtworkInfo{}, nil
}

func (s *ArtworkServiceServer) GetArtworkInfoList(req *proto.GetArtworkInfoRequest, stream proto.ArtworkService_GetArtworkInfoListServer) error {
    // 实现
    for i := 0; i < 10; i++ {
        err := stream.Send(&proto.ArtworkInfo{})
        if err != nil {
            return err
        }
    }
    return nil
}

var server = &ArtworkServiceServer{}

func main() {
}

可以看到, 流式方法和一元方法的区别在于, 流式方法的参数或返回值是一个流, 需要向流中写入或读取消息.

go
func (s *ArtworkServiceServer) GetArtworkInfoList(req *proto.GetArtworkInfoRequest, stream proto.ArtworkService_GetArtworkInfoListServer) error {
    // 实现
    for i := 0; i < 10; i++ {
        err := stream.Send(&proto.ArtworkInfo{})
        if err != nil {
            return err
        }
    }
    return nil
}

运行 gRPC 服务器

main 函数中, 创建一个 gRPC 服务器, 并注册 ArtworkServiceServer:

go
func main() {
    lis, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    proto.RegisterArtworkServiceServer(s, server)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

WARNING注意

此处没有使用任何安全措施

客户端调用

在客户端调用 gRPC 服务, 只需要初始化连接, 并创建客户端, 然后直接调用定义的方法.

go
package main

import (
    "context"
    "log"

    "example/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    conn, err := grpc.Dial("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    client := proto.NewArtworkServiceClient(conn)
    resp, err := client.GetArtworkInfo(context.Background(), &proto.GetArtworkInfoRequest{ArtworkID: 1})
    if err != nil {
        log.Fatalf("could not get artwork info: %v", err)
    }
    log.Println(resp)
}

而对于流式方法, 客户端需要从流中读取响应消息, 直到没有消息为止.

go
package main

import (
    "context"
    "log"
    "io"

    "example/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    conn, err := grpc.Dial("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    client := proto.NewArtworkServiceClient(conn)
    stream, err := client.GetArtworkInfoList(context.Background(), &proto.GetArtworkInfoRequest{ArtworkID: 1})
    if err != nil {
        log.Fatalf("could not get artwork info: %v", err)
    }
    for {
        resp, err := stream.Recv()
        if err == io.EOF { // 判断是否已结束
            break
        }
        if err != nil {
            log.Fatalf("could not get artwork info: %v", err)
        }
        log.Println(resp)
    }
}

参考


To Be Continued.
Go 的 gRPC 和 Protocol Buffers---TLS 认证
使用conda安装和管理python多版本环境