前置條件:
獲取 gRPC-go 源碼
$ go get google.golang.org/grpc
簡單例子的源碼位置:
$ cd $GOPATH/src/google.golang.org/grpc/examples/helloworld
復雜些例子的源碼位置:
$ cd $GOPATH/src/google.golang.org/grpc/examples/route_guide
寫一個gRPC的服務,一般分下面幾步:
- 在一個 .proto 文件內定義服務。
- 用 protocol buffer 編譯器生成服務器和客戶端代碼。
- 使用 gRPC 的 Go API 為你的服務實現一個簡單的客戶端和服務器。
定義服務
在一個 .proto 文件中定義服務
簡單的例子服務定義在: examples/helloworld/helloworld/helloworld.proto
route_guide 的定義在 : examples/route_guide/routeguide/route_guide.proto
要定義一個服務,你必須在你的 .proto 文件中指定 service:
然后在你的服務中定義 rpc 方法,指定請求的和響應類型。
gRPC 允許你定義4種類型的 service 方法,這些都在 RouteGuide 服務中使用到了:
簡單RPC
一個 簡單 RPC , 客戶端發送帶參請求到服務器並等待響應返回,就像平常的函數調用一樣。
服務器端流式 RPC
一個 服務器端流式 RPC , 客戶端發送請求到服務器,拿到一個流去讀取返回的消息序列。 客戶端讀取返回的流,直到里面沒有任何消息。從例子中可以看出,通過在 響應 類型前插入 stream 關鍵字,可以指定一個服務器端的流方法。
客戶端流式 RPC
一個 客戶端流式 RPC , 客戶端寫入一個消息序列並將其發送到服務器,同樣也是使用流。一旦客戶端完成寫入消息,它等待服務器完成讀取返回它的響應。通過在 請求 類型前指定 stream 關鍵字來指定一個客戶端的流方法。
雙向流式 RPC
一個 雙向流式 RPC 是雙方使用讀寫流去發送一個消息序列。兩個流獨立操作,因此客戶端和服務器可以以任意喜歡的順序讀寫:比如, 服務器可以在寫入響應前等待接收所有的客戶端消息,或者可以交替的讀取和寫入消息,或者其他讀寫的組合。 每個流中的消息順序被預留。你可以通過在請求和響應前加 stream 關鍵字去制定方法的類型。
消息類型
上面看起來像函數參數、返回值得,由於要涉及到跨服務器調用,這些其實傳遞的是消息。我們的 .proto 文件也包含了所有請求的 protocol buffer 消息類型定義以及在服務方法中使用的響應類型——比如,下面的Point消息類型:
生成服務器和客戶端代碼
我們需要通過 protocol buffer 的編譯器 protoc 以及一個特殊的 gRPC Go 插件來完成用 protocol buffer 編譯器生成服務器和客戶端代碼。
簡單期間,有個 bash 腳本可以幫我們生成合適的代碼 codegen.sh (https://github.com/grpc/grpc-go/blob/master/codegen.sh)
運行 codegen.sh route_guide.proto 就可以在當前目錄下產生 route_guide.pb.go 文件。
在這個產生的文件中, 既有 routeGuideClient 客戶端代碼部分, 也有 routeGuideRouteChatServer 服務器段代碼部分。
創建服務器
這部分的源碼在: grpc-go/examples/route_guide/server/server.go
讓 RouteGuide 服務工作有兩個部分:
- 實現我們服務定義的生成的服務接口:做我們的服務的實際的“工作”。
- 運行一個 gRPC 服務器,監聽來自客戶端的請求並返回服務的響應。
實現RouteGuide
在源碼中,我們可以看到實現了接口RouteGuideServer的routeGuideServer數據結構。 這個接口是在route_guide.pb.go中自動產生的。
簡單RPC
GetFeature,它從客戶端拿到一個 Point 對象,然后從返回包含從數據庫拿到的feature信息的 Feature.
該方法傳入了 RPC 的上下文對象,以及客戶端的 Point 參數。它返回了Feature 響應信息和error信息。
在方法中我們遍歷所有服務器端保存的信息,找到位置信息匹配的,然后將其和一個nil錯誤一起返回給客戶端。
服務器端流式 RPC
ListFeatures 是一個服務器端的流式 RPC,我們將多個 Feature 發回給客戶端。
這里的請求參數是一個 Rectangle,客戶端期望返回多個 Feature,這次我們使用了一個請求對象和一個特殊的RouteGuide_ListFeaturesServer來寫入我們的響應,而不是得到方法參數中的入參和返回值。
在這個方法中,我們填充了盡可能多的 Feature 對象去返回,用steam的 Send() 方法把它們寫入 RouteGuide_ListFeaturesServer。
最后,我們返回了一個 nil 錯誤告訴 gRPC 響應的寫入已經完成。
如果在調用過程中發生任何錯誤,我們會返回一個非 nil 的錯誤;
客戶端流式 RPC
客戶端流方法 RecordRoute,我們通過它可以從客戶端拿到一個 Point 的流,其中包括我們需要的Point信息。
這次這個方法用了一個 RouteGuide_RecordRouteServer 流,服務器可以用它來同時讀 和 寫消息——它可以用自己的 Recv() 方法接收客戶端消息並且用 SendAndClose() 方法返回它的單個響應。
在方法體中,我們使用 RouteGuide_RecordRouteServer 的 Recv() 方法去反復讀取客戶端的請求到一個請求對象(在這個場景下是 Point),直到沒有更多的消息。
服務器需要在每次調用后檢查 Read() 返回的錯誤。如果返回值為 nil,流依然完好,可以繼續讀取;
如果返回值為 io.EOF,消息流結束,服務器可以返回它的 RouteSummary。
如果它還有其它值,我們原樣返回錯誤,gRPC 層會把它轉換為 RPC 狀態。
雙向流式 RPC
雙向流式 RPC RouteChat()。
這里讀寫的語法和客戶端流方法相似,除了服務器會使用流的 Send() 方法而不是 SendAndClose(),因為它需要寫多個響應。雖然客戶端和服務器端總是會拿到對方寫入時順序的消息,它們可以以任意順序讀寫——流的操作是完全獨立的。
啟動服務器
一旦我們實現了所有的方法,我們還需要啟動一個gRPC服務器,這樣客戶端才可以使用服務。
為了構建和啟動服務器,我們需要:
- 使用 lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) 指定我們期望客戶端請求的監聽端口。
- 使用grpc.NewServer()創建 gRPC 服務器的一個實例。
- 在 gRPC 服務器注冊我們的服務實現。
- 用服務器 Serve() 方法以及我們的端口信息區實現阻塞等待,直到進程被殺死或者 Stop() 被調用。
創建客戶端
建立跟服務器的連接
為了調用服務方法,我們首先創建一個 gRPC conn。我們通過給 grpc.Dial() 傳入服務器地址和端口號做到這點,如下:
你可以使用 DialOptions 在 grpc.Dial 中設置授權認證(如, TLS,GCE認證,JWT認證),如果服務有這樣的要求的話 —— 但是對於 RouteGuide 服務,我們不用這么做。
一旦 gRPC conn 建立起來,我們需要一個client去執行 RPC。我們通過 .proto 生成的 pb 包提供的 NewRouteGuideClient 方法來完成。
調用服務器方法
簡單RPC
調用簡單 RPC GetFeature 幾乎是和調用一個本地方法一樣直觀。
服務器端流式 RPC
我們給方法傳入一個上下文和請求。然而,我們得到返回的是一個 RouteGuide_ListFeaturesClient
實例,而不是一個應答對象。客戶端可以使用 RouteGuide_ListFeaturesClient
流去讀取服務器的響應。
我們使用 RouteGuide_ListFeaturesClient
的 Recv()
方法去反復讀取服務器的響應到一個響應 protocol buffer 對象(在這個場景下是Feature
)直到消息讀取完畢:每次調用完成時,客戶端都要檢查從 Recv()
返回的錯誤 err
。如果返回為 nil
,流依然完好並且可以繼續讀取;如果返回為 io.EOF
,則說明消息流已經結束;否則就一定是一個通過 err
傳過來的 RPC 錯誤。
客戶端流式 RPC
RouteGuide_RecordRouteClient 有一個 Send() 方法,我們可以用它來給服務器發送請求。一旦我們完成使用 Send() 方法將客戶端請求寫入流,就需要調用流的 CloseAndRecv()方法,讓 gRPC 知道我們已經完成了寫入同時期待返回應答。我們從 CloseAndRecv() 返回的 err 中獲得 RPC 的狀態。如果狀態為nil,那么CloseAndRecv()的第一個返回值將會是合法的服務器應答。
雙向流式 RPC
我們只給函數傳入一個上下文對象,拿到可以用來讀寫的流。
運行例子
服務器端:
$ go run server/server.go
客戶端:
$ go run client/client.go
參考資料:
中文 gRPC 基礎 Go http://doc.oschina.net/grpc?t=60133
英文 gRPC 基礎 Go http://www.grpc.io/docs/tutorials/basic/go.html