gRPC short note
What is gRPC
在了解什麼是 gRPC 之前需要先解釋一下什麼是 RPC(Remote Procedure Call)。RPC 是一種程式之間溝通的方式,A 程式可以呼叫 B 程式去執行一些 function ,也可以把他想像為在一個程式中呼叫另一個程式的 function 。gRPC 為 google 推出的一個 framework 能實現上述提到的功能。
Why
未來如果要將 backend microservices 化,會遇到一個問題就是 services 之間要如何溝通,當然我們能夠沿用 HTTP 作為溝通的方式,但 HTTP 有幾個缺點:
與 gRPC 相比、傳輸同一筆資料需要消耗更多頻寬。舉例:
json:
{”id”:2}
(9 bytes)xml:
<id>42</id>
(11 bytes)protobuf:
0x08 0x2a
(2 bytes) → gRPC 的傳輸方式傳輸方式只有一種(client 發送 request, server 給予 response)
…
總而言之 gRPC 的方式更適合 microservice 架構
更詳細的比較可以參考這篇:
How
假設一個情境,我們要請我們的 database service 幫我們儲存一個使用者的聊天紀錄,並回傳是否成功儲存。在這個情境下,database service 為 gRPC 的 server 端,我們的程式為 client 端。
在傳輸任何資料前,我們需定義我們傳輸資料的格式以及 server 端會如和處理資料。gRPC 傳輸資料預設是透過 protocol buffer ,所以我們需要寫一個 proto file,裡面會描述資料是如何傳輸的。
首先我們需要寫使用的 protobuf 版本
databaseService.proto
1syntax = "proto3";
接著我們定義我們傳輸資料的格式。
1message UserMessage {
2 int32 to = 1;
3 string content = 2;
4 string type = 3;
5 string reply = 4;
6}
7
8message Reply {
9 bool ok = 1;
10}
接著我們定義我們的 service 名稱並該 service 的 method
1service DataBase {
2 rpc StoreMessage (UserMessage) returns (Reply) {}
3}
整體的 protobuf code 長這樣
1syntax = "proto3";
2service DataBase {
3 rpc StoreMessage (UserMessage) returns (Reply) {}
4}
5message UserMessage {
6 int32 to = 1;
7 string content = 2;
8 string type = 3;
9 string reply = 4;
10}
11
12message Reply {
13 bool ok = 1;
14}
上述範例中我們定義了一個 service 並且指定該 service 有一個 method 為 StoreMessage
,我們在 client 端可以直接呼叫 StoreMessage
function 並傳入相對應的 UserMessage
, server 端就會接受到該筆資訊,並會依照 Reply
回傳資料給 client 。透過範例我們可以發現 client 端不需要去在乎 StoreMessage
是怎麼去實現的,實現的邏輯就交給 server 端。
接下來我會以 Go 為 client 端、 Node.js 為 server 端來示範兩種不同語言是如何透過 gRPC 溝通。
Server 端
收先安裝必要的 packages
1npm install @grpc/grpc-js @grpc/proto-loader
因為我們使用 typescript 所以我們可以用 proto-loader-gen-types
根據我們寫的 databaseService.proto 自動生成許多 interface 讓我們可以使用 ide 的 type hint ,並且確保我們寫的程式都有符合 databaseService.proto 裡的規定。
我們先建立一個 proto
資料夾並將 databaseService.proto
放入其中,接著在 command line 中輸入以下指令
1$(npm bin)/proto-loader-gen-types --longs=String --enums=String --defaults --oneofs --grpcLib=@grpc/grpc-js --outDir=proto/ proto/databaseService.proto.proto
我們可以在 proto
資料夾中找到三個新的檔案 DataBase.ts
databaseService.ts
Reply.ts UserMessage.ts
我們將必要的 package 與 自動生成的 interfaces 載入
1import * as grpc from '@grpc/grpc-js';
2import * as protoLoader from '@grpc/proto-loader';const packageDefinition = protoLoader.loadSync('./proto/databaseService.proto')
3const proto = (grpc.loadPackageDefinition(packageDefinition) as unknown) as ProtoGrpcType;
4import { ProtoGrpcType } from './proto/databaseService'
5import { DataBaseHandlers } from './proto/DataBase'
6import { UserMessage } from './proto/UserMessage'
7import { Reply } from './proto/Reply'
載入我們寫的 databaseService.proto
1const packageDefinition = protoLoader.loadSync('./proto/databaseService.proto')
2const proto = grpc.loadPackageDefinition(packageDefinition)
接著我們寫一個 function 去實作 StoreMessage
1function MyStoreMessage(call:grpc.ServerUnaryCall<UserMessage,Reply>,callback:grpc.sendUnaryData<Reply>){
2 if(call.request){
3 // request 裡面包含了在 databaseService.proto 定義的 UserMessage 資料,我們這邊直接 console log 出來
4 console.log(call.request.to)
5 console.log(call.request.content)
6 console.log(call.request.type)
7 console.log(call.request.reply)
8 // ....
9 // 對資料庫進行操作
10 // ....
11 }
12 callback(null,{ok: true}) //callback 為回傳給 client 端的資料,需要符合 databaseService.proto 定義的 Reply 格式
13}
接著新增一個 server 並將我們寫的 function 傳入
1const server = new grpc.Server();
2server.addService(proto.DataBase.service,{StoreMessage:MyStoreMessage})
監聽一個 port 並開啟 server
1server.bindAsync(
2 '0.0.0.0:30030',
3 grpc.ServerCredentials.createInsecure(),
4 (err: Error | null, port: number) => {
5 if (err) {
6 console.error(`Server error: ${err.message}`);
7 } else {
8 console.log(`Server bound on port: ${port}`);
9 server.start();
10 }
11 }
12)
Client 端
安裝 Protocol buffer ****compiler
安裝必要的 modules
1go get -u google.golang.org/grpc
我們先建立一個 proto
資料夾並將 databaseService.proto
放入其中,接著我們要在 databaseService.proto 中新增一個設定
1option go_package = "example.com/grpc-example-client/proto"; // 此為 go 程式 go package 的路徑
在 commandline 中輸入以下指令
1protoc --go_out=. --go_opt=paths=source_relative \\
2 --go-grpc_out=. --go-grpc_opt=paths=source_relative \\
3 proto/databaseService.proto
我們可以在 proto
資料夾中找到兩個新的檔案 databaseService.pb.go
databaseService_grpc.pb.go
這兩個檔案包含了所有的 interface 以及 type
我們先 import 必要的 module 並在 main function 中建立與 server 端的連線
1package main
2
3import (
4 pb "example.com/grpc-example-client/proto"
5 "google.golang.org/grpc"
6)
7
8func main() {
9 conn, _ := grpc.Dial("localhost:30030", grpc.WithInsecure(), grpc.WithBlock())
10 defer conn.Close()
11}
接著新增一個 client
1client := pb.NewDataBaseClient(conn)
我們產生一筆資料並藉由呼叫 client.StoreMessage
執行 RPC
1userMsg := &pb.UserMessage{
2 To: 2,
3 Content: "test message",
4 Type: "message",
5 Reply: "",
6 }
7reply, _ := client.StoreMessage(context.Background(), userMsg)
8fmt.Println(reply.Ok)