Yungen's blog

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 有幾個缺點:

  1. 與 gRPC 相比、傳輸同一筆資料需要消耗更多頻寬。舉例:

    json: {”id”:2} (9 bytes)

    xml: <id>42</id> (11 bytes)

    protobuf: 0x08 0x2a (2 bytes) → gRPC 的傳輸方式

  2. 傳輸方式只有一種(client 發送 request, server 給予 response)

總而言之 gRPC 的方式更適合 microservice 架構

更詳細的比較可以參考這篇:

比較 gRPC 服務與 HTTP API

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)

#Posts