ProtoBuf 是一套接口描述語言(IDL)和相關(guān)工具集(主要是 protoc,基于 C++ 實現(xiàn)),類似 Apache 的 Thrift)。用戶寫好 .proto 描述文件,之后使用 protoc 可以很容易編譯成眾多計算機語言(C++、Java、Python、C#、Golang 等)的接口代碼。這些代碼可以支持 gRPC,也可以不支持。
gRPC 是 Google 開源的 RPC 框架和庫,已支持主流計算機語言。底層通信采用 gRPC 協(xié)議,比較適合互聯(lián)網(wǎng)場景。gRPC 在設(shè)計上考慮了跟 ProtoBuf 的配合使用。
兩者分別解決的不同問題,可以配合使用,也可以分開。
典型的配合使用場景是,寫好 .proto 描述文件定義 RPC 的接口,然后用 protoc(帶 gRPC 插件)基于 .proto 模板自動生成客戶端和服務(wù)端的接口代碼。
ProtoBuf
需要工具主要包括:
- 編譯器:protoc,以及一些官方?jīng)]有帶的語言插件;
- 運行環(huán)境:各種語言的 protobuf 庫,不同語言有不同的安裝來源;
語法類似 C++ 語言,可以參考 語言規(guī)范。
比較核心的,message 是代表數(shù)據(jù)結(jié)構(gòu)(里面可以包括不同類型的成員變量,包括字符串、數(shù)字、數(shù)組、字典……),service代表 RPC 接口。變量后面的數(shù)字是代表進行二進制編碼時候的提示信息,1~15 表示熱變量,會用較少的字節(jié)來編碼。另外,支持導(dǎo)入。
默認所有變量都是可選的(optional),repeated 則表示數(shù)組。主要 service rpc 接口只能接受單個 message 參數(shù),返回單個 message;
syntax = "proto3";
package hello;
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
service HelloService {
rpc SayHello(HelloRequest) returns (HelloResponse){}
}
編譯最關(guān)鍵參數(shù)是指定輸出語言格式,例如,python 為 --python_out=OUT_DIR。
一些還沒有官方支持的語言,可以通過安裝 protoc 對應(yīng)的 plugin 來支持。例如,對于 go 語言,可以安裝
$ go get -u github.com/golang/protobuf/{protoc-gen-go,proto} // 前者是 plugin;后者是 go 的依賴庫
之后,正常使用 protoc --go_out=./ hello.proto 來生成 hello.pb.go,會自動調(diào)用 protoc-gen-go 插件。
ProtoBuf 提供了 Marshal/Unmarshal 方法來將數(shù)據(jù)結(jié)構(gòu)進行序列化操作。所生成的二進制文件在存儲效率上比 XML 高 3~10 倍,并且處理性能高 1~2 個數(shù)量級。
gRPC
工具主要包括:
- 運行時庫:各種不同語言有不同的 安裝方法,主流語言的包管理器都已支持。
- protoc,以及 grpc 插件和其它插件:采用 ProtoBuf 作為 IDL 時,對 .proto 文件進行編譯處理。
官方文檔 寫的挺全面了。
類似其它 RPC 框架,gRPC 的庫在服務(wù)端提供一個 gRPC Server,客戶端的庫是 gRPC Stub。典型的場景是客戶端發(fā)送請求,同步或異步調(diào)用服務(wù)端的接口??蛻舳撕头?wù)端之間的通信協(xié)議是基于 HTTP2 的 gRPC 協(xié)議,支持雙工的流式保序消息,性能比較好,同時也很輕。
采用 ProtoBuf 作為 IDL,則需要定義 service 類型。生成客戶端和服務(wù)端代碼。用戶自行實現(xiàn)服務(wù)端代碼中的調(diào)用接口,并且利用客戶端代碼來發(fā)起請求到服務(wù)端。一個完整的例子可以參考 這里。
以上面 proto 文件為例,需要執(zhí)行時添加 grpc 的 plugin:
$ protoc --go_out=plugins=grpc:. hello.proto
生成服務(wù)端代碼
服務(wù)端相關(guān)代碼如下,主要定義了 HelloServiceServer 接口,用戶可以自行編寫實現(xiàn)代碼。
type HelloServiceServer interface {
SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
}
func RegisterHelloServiceServer(s *grpc.Server, srv HelloServiceServer) {
s.RegisterService(&_HelloService_serviceDesc, srv)
}
用戶需要自行實現(xiàn)服務(wù)端接口,代碼如下。
比較重要的,創(chuàng)建并啟動一個 gRPC 服務(wù)的過程:
- 創(chuàng)建監(jiān)聽套接字:
lis, err := net.Listen("tcp", port); - 創(chuàng)建服務(wù)端:
grpc.NewServer(); - 注冊服務(wù):
pb.RegisterHelloServiceServer(); - 啟動服務(wù)端:
s.Serve(lis)。
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "proto/proto"
)
type server struct{}
// 這里實現(xiàn)服務(wù)端接口中的方法。
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{Message: "Hello1222222121" + in.Name}, nil
}
// 創(chuàng)建并啟動一個 gRPC 服務(wù)的過程:創(chuàng)建監(jiān)聽套接字、創(chuàng)建服務(wù)端、注冊服務(wù)、啟動服務(wù)端。
func main() {
lis, err := net.Listen("tcp", ":1200")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterHelloServiceServer(s, &server{})
s.Serve(lis)
}
編譯并啟動服務(wù)端。
生成客戶端代碼
生成的 go 文件中客戶端相關(guān)代碼如下,主要和實現(xiàn)了 HelloServiceClient 接口。用戶可以通過 gRPC 來直接調(diào)用這個接口。
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
}
用戶直接調(diào)用接口方法:創(chuàng)建連接、創(chuàng)建客戶端、調(diào)用接口。
package main
import (
"log"
"os"
"golang.org/x/net/context"
"google.golang.org/grpc"
pb "proto/proto"
)
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial("127.0.0.1:1200", 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 := "SoWhat"
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)
}
編譯并啟動客戶端,查看到服務(wù)端返回的消息。