gRPC 實操指南(golang)

2021-02-20 nanko的快樂小窩
1 RPC(Remote Procedure Call Protocol)1.1 什麼是RPC

RPC即遠程調用協議,簡單來說就是調用遠程的函數。

正常單機開發的情況下,我們通過函數的方式實現部分功能的解耦

func sum(num1,num2 int) int {
return num1 + num2
}

如上是一個最簡單的求和函數,我們只需要調用函數就可以實現求和的功能。

但大部分時候函數不會這麼簡單,尤其對於非單機的分布式系統,遠程調用就尤為重要。

1.2 RPC業務場景

RPC的應用場景很廣泛:

•所有的分布式機都需要進行登陸的驗證,對於所有的主機都實現相同的登陸驗證邏輯維護極差,同時也失去部分分布式意義,所以從解耦的角度考慮,我們需要定義一個統一的登陸驗證業務來做。•C/S架構的傳輸業務,如股票軟體,每天需要用戶登陸的時候去伺服器拉取最新的數據,或者較簡單的文件傳輸業務,登陸驗證業務,證書業務都可以使用rpc的方式•跨語言開發的項目,比如web業務使用golang進行開發,底層使用cpp或c,部分腳本使用py,跨語言通信可以通過RPC提供的不同語言的開發機制進行實現。

因而實際上,RPC就是一個遠程的函數,只不過RPC協議做的就是把整個過程透明化,以使得從開發角度來看,和本地函數調用沒有區別。

1.3 主流RPC框架

目前主流的RPC,有ali的Dubbo,還有google的gRPC(本文主題)等

一般RPC框架如下所示:

•客戶端:客戶端作為整個RPC業務的發起者,如上所說的股票軟體,需要客戶端主動發起請求去拉取最新的股票數據。•服務端:服務端接受客戶端的請求,並做出相應的回應。簡單來說,函數實體在服務端,數據處理在服務端。

服務端和客戶端是每個RPC框架,開發者可見度最高的部分,實現RPC業務的重點就在於對C/S的設計和理解。首先,客戶端一定是率先發起請求的部分,服務端一定是具體處理請求的部分。比如之前我們說的求和函數,函數主體一定是在服務端,客戶端有兩個數字num1,num2,向服務端發起RPC遠程調用,並最後拿到求和結果。

分清C/S很重要!!!!!

•客戶端stub,服務端stub,可以變相的理解為應用層。主要是對客戶端的rpc調用和服務端的返回進行序列化和反序列化,並進行傳輸,即把rpc業務抽象成tcp socket的send和receive。(gRPC使用的就是tcp,http2.0協議,建立在傳輸層)

2 gRPC2.1 什麼是gRPC

gRPC是google的開源RPC框架,引用官網的一句話

A high-performance, open-source universal RPC framework

如圖,展示了gRPC跨語言開發的結構圖,本文將描述golang使用grpc的過程。

嚴格來說,grpc通過tcp進行通信,使用http2.0協議,同時使用protobuf定義接口,因而相對於傳統的restful api來說,速度更快,數據更小,接口要求更嚴謹。(protobuf此處不做詳細介紹,Google Protobuf[1])

2.2 四種gRPC服務類型

準確來說不應稱為四種,實際上是因為rpc入參和出參都可實現流式或非流式,進而排列組合形成四種常用的gRPC模式。

•簡單RPC

即客戶端發起一次請求,服務端進行響應(類似restful api)。這種模式下,rpc調用和本地函數基本相同,常常用於登陸驗證,握手協議,簡單業務等。

    •客戶端流RPC

即客戶端流式發送請求,有序發送很多req包(如文件流上傳),server接收到所有的req包後會檢測到EOF,回發一個res並關閉連接。比如雲計算應用,客戶端傳輸眾多基礎數據,等待服務端計算完成並返回結果。

    •服務端流RPC

即客戶端發起一次請求,服務端會發很多res包(如文件流下載),server發送完成後關閉連接。常用於數據的拉取,如請求大量數據,無法及時進行反饋,進而通過流式進行反饋。

    •雙端流RPC

