1. 程式人生 > >ProtoBuf 與 gRPC 你需要知道的知識

ProtoBuf 與 gRPC 你需要知道的知識

ProtoBuf 是一套介面描述語言(IDL)和相關工具集(主要是 protoc,基於 C++ 實現),類似 Apache 的 Thrift)。使用者寫好 .proto 描述檔案,之後使用 protoc 可以很容易編譯成眾多計算機語言(C++、Java、Python、C#、Golang 等)的介面程式碼。這些程式碼可以支援 gRPC,也可以不支援。

gRPC 是 Google 開源的 RPC 框架和庫,已支援主流計算機語言。底層通訊採用 gRPC 協議,比較適合網際網路場景。gRPC 在設計上考慮了跟 ProtoBuf 的配合使用。

兩者分別解決的不同問題,可以配合使用,也可以分開。

典型的配合使用場景是,寫好 .proto 描述檔案定義 RPC 的介面,然後用 protoc(帶 gRPC 外掛)基於 .proto 模板自動生成客戶端和服務端的介面程式碼。

ProtoBuf

需要工具主要包括:

  • 編譯器:protoc,以及一些官方沒有帶的語言外掛;
  • 執行環境:各種語言的 protobuf 庫,不同語言有不同的安裝來源;

語法類似 C++ 語言,可以參考 語言規範

比較核心的,message 是代表資料結構(裡面可以包括不同型別的成員變數,包括字串、數字、陣列、字典……),service代表 RPC 介面。變數後面的數字是代表進行二進位制編碼時候的提示資訊,1~15 表示熱變數,會用較少的位元組來編碼。另外,支援匯入。

預設所有變數都是可選的(optional),repeated 則表示陣列。主要 service rpc 介面只能接受單個 message 引數,返回單個 message;

syntax = "proto3";
package hello;

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
  repeated int32 number=4;
}

service HelloService {
  rpc SayHello(HelloRequest) returns
(HelloResponse){} }

編譯最關鍵引數是指定輸出語言格式,例如,python 為 --python_out=OUT_DIR

一些還沒有官方支援的語言,可以通過安裝 protoc 對應的 plugin 來支援。例如,對於 go 語言,可以安裝

$ go get -u github.com/golang/protobuf/{protoc-gen-go,proto} // 前者是 plugin;後者是 go 的依賴庫

之後,正常使用 protoc --go_out=./ hello.proto 來生成 hello.pb.go,會自動呼叫 protoc-gen-go 外掛。

ProtoBuf 提供了 Marshal/Unmarshal 方法來將資料結構進行序列化操作。所生成的二進位制檔案在儲存效率上比 XML 高 3~10 倍,並且處理效能高 1~2 個數量級。

gRPC

工具主要包括:

  • 執行時庫:各種不同語言有不同的 安裝方法,主流語言的包管理器都已支援。
  • protoc,以及 grpc 外掛和其它外掛:採用 ProtoBuf 作為 IDL 時,對 .proto 檔案進行編譯處理。

官方文件 寫的挺全面了。

類似其它 RPC 框架,gRPC 的庫在服務端提供一個 gRPC Server,客戶端的庫是 gRPC Stub。典型的場景是客戶端傳送請求,同步或非同步呼叫服務端的介面。客戶端和服務端之間的通訊協議是基於 HTTP2 的 gRPC 協議,支援雙工的流式保序訊息,效能比較好,同時也很輕。

採用 ProtoBuf 作為 IDL,則需要定義 service 型別。生成客戶端和服務端程式碼。使用者自行實現服務端程式碼中的呼叫介面,並且利用客戶端程式碼來發起請求到服務端。一個完整的例子可以參考 這裡

以上面 proto 檔案為例,需要執行時新增 grpc 的 plugin:

$ protoc --go_out=plugins=grpc:. hello.proto

生成服務端程式碼

服務端相關程式碼如下,主要定義了 HelloServiceServer 介面,使用者可以自行編寫實現程式碼。

type HelloServiceServer interface {
        SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
}

func RegisterHelloServiceServer(s *grpc.Server, srv HelloServiceServer) {
        s.RegisterService(&_HelloService_serviceDesc, srv)
}

使用者需要自行實現服務端介面,程式碼如下。

比較重要的,建立並啟動一個 gRPC 服務的過程:

  • 建立監聽套接字:lis, err := net.Listen("tcp", port)
  • 建立服務端:grpc.NewServer()
  • 註冊服務:pb.RegisterHelloServiceServer()
  • 啟動服務端:s.Serve(lis)
type server struct{}

// 這裡實現服務端介面中的方法。
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

// 建立並啟動一個 gRPC 服務的過程:建立監聽套接字、建立服務端、註冊服務、啟動服務端。
func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterHelloServiceServer(s, &server{})
    s.Serve(lis)
}

編譯並啟動服務端。

生成客戶端程式碼

生成的 go 檔案中客戶端相關程式碼如下,主要和實現了 HelloServiceClient 介面。使用者可以通過 gRPC 來直接呼叫這個介面。

type HelloServiceClient interface {
        SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
}

type helloServiceClient struct {
        cc *grpc.ClientConn
}

func NewHelloServiceClient(cc *grpc.ClientConn) HelloServiceClient {
        return &helloServiceClient{cc}
}

func (c *helloServiceClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
        out := new(HelloResponse)
        err := grpc.Invoke(ctx, "/hello.HelloService/SayHello", in, out, c.cc, opts...)
        if err != nil {
                return nil, err
        }
        return out, nil
}

使用者直接呼叫介面方法:建立連線、建立客戶端、呼叫介面。

func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewHelloServiceClient(conn)

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

編譯並啟動客戶端,檢視到服務端返回的訊息。