avatar

初探gRPC

RPC(Remote Procedure Call)

一般在發展應用系統時,我們會將一些專屬功能程式,另外編寫成副程式,來讓其它應用程式呼叫,也稱為『程序呼叫』(Procedure Call),其運作方式如下圖所示。一個專屬程序也許會給予一個特殊功能,譬如一般資料庫系統,我們會將有關資料的查詢、更新、插入等等的動作製作成專屬程序,其它應用程式只要呼叫該程序並給予適當參數,就可以得到所需要的資訊。如果專屬程序和應用程式都存在同一部主機上,問題就比較簡單,一般也稱之為『本地程序呼叫』(Local Procedure Call, LRC)。

但近來網路應用非常普遍,常需要將某些專屬功能以一個獨立的設備來實現,以增加系統的效率。譬如,非常流行將資料庫系統以一專屬設備來服務,也就是常見到的『資料庫伺服器』(Database Server),它所提供的專屬程式(查詢、更新、插入等)就必須能夠讓遠端電腦(或客戶端)呼叫,其運作方式如下圖所示。一部專屬主機能提供各種服務程式,讓遠端電腦透過網路來查詢,此工作模式稱之為『遠端程序呼叫』(Remote Procedure Call, RPC),這就是網路上的高階程式介面。

reference link

gRPC

gRPC 是由 Google 所開源的一項 RPC(Remote Procedure Call) 專案。

由於 Google 內部使用了相當多的 Microservices, 也因此 Google 內部十分仰賴以 RPC 技術作為資料傳遞、處理的骨幹,他們內部也使用了一套稱為 Stubby 的 RPC 技術框架,可以視為 gRPC 的前身,不過隨著 SPDY, HTTP/2 及 QUIC 等技術的出現,加上多年使用 Stubby 的經驗以及為了改近 Stubby 的不足,促使 Google 決定打造一套新世代的 RPC 框架,最後造就 gRPC 問世。

Protocol Buffers

Restful AP在不同 service 之間做訊息溝通的時候,最常使用的也許是透過 json 來做傳遞,不過在使用 json 上面可能會遇到以下幾個問題:

  • json 的序列化與反序列化速度太慢
  • 透過 json 傳資料的結構不夠清楚,得靠 API 文件搞定
  • 用 json 傳資料的 size 太大

protocol buffers 便是 Google 爲了解決以上問題而生,它可以透過一個 .proto 檔案,在各語言生出相對應的檔案使用。目前支援的程式語言很多,有 Python、golang、js、java 等等。
相對於JSON和XML具有以下優點:

  • 簡潔
  • 體積小:消息大小只需要XML的1/10 ~ 1/3
  • 速度快:解析速度比XML快20 ~ 100倍
  • 使用Protobuf的編譯器,可以生成更容易在編程中使用的數據訪問代碼
  • 更好的兼容性,Protobuf設計的一個原則就是要能夠很好的支持向下或向上兼容

結構就是文件

gRPC 中的 Client Stub 及 Server Stub 就必須依賴 proto 檔來產生,可以把 proto 檔看成是 API 的定義檔,所以撰寫 gRPC 服務得先從 proto 檔下手。
以下為proto的範例

syntax = "proto3";

service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
string greeting = 1;
}

message HelloResponse {
string reply = 1;
}

Protocol Buffers 中會需要定義資料型態,這讓你明白你應該要傳入什麼類型的資料,這些資料型態可以在這個頁面找到。而 message 代表了我們要跟 gRPC 傳遞的參數、型別,service 則是敘述了 service name 以及傳入、傳回的參數。

後面的數字是什麼?

實際上每個欄位後面標註數字是 Protocol Buffers 編碼與解碼所會用到的編號,這令你能夠移除其中一個欄位而不打亂整個資料結構的編碼與解碼(除非你更改了數字編號),這些標籤在二進位編碼時會被使用到,標籤 1 到 15 在編碼時會比較省空間,16 以上的標籤在儲存時會多出一個位元組(byte),通常我們會讓重複出現(repeated)的欄位使用 15 以下的標籤,而不常出現的欄位則使用 16 以上的標籤,達到最佳化的的目的。

傳遞的內容看起來怎樣?

