> For the complete documentation index, see [llms.txt](https://yeasy.gitbook.io/blockchain_guide/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yeasy.gitbook.io/blockchain_guide/appendix/grpc.md).

# ProtoBuf 与 gRPC

[ProtoBuf](https://github.com/google/protobuf) 是一套接口描述语言（[Interface Definition Language，IDL](https://en.wikipedia.org/wiki/Interface_description_language)），类似 Apache 的 [Thrift](https://thrift.apache.org/)。

相关处理工具主要是 [protoc](https://github.com/google/protobuf)，基于 C++ 语言实现。

用户写好 `.proto` 描述文件，之后便可以使用 protoc 自动编译生成众多计算机语言（C++、Java、Python、C#、Go 等）的接口代码。这些代码可以支持 gRPC，也可以不支持。

[gRPC](https://github.com/grpc/grpc) 是 Google 开源的 RPC 框架和库，已支持主流计算机语言。底层通信采用 HTTP2 协议，比较适合互联网场景。gRPC 在设计上考虑了跟 ProtoBuf 的配合使用。

两者分别解决不同问题，可以配合使用，也可以分开单独使用。

典型的配合使用场景是，写好 `.proto` 描述文件定义 RPC 的接口，然后用 protoc（带 gRPC 插件）基于 `.proto` 模板自动生成客户端和服务端的接口代码。

## ProtoBuf

需要工具主要包括：

* 编译工具：[protoc](https://github.com/google/protobuf)，以及一些官方没有带的语言插件；
* 运行环境：各种语言的 protobuf 库，不同语言有不同的安装来源；

语法类似 C++ 语言，可以参考 ProtoBuf 语言规范：<https://developers.google.com/protocol-buffers/docs/proto。>

比较核心的，`message` 是代表数据结构（里面可以包括不同类型的成员变量，包括字符串、数字、数组、字典……），`service` 代表 RPC 接口。变量后面的数字是代表进行二进制编码时候的提示信息，1\~15 表示热变量，会用较少的字节来编码。另外，支持导入。

默认所有变量都是可选的（optional），repeated 则表示数组。主要 service rpc 接口接受单个 message 参数，返回单个 message。

参考官方给出示例，如下所示：

```protobuf
syntax = "proto3";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}
```

编译最关键的参数是输出语言格式参数，例如，python 为 `--python_out=OUT_DIR`。

一些还没有官方支持的语言，可以通过安装 protoc 对应的 plugin 来支持。例如，对于 Go 语言，可以安装

```sh
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
```

*注：旧版导入路径 `github.com/golang/protobuf` 已迁移至 `google.golang.org/protobuf`。新项目应使用新路径。*

之后，正常使用 `protoc --go_out=./ ./hello.proto` 命令调用 `protoc-gen-go` 插件来生成 hello.pb.go。

ProtoBuf 提供了 `Marshal/Unmarshal` 方法来将数据结构进行序列化操作。所生成的二进制文件在存储效率上比 XML 高 3~~10 倍，并且处理性能高 1~~2 个数量级。

## gRPC

相关工具主要包括：

* 运行时库：各种不同语言有不同的安装方法，可参考 [gRPC 官方文档](https://grpc.io/docs/) 和对应语言的 quickstart，例如 [Go quickstart](https://grpc.io/docs/languages/go/quickstart/)；主流语言的包管理器都已支持。
* protoc，以及 gRPC 插件和其它插件：采用 ProtoBuf 作为 IDL 时，对 .proto 文件进行编译处理。

类似其它 RPC 框架，gRPC 的库在服务端提供一个 gRPC Server，客户端的库是 gRPC Stub。典型的场景是客户端发送请求，同步或异步调用服务端的接口。客户端和服务端之间的通信协议是基于 HTTP2 的 [gRPC](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md) 协议，支持双工的流式保序消息，性能比较好，同时也很轻。

采用 ProtoBuf 作为 IDL，则需要定义 service 类型。生成客户端和服务端代码。用户自行实现服务端代码中的调用接口，并且利用客户端代码来发起请求到服务端。一个完整的例子可以参考 [https://github.com/grpc/grpc-go/blob/master/examples/helloworld](https://github.com/grpc/grpc-go/blob/master/examples/helloworld/)。

以上面 proto 文件为例，需要执行时添加 gRPC 的 plugin：

```sh
$ protoc --go_out=. --go-grpc_out=. hello.proto
```

*注：旧版命令 `--go_out=plugins=grpc` 已弃用。新版使用独立的 `protoc-gen-go-grpc` 插件，需分别指定 `--go_out` 和 `--go-grpc_out` 参数。*

执行后会生成对应的 .pb.go 和 \_grpc.pb.go 文件，分别包含消息结构定义和 gRPC 服务接口代码。

gRPC 更多原理可以参考[官方文档：https://grpc.io/docs/](https://grpc.io/docs/)。

### 实现服务端代码

生成的 .pb.go 文件中，服务端相关的核心代码如下，主要定义了 GreeterServer 接口，用户可以自行修改或编写实现代码。

```go
type GreeterServer interface {
    SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
    s.RegisterService(&_Greeter_serviceDesc, srv)
}
```

用户需要自行实现服务端接口，代码如下。

比较重要的，创建并启动一个 gRPC 服务的过程：

* 创建监听套接字：`lis, err := net.Listen("tcp", port)`；
* 创建服务端：`grpc.NewServer()`；
* 注册服务：`pb.RegisterGreeterServer()`；
* 启动服务端：`s.Serve(lis)`。

```go
package main

import (
    "context"
    "fmt"
    "log"
    "net"

    "google.golang.org/grpc"
    pb "hello/hello"
    )

const (
    port = ":50051"
)

type server struct{ }

// 这里实现服务端接口中的方法。
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v\n", in.GetName())
    return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

// 创建并启动一个 gRPC 服务的过程：创建监听套接字、创建服务端、注册服务、启动服务端。
func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
            log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    fmt.Printf("Starting listen on port: %s", port)
    if err := s.Serve(lis); err != nil {
            log.Fatalf("failed to serve: %v", err)
    }
}
```

编译并启动服务端。

```bash
$ go run server/server.go
Starting listen on port: :50051
```

### 生成客户端代码

生成的 Go 文件中客户端相关代码如下，主要定义和实现了 GreeterClient 接口。用户可以通过 gRPC 来直接调用这个接口。

```go
type GreeterClient interface {
    // Sends a greeting
    SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}

type greeterClient struct {
    cc *grpc.ClientConn
}

func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
    return &greeterClient{cc}
}

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
    out := new(HelloReply)
    err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}
```

用户直接调用接口方法：创建连接、创建客户端、调用接口。

```go
package main

import (
    "context"
    "log"
    "os"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    pb "hello/hello"
)

const (
    address     = "localhost:50051"
    defaultName = "world"
)

func main() {
    // Create a client connection. RPC calls use the context deadline below.
    conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
            log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    name := defaultName
    if len(os.Args) > 1 {
            name = os.Args[1]
    }
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
    if err != nil {
            log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.GetMessage())
}
```

编译并启动客户端，可以查看到服务端返回的消息。

```bash
$ go run client/client.go
Greeting: Hello world
```