即雙方對話,可以實現一問一答,一問多答,多問一答等,常用於聊天室等及時通訊業務。

3 gRPC實操3.1 環境配置3.1.1 首先使用go get獲取grpc的官方軟體包
 go get google.golang.org/grpc

3.1.2 下載protobuf編譯器

protobuf代碼生成工具[2],通過proto文件生成對應的代碼。

(此處需要加入環境變量,各個系統操作不同,不贅述,protoc命令能夠正常使用即可)

3.1.3 安裝golang編譯插件

我們需要.proto最終生成可用的golang代碼,因而需要獨立安裝golang grpc的插件

go get -u github.com/golang/protobuf/protoc-gen-go

3.2 編寫proto文件

protobuf的詳細語法見官方文檔,此處主要介紹rpc相關的內容

proto中rpc業務實際上就是一個函數,由服務端重寫(overwrite)的函數,一般網上的文章會把gRPC分為四種:簡單RPC,服務端流RPC,客戶端流RPC,雙端流RPC。實際上區別就在於rpc函數的入參和出參,接下來詳細介紹一下四種情況,和一般的應用場景。

3.2.1 簡單RPC
//指定使用proto3(proto2,3有很多不同,不可混寫)
syntax = "proto3";
//指定生成的go_package,簡單來說就是生成的go代碼使用什麼包,即package proto
option go_package = ".;proto";

//定義rpc服務
//此處rpc服務的定義,一定要從服務端的角度考慮,即接受請求,處理請求並返迴響應的一端
//請求接受一個LoginReq(username+password)
//響應回發一條msg("true" or "false")
service Login{
rpc Login(LoginReq)returns(LoginRes){}
}

message LoginReq {
string username = 1;
string password = 2;
}

message LoginRes {
string msg = 1;
}

以上就是一個簡單的RPC業務,功能是進行登陸驗證。

但實際上業務不會這麼簡單,比如請求或者響應體特別大,肯定不能封裝到一個protobuf包進行傳輸,因而需要使用流式傳輸,如請求視頻資源,或者上傳文件等,此時就引出了兩種單向流類型,即客戶端流和服務端流。

3.2.2 客戶端流RPC

簡單來說,就是客戶端請求是個流,其他和簡單RPC類似。

syntax = "proto3";
option go_package = ".;proto";

//下載服務
//請求接受一個UploadReq(username+password)
//響應回發多條數據("true" or "false")
service Upload{
rpc Upload(stream UploadReq)returns(UploadRes){}
}

message UploadReq {
string path = 1;
int64 offset = 2;
int64 size = 3;
bytes data = 4;
}

message UploadRes {
string msg = 1;
}

這裡展示的應用場景為上傳文件,即客戶端指定文件路徑,數據偏移量和大小,以及傳輸的二進位數據,打包通過protobuf發送給服務端,服務端不停接受req並寫文件,最終寫完之後給客戶端一個反饋res。

RPC的流指的是客戶端流式發送數據,本質上是分塊寫的思想。即每個數據包指定路徑,偏移和寫入大小,同時包含數據內容,每次寫一個固定大小的塊(如2M),流式指的是流式發送很多個塊,如1G為512個2M的塊。

3.2.3 服務端流RPC

同上~

syntax = "proto3";
option go_package = ".;proto";

//下載服務
//請求接受一個DownloadReq(username+password)
//響應回發多條數據("true" or "false")
service Download{
rpc Download(DownloadReq)returns(stream DownloadRes){}
}

message DownloadReq {
string path = 1;
int64 offset = 2;
int64 size = 3;
}

message DownloadRes {
int64 offset = 1;
int64 size = 2;
bytes data = 3;
}

理解了客戶端流,服務端流也一樣的道理,客戶端發送一個請求,服務端不停的發送響應,直到全部發送完成。

上述代碼的場景即為下載文件,發送一次請求,請求讀取某個路徑下的文件,比如讀取6M大小,從2M的位置開始讀,響應即分為三個塊,分別包含2-4,4-6,6-8的數據(塊大小可以定製,僅以2M舉例)。

