這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
gRPC是一個高效能、通用的開源RPC架構,其由Google主要面向行動裝置 App開發並基於HTTP/2協議標準而設計,基於ProtoBuf(Protocol Buffers)序列化協議開發,且支援眾多開發語言。 gRPC提供了一種簡單的方法來精確地定義服務和為iOS、Android和後台支援服務自動產生可靠性很強的用戶端功能庫。 用戶端充分利用進階流和連結功能,從而有助於節省頻寬、降低的TCP連結次數、節省CPU使用、和電池壽命。
gRPC具有以下重要特徵:
強大的IDL特性 RPC使用ProtoBuf來定義服務,ProtoBuf是由Google開發的一種資料序列化協議,效能出眾,得到了廣泛的應用。
支援多種語言 支援C++、Java、Go、Python、Ruby、C#、Node.js、Android Java、Objective-C、PHP等程式設計語言。 3.基於HTTP/2標準設計
gRPC已經應用在Google的雲端服務和對外提供的API中。
gRPC開發起來非常的簡單,你可以閱讀 一個 helloworld 的例子來瞭解它的基本開發流程 (本系列文章以Go語言的開發為例)。
最基本的開發步驟是定義 proto
檔案, 定義請求 Request 和 響應 Response 的格式,然後定義一個服務 Service, Service可以包含多個方法。
基本的gRPC開發很多文章都介紹過了,官方也有相關的文檔,這個系列的文章也就不介紹這些基礎的開發,而是想通過代碼示範gRPC更深入的開發。 作為這個系列的第一篇文章,想和大家分享一下gRPC流式開發的知識。
gRPC的流可以分為三類, 用戶端流式發送、伺服器流式返回以及用戶端/伺服器同時串流, 也就是單向流和雙向流。 下面針對這三種情況分別通過例子介紹。
伺服器流式響應
通過使用流(streaming),你可以向伺服器或者用戶端發送批量的資料, 伺服器和用戶端在接收這些資料的時候,可以不必等所有的訊息全收到後才開始響應,而是接收到第一條訊息的時候就可以及時的響應, 這顯然比以前的類HTTP 1.1的方式更快的提供響應,從而提高效能。
比如有一批記錄個人收入資料,用戶端流式發送給伺服器,伺服器計算出每個人的個人所得稅,將結果流式發給用戶端。這樣用戶端的發送可以和伺服器端的計算並行之行,從而減少服務的延遲。這隻是一個簡單的例子,你可以利用流來實現RPC調用的非同步執行,將用戶端的調用和伺服器端的執行並行的處理,
當前gRPC通過 HTTP2 協議傳輸,可以方便的實現 streaming 功能。 如果你對gRPC如何通過 HTTP2 傳輸的感興趣, 你可以閱讀這篇文章 gRPC over HTTP2, 它描述了 gRPC 通過 HTTP2 傳輸的低層格式。Request 和 Response 的格式如下:
- Request → Request-Headers *Length-Prefixed-Message EOS
- Response → (Response-Headers *Length-Prefixed-Message Trailers) / Trailers-Only
要實現伺服器的流式響應,只需在proto
中的方法定義中將響應前面加上stream
標記, 如中SayHello1
方法,HelloReply
前面加上stream
標識。
123456789101112131415161718192021 |
syntax = "proto3";package pb;import "github.com/gogo/protobuf/gogoproto/gogo.proto";// The greeting service definition.service Greeter { // Sends a greeting rpc SayHello1 (HelloRequest) returns (stream HelloReply) {}}// The request message containing the user's name.message HelloRequest { string name = 1;}// The response message containing the greetingsmessage HelloReply { string message = 1;} |
這個例子中我使用gogo來產生更有效protobuf代碼,當然你也可以使用原生的工具產生。
12 |
GOGO_ROOT=${GOPATH}/src/github.com/gogo/protobufprotoc -I.:${GOPATH}/src --gogofaster_out=plugins=grpc:. helloworld.proto |
產生的程式碼就已經包含了流的處理,所以和普通的gRPC代碼差別不是很大, 只需要注意的伺服器端代碼的實現要通過流的方式發送響應。
12345678 |
func (s *server) SayHello1(in *pb.HelloRequest, gs pb.Greeter_SayHello1Server) error {name := in.Namefor i := 0; i < 100; i++ {gs.Send(&pb.HelloReply{Message: "Hello " + name + strconv.Itoa(i)})}return nil} |
和普通的gRPC有什麼區別?
普通的gRPC是直接返回一個HelloReply
對象,而流式響應你可以通過Send
方法返回多個HelloReply
對象,物件流程序列化後流式返回。
查看它低層的實現其實是使用ServerStream.SendMsg
實現的。
123456789 |
type Greeter_SayHello1Server interface {Send(*HelloReply) errorgrpc.ServerStream}func (x *greeterSayHello1Server) Send(m *HelloReply) error {return x.ServerStream.SendMsg(m)} |
對於用戶端,我們需要關注兩個方面有沒有變化, 一是發送請求,一是讀取響應。下面是用戶端的代碼:
123456789101112131415161718192021222324 |
conn, err := grpc.Dial(*address, grpc.WithInsecure()) if err != nil { log.Fatalf("faild to connect: %v", err) } defer conn.Close() c := pb.NewGreeterClient(conn)stream, err := c.SayHello1(context.Background(), &pb.HelloRequest{Name: *name})if err != nil {log.Fatalf("could not greet: %v", err)}for {reply, err := stream.Recv()if err == io.EOF {break}if err != nil {log.Printf("failed to recv: %v", err)}log.Printf("Greeting: %s", reply.Message)} |
發送請求看起來沒有太大的區別,只是返回結果不再是一個單一的HelloReply
對象,而是一個Stream
。這和伺服器端代碼正好對應,通過調用stream.Recv()
返回每一個HelloReply
對象, 直到出錯或者流結束(io.EOF)。
可以看出,產生的程式碼提供了往/從流中方便的發送/讀取對象的能力,而這一切, gRPC都幫你產生好了。
用戶端流式發送
用戶端也可以流式的發送對象,當然這些對象也和上面的一樣,都是同一類型的對象。
首先還是要在proto
檔案中定義,與上面的定義類似,在請求的前面加上stream
標識。
12345678910111213141516171819202122 |
syntax = "proto3";package pb;import "github.com/gogo/protobuf/gogoproto/gogo.proto";option (gogoproto.unmarshaler_all) = true;// The greeting service definition.service Greeter { rpc SayHello2 (stream HelloRequest) returns (HelloReply) {}}// The request message containing the user's name.message HelloRequest { string name = 1;}// The response message containing the greetingsmessage HelloReply { string message = 1;} |
注意這裡我們只標記了請求是流式的, 響應還是以前的樣子。
產生相關的代碼後, 用戶端的代碼為:
12345678910111213141516171819 |
func sayHello2(c pb.GreeterClient) {var err errorstream, err := c.SayHello2(context.Background())for i := 0; i < 100; i++ {if err != nil {log.Printf("failed to call: %v", err)break}stream.Send(&pb.HelloRequest{Name: *name + strconv.Itoa(i)})}reply, err := stream.CloseAndRecv()if err != nil {fmt.Printf("failed to recv: %v", err)}log.Printf("Greeting: %s", reply.Message)} |
這裡的調用c.SayHello2
並沒有直接穿入請求參數,而是返回一個stream
,通過這個stream
的Send
發送,我們可以將物件流程式發送。這個例子中我們發送了100個請求。
用戶端讀取的方法是stream.CloseAndRecv()
,讀取完畢會關閉這個流的發送,這個方法返回最終結果。注意用戶端只負責關閉流的發送。
伺服器端的代碼如下:
123456789101112131415161718 |
func (s *server) SayHello2(gs pb.Greeter_SayHello2Server) error {var names []stringfor {in, err := gs.Recv()if err == io.EOF {gs.SendAndClose(&pb.HelloReply{Message: "Hello " + strings.Join(names, ",")})return nil}if err != nil {log.Printf("failed to recv: %v", err)return err}names = append(names, in.Name)}return nil} |
伺服器端收到每條訊息都進行了處理,這裡的處理簡化為增加到一個slice中。一旦它檢測的用戶端關閉了流的發送,它則把最終結果發送給用戶端,通過關閉這個流。流的關閉通過io.EOF
這個error來區分。
雙向流
將上面兩個例子整合,就是雙向流的例子。 用戶端流式發送,伺服器端流式響應,所有的發送和讀取都是串流的。
proto
中的定義如下, 請求和響應的前面都加上了stream
標識:
123456789101112131415161718192021 |
syntax = "proto3";package pb;import "github.com/gogo/protobuf/gogoproto/gogo.proto";// The greeting service definition.service Greeter { rpc SayHello3 (stream HelloRequest) returns (stream HelloReply) {}}// The request message containing the user's name.message HelloRequest { string name = 1;}// The response message containing the greetingsmessage HelloReply { string message = 1;} |
用戶端的代碼:
12345678910111213141516171819202122232425 |
func sayHello3(c pb.GreeterClient) {var err errorstream, err := c.SayHello3(context.Background())if err != nil {log.Printf("failed to call: %v", err)return}var i int64for {stream.Send(&pb.HelloRequest{Name: *name + strconv.FormatInt(i, 10)})if err != nil {log.Printf("failed to send: %v", err)break}reply, err := stream.Recv()if err != nil {log.Printf("failed to recv: %v", err)break}log.Printf("Greeting: %s", reply.Message)i++}} |
通過stream.Send
發送請求,通過stream.Recv
讀取響應。用戶端可以通過CloseSend
方法關閉發送流。
伺服器端代碼也是通過Send
發送響應,通過Recv
響應:
12345678910111213141516 |
func (s *server) SayHello3(gs pb.Greeter_SayHello3Server) error {for {in, err := gs.Recv()if err == io.EOF {return nil}if err != nil {log.Printf("failed to recv: %v", err)return err}gs.Send(&pb.HelloReply{Message: "Hello " + in.Name})}return nil} |
這基本上"退化"成一個TCP的client和server的架構。
在實際的應用中,你可以根據你的情境來使用單向流還是雙向流。