# 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 文件中客户端相关代码如下，主要和实现了 HelloServiceClient 接口。用户可以通过 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
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yeasy.gitbook.io/blockchain_guide/appendix/grpc.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