3.2.4 雙端流RPC

雙端流RPC就是入參,出參皆為流。一般的應用場景,如聊天室,聊天室需要維持一個長連結,連接過程中雙方進行通信,都是流式的信息,類似應用場景使用雙端流式的RPC。

綜上,其實分類的四種RPC本質上只是RPC函數在入參和出參上有一些不同,本質上沒有太大區別。但go中具體每個rpc業務的複寫,針對流式和非流式處理不同,下面會詳細描述,golang中如何實現除雙端流之外的三種RPC(雙端流同理)。

3.3 生成go rpc代碼

編寫完proto文件就可以通過proto去生成對應的go語言代碼了~

 protoc --go_out=plugins=grpc:. *.proto

protoc為編譯器的命令,指定使用插件為grpc,輸出目錄為.(grpc:.)當前目錄,待編譯文件為*.proto。此處可以指定某個文件編譯,也可以指定輸出目錄,這條命令會編譯當前目錄下的所有proto文件並生成到當前目錄。

以login為例子,生成的pb.go,rpc的核心就在Client和Server的兩個interface中

Client interface

// LoginClient is the client API for Login service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type LoginClient interface {
Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginRes, error)
}

Server interface

// LoginServer is the server API for Login service.
type LoginServer interface {
Login(context.Context, *LoginReq) (*LoginRes, error)
}

客戶端調用Client interface的方法,服務端重寫Server interface的方法

一定要理解上述這句話!!!!!

例如這個列出伺服器目錄的rpc方法,客戶端只需要創建客戶端實例對象,然後調用這個方法就可以,傳入req,接受res。因而我們說,對於客戶端來說,此次調用和本地函數沒有區別,但實際上是gRPC實現的遠程調用,對於客戶端開發是不可見的。

再說服務端,服務端需要重寫Server中的方法,即服務端需要實現Server接口,對req進行處理,並生成res,同時提供ctx上下文用作並發處理。

綜上!!!!客戶端是這個函數的調用者,需要調用這個函數,服務端是這個函數的定義者,需要重寫這個函數

3.4 服務端

下述代碼皆可從我的github庫中獲得源碼grpc-example[3]

3.4.1 重寫Server interface3.4.1.1 簡單RPC
package main

import (
"context"
"grpcExample/simple_rpc/proto"
)

type LoginServer struct {}

//判斷用戶名,密碼是否為root,123456,驗證正確即返回
func (*LoginServer)Login(ctx context.Context, req *proto.LoginReq) (*proto.LoginRes, error) {
//為降低複雜度,此處不對ctx進行處理
if req.Username == "root" && req.Password == "123456" {
return &proto.LoginRes{Msg: "true"},nil
} else {
return &proto.LoginRes{Msg: "false"},nil
}
}

此處的login函數即為server端重寫的server interface的login函數,目的是處理req,生成res並返回。整個rpc業務的核心就在於服務端重寫的方法,此處驗證用戶名和密碼並返回提示信息。(僅用於grpc演示,忽略網絡安全相關內容)

3.4.1.2 客戶端流RPC
package main

import (
"grpcExample/client_stream_rpc/proto"
"io"
"log"
)

type UploadServer struct{}

func (*UploadServer) Upload(uploadServer proto.Upload_UploadServer) error {
for {
//循環接受客戶端傳的流數據
recv, err := uploadServer.Recv()
//檢測到EOF(客戶端調用close)
if err == io.EOF {
//發送res
err := uploadServer.SendAndClose(&proto.UploadRes{Msg: "finish"})
if err != nil {
return err
}
return nil
} else if err != nil{
return err
}
log.Printf("get a upload data package~ offset:%v, size:%v\n",recv.Offset,recv.Size)
}
}

客戶端流式的rpc的入參是一個server對象,可以通過這個server對象調用Recv函數獲取客戶端發送的每一個流。此處如果客戶端關閉連接,服務端會收到一個io.EOF的error,因而此處需要對err進行判斷處理,如果客戶端方傳輸完成關閉連接等待響應,服務端檢測到EOF,應調用SendAndClose發送res響應信息並關閉連接,進而完成客戶端流的傳輸。