使用 Protocol Buffers 在傳輸資料時,會將資料「序列化」後才送出,下圖是經過序列化後的示意
image alt
而這些被「序列化」的資料,也可以被「反序列化」回來。

Protocol Buffers 的優點除了以上所提到的,從這張圖也可以發現它提升了資料的隱匿性,一般在使用 JSON 做資料交換時,我們可以很容易地了解傳遞的內容,但是透過 Protocol Buffers 來做傳輸,因為經過編碼後,只能看到一堆字節,因此在不知道proto檔的情況下,是無法解讀出裡面的內容。

Python

https://codinganimal.info/grpc-tutorial-for-python-2fa0fe2ff853

安裝套件

pip install grpcio grpcio-tools

產生gRPC的python檔案

python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. hello.proto

產生hello_pb2.pyhello_pb2_grpc.py

Server

from concurrent import futures
import time

import grpc
import hello_pb2
import hello_pb2_grpc


# 創建一個 HelloServiceServicer,要繼承自 hello_pb2_grpc.HelloServiceServicer
class HelloServiceServicer(hello_pb2_grpc.HelloServiceServicer):

# 由於我們 service 定義了 SayHello 這個 rpc,所以要實作 SayHello 這個 method
def SayHello(self, request, context):
# response 是個 HelloResponse 形態的 message
response = hello_pb2.HelloResponse()
response.reply = f'Hello, {request.greeting}'
return response


def serve():
# 創建一個 gRPC server
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

# 利用 add_HelloServiceServicer_to_server 這個 method 把上面定義的 HelloServiceServicer 加到 server 當中
hello_pb2_grpc.add_HelloServiceServicer_to_server(HelloServiceServicer(), server)

# 讓 server 跑在 port 9999 中
server.add_insecure_port('[::]:9999')
server.start()
try:
while True:
time.sleep(86400)
except KeyboardInterrupt:
server.stop(0)


if __name__ == '__main__':
serve()

Client

import grpc

import hello_pb2
import hello_pb2_grpc

# 連接到 127.0.0.1:9999
channel = grpc.insecure_channel('127.0.0.1:9999')

# 創建一個 stub (gRPC client)
stub = hello_pb2_grpc.HelloServiceStub(channel)

# 創建一個 HelloRequest 丟到 stub 去
request = hello_pb2.HelloRequest(greeting="IBDO")

# 呼叫 SayHello service,回傳 HelloResponse
response = stub.SayHello(request)

print(response.reply)

Go

https://myapollo.com.tw/zh-tw/golang-grpc-tutorial-part-1/

安裝套件

  • google.golang.org/grpc
  • github.com/golang/protobuf/protoc-gen-go

輸出go的程式碼

protoc -I . hello.proto --go_out=plugins=grpc:.

會輸出hello.pb.go

Server

package main

import (
"context"
pb "github.com/my/repo/hello"
"google.golang.org/grpc"
"log"
"net"
)

type service struct {
pb.UnimplementedHelloServiceServer
}

func (s *service) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
log.Printf("Received: %v", in.GetGreeting())
return &pb.HelloResponse{Reply: "Hello, " + in.GetGreeting()}, nil
}

func main() {
addr := "127.0.0.1:9999"
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

log.Println("Server listening on", addr)
gRPCServer := grpc.NewServer()
pb.RegisterHelloServiceServer(gRPCServer, &service{})
if err := gRPCServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

Client

package main

import (
"context"
"fmt"
pb "github.com/my/repo/hello"
"google.golang.org/grpc"
"log"
"time"
)

func main() {
addr := "127.0.0.1:9999"
conn, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("Can not connect to gRPC server: %v", err)
}
defer conn.Close()

c := pb.NewHelloServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Greeting: "Moto"})
if err != nil {
log.Fatalf("Could not get nonce: %v", err)
}
fmt.Println("Response:", r.GetReply())
}
{% note info %}
上面的code都放在[這邊](https://github.com/alenweiru/gRPC-demo)
{% endnote %}

Reference

比起 JSON 更方便、更快速、更簡短的 Protobuf 格式
初識 Protocol Buffers
API 文件就是你的伺服器,REST 的另一個選擇:gRPC
awesome-grpc

Author: Alen Chen
Link: https://alenweiru.gitlab.io/posts/89c3ff5d/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.