這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
前置條件:
擷取 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