3.4.1.3 服務端流RPC
package main

import (
"grpcExample/server_stream_rpc/proto"
"log"
)

type DownloadServer struct{}

func (*DownloadServer) Download(req *proto.DownloadReq, downloadServer proto.Download_DownloadServer) error {
offset := req.Offset
//循環發送數據
for {
err := downloadServer.Send(&proto.DownloadRes{
Offset: offset,
Size: 4 * 1024,
Data: nil,
})
if err != nil {
return err
}
offset += 4 * 1024
if offset >= req.Offset + req.Size {
break
}
}
return nil
}

3.4.2 註冊服務
func main() {
lis, err := net.Listen("tcp", ":6012")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

//構建一個新的服務端對象
s := grpc.NewServer()
//向這個服務端對象註冊服務
proto.RegisterDownloadServer(s,&DownloadServer{})
//註冊服務端反射服務
reflection.Register(s)

//啟動服務
s.Serve(lis)

//可配合ctx實現服務端的動態終止
//s.Stop()
}

實際使用中,可以將這部分獨立為一個模塊,通過ctx控制server的啟動和停止,進而靈活的控制grpc服務。

3.5 客戶端3.5.1 調用Client func3.5.1.1 簡單RPC
package main

import (
"context"
"google.golang.org/grpc"
"grpcExample/simple_rpc/proto"
"log"
"time"
)

func main() {
//創立grpc連接
grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithInsecure())
if err != nil {
log.Fatalln(err)
}

//通過grpc連接創建一個客戶端實例對象
client := proto.NewLoginClient(grpcConn)

//設置ctx超時(根據情況設定)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

//通過client客戶端對象,調用Login函數
res, err := client.Login(ctx, &proto.LoginReq{
Username: "root",
Password: "123456",
})
if err != nil {
log.Fatalln(err)
}

//輸出登陸結果
log.Println("the login answer is", res.Msg)
}

所以,客戶端只需要維持一個實例化的client對象,通過client調用方法就可以使用RPC服務,注意和服務端不同的是,每個服務都需要一個客戶端,即服務端是在一個對象上註冊很多個服務,而客戶端調用每個RPC業務都需要一個對應函數的Client對象。

3.5.1.2 客戶端流RPC
package main

import (
"context"
"google.golang.org/grpc"
"grpcExample/client_stream_rpc/proto"
"log"
"time"
)

func main(){
//創立grpc連接
grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithInsecure())
if err != nil {
log.Fatalln(err)
}

//通過grpc連接創建一個客戶端實例對象
client := proto.NewUploadClient(grpcConn)

//設置ctx超時(根據情況設定)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

//和簡單rpc不同,此時獲得的不是res,而是一個client的對象,通過這個連接對象去發送數據
uploadClient,err := client.Upload(ctx)
if err != nil {
log.Fatalln(err)
}

var offset int64
var size int64
size = 4 * 1024

//循環處理數據,當大於64kb退出
for {
err := uploadClient.Send(&proto.UploadReq{
Path: "../test.txt",
Offset: offset,
Size: size,
Data: nil,
})
if err != nil {
log.Fatalln(err)
}
offset += size
//發送超過64KB,調用CloseAndRecv方法接收response
if offset >= 64 * 1024 {
res, err := uploadClient.CloseAndRecv()
if err != nil {
log.Fatalln(err)
}
log.Println("upload over~, response is ",res.Msg)
break
}
}
}

客戶端流在調用函數的時候獲得的不是單純的res對象,而是一個client對象,通過這個對象控制流的發送,並且在發送完成後主動調用CloseAndRecv去關閉連接並接受服務端的返回res。

3.5.1.3 服務端流RPC
package main

import (
"context"
"google.golang.org/grpc"
"grpcExample/server_stream_rpc/proto"
"log"
"time"
)

func main(){
//創立grpc連接
grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithInsecure())
if err != nil {
log.Fatalln(err)
}

//通過grpc連接創建一個客戶端實例對象
client := proto.NewDownloadClient(grpcConn)

//設置ctx超時(根據情況設定)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

//和簡單rpc不同,此時獲得的不是res,而是一個client的對象,通過這個連接對象去讀取數據
downloadClient,err := client.Download(ctx,&proto.DownloadReq{
Path: "../test.txt",
Offset: 0,
Size: 64 * 1024,
})
if err != nil {
log.Fatalln(err)
}

//循環處理數據,當監測到讀取完成後退出
for {
res, err := downloadClient.Recv()
if err != nil {
log.Fatalln(err)
}
log.Printf("get a date package~ offset:%v, size:%v\n",res.Offset,res.Size)
if res.Size + res.Offset >= 64 * 1024 {
break
}
}

log.Println("download over~")
}

此處獲取的也是一個讀取數據需要的對象,即客戶端發送請求後得到該對象,通過該對象調用Recv來讀取服務端流式發送的數據。

4 寫在最後

建議先理解grpc的C/S架構

建議閱讀:

•Go gRPC教程[4]•gRPC-go example[5]

github(vx):cjq99419 歡迎提問和批評指正!

References

[1] Google Protobuf: https://developers.google.com/protocol-buffers
[2] protobuf代碼生成工具: https://github.com/protocolbuffers/protobuf/releases
[3] grpc-example: https://github.com/cjq99419/grpc-example
[4] Go gRPC教程: https://studygolang.com/articles/28205
[5] gRPC-go example: https://github.com/grpc/grpc-go/tree/master/examples

相關焦點

  • 從0到1:帶你完整寫一個 golang grpc 服務
    或者更簡單的方法,直接執行如下命令就可以安裝go install google.golang.org/grpc/cmd/protoc-gen-go-grpc第三步:下載 grpc# 安裝 grpc go get -u google.golang.org/grpc# gRPC
  • Golang 語言 gRPC 怎麼使用?
    最後,我們介紹一下 protoc 編譯生成 pb 文件需要使用的插件 protoc-gen-go 和 protoc-gen-go-grpc。插件安裝方式,具體如下:$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
  • 手把手教你快速理解gRPC!
    是不同語言在實現RPC中的具體接口。一個RPC可能對應多種API,比如同步的、異步的、回調的。std::string &key, const std::string &value)void AddTrailingMetadata (const std::string &key, const std::string &value)
  • 【Python】建立gRPC服務端與.Net Core 客戶端
    1.更新pip python -m pip install --upgrade pip2.安裝grpc python -m pip install grpcio3.安裝gRPC-tool python-m pip install grpcio-tools4.編寫.proto文件 如果對ProtoBuf不熟悉,請參考另外一篇博文【gRPC】ProtoBuf 語言快速學習指南主要是要記住幾個關鍵要點: service
  • Golang程序性能分析(三)用pprof分析gRPC服務的性能
    再一個現在很多服務都是分布式的,如果服務A調用了服務B,服務B裡的方法執行的比較耗時的話,在A的分析數據裡只能知道grpc.invoke(客戶端調用gRPC方法的請求都是由invoke發出的)耗時長,這時又得去服務B上採集數據,做不到全鏈路服務性能的採集,這塊如果誰知道好的解決方案可以在留言裡說一下。
  • gRPC doc 閱讀筆記
    gRPC 項目中的 [doc](https://github.com/grpc/grpc/blob/master/doc) ,大部分為面向使用者的功能使用介紹
  • Golang開源項目推薦(持續更新)
    一、GoGuide「Go語言學習指南」一份涵蓋大部分 Golang 程式設計師所需要掌握的核心知識、Go教程、Go開源書籍。
  • 招生實操指南,來了
    1招生實操指南
  • Golang進階版學習路線:迭代式成長(不斷更新中)
    我們始終服務的是段位: >入門 且 >=初級 <高級 認知:刻苦努力好學的同學們我們的口號是:讓一部分人先學會golang同時我們認為:1、入門階段的語法完全可以自學或看其他平臺的視頻。
  • golang mod 入門
    golang 提供了 go mod命令來管理包。/x/crypto/acme/autocert latestgo: finding golang.org/x/crypto/acme latestgo: finding golang.org/x/crypto latestbuild command-line-arguments: cannot find module for path _/home/gs/helloworld
  • Golang 入門 : 配置代理
    當我們使用 go get、go install、go mod 等命令時,類似於 golang.org/x/... 的包會是無法下載的。我們常見的 golang.org/x/... 包,一般在 GitHub 上都有官方的鏡像倉庫對應。比如 zieckey/golang.org 就是作為 golang.org/x 的鏡像庫存在的。我們可以手動下載或 clone 對應的 GitHub 倉庫到指定的目錄下,比如從 zieckey/golang.org 下載 x 目錄下的所有包。
  • Golang指南:頂級Golang框架、IDE和工具列表
    來自:碼農網,譯者:小峰連結:www.codeceo.com/article/golang-framework-ide-tools.html
  • 【法律知識】115法律人PPT速成實操指南——零基礎做出高質感PPT(完結)
    以下忽略,為內容填充115法律人PPT速成實操指南——零基礎做出高質感PPT(完結)115法律人PPT速成實操指南——零基礎做出高質感PPT(完結)  因為她忽然想到這些事的確有可能會發生的  連一蓮在法律人PPT速成實操指南——零基礎做出高質感PPT發愣。  穿紅裙的姑娘又道:「因為我看得出,你嘴裡雖然說得兇,其實心裡卻對他很關心。」  連一蓮道法律人PPT速成實操指南——零基礎做出高質感PPT:「你真的看得出我對法律人PPT速成實操指南——零基礎做出高質感PPT他很關心?」
  • golang中的fallthrough
    收錄於話題 #golang因為fallthrough不能孤立使用,需要在switch語句中使用,使用方法和break接近。不加break和fallthrough先看代碼。
  • golang 編程風格最佳實踐
    /doc/effective_go.htmluber golang 代碼規範 https://github.com/uber-go/guideuber golang 代碼規範中文 https://github.com/xxjwxc/uber_go_guide_cn代碼目錄規範
  • Golang入門教程——map篇
    今天是golang專題的第7篇文章,我們來聊聊golang當中map的用法。map這個數據結構我們經常使用,存儲的是key-value的鍵值對。在C++/java當中叫做map,在Python中叫做dict。
  • Golang的字符編碼與regexp
    需要注意的是,生成前綴字符串時其底層將調用 strings.Builder 的 WriteRune() 函數(https://github.com/golang/go/blob/master/src/regexp/syntax/prog.go#L147),內部將調用 utf8.EncodeRune() 強制轉換表達式的字符為 UTF-8 編碼(如:\xff => \xc3\xbf)。
  • golang標準庫log
    收錄於話題 #golang log.Print("my log") log.Printf("my log %d", 100) name := "tom" age := 20 log.Println(name, ",", age) log.Panic("致命錯誤!") // log.Fatal("致命錯誤!")
  • golang標準庫template
    "text/template")type Person struct { Name string可以使用管道符號|連結多個命令,用法和unix下的管道類似:|前面的命令將運算結果(或返回值)傳遞給後一個命令的最後一個位置。需要注意的是,並非只有使用了|才是pipeline。Go template中,pipeline的概念是傳遞數據,只要能產生數據的,都是pipeline。
  • Golang入門教程——面向對象篇
    今天是golang專題的第9篇文章,我們一起來看看golang當中的面向對象的部分。在現在高級語言當中,面向對象幾乎是不可或缺也是一門語言最重要的部分之一。golang作為一門剛剛誕生十年的新興語言自然是支持面向對象的,但是golang當中面向對象的概念和特性與我們之前熟悉的大部分語言都不盡相同。比如Java、Python等,相比之下, golang這個部分的設計非常得簡潔和優雅(仁者見仁),所以即使你之前沒有系統地了解過面向對象,也沒有關係,也一定能夠看懂。