在校生的萬(wàn)字長(zhǎng)文:gRPC 實(shí)操指南(golang)
1 RPC(Remote Procedure Call Protocol)
1.1 什么是RPC
RPC即遠(yuǎn)程調(diào)用協(xié)議,簡(jiǎn)單來(lái)說就是調(diào)用遠(yuǎn)程的函數(shù)。
正常單機(jī)開發(fā)的情況下,我們通過函數(shù)的方式實(shí)現(xiàn)部分功能的解耦
func sum(num1,num2 int) int {
return num1 + num2
}
如上是一個(gè)最簡(jiǎn)單的求和函數(shù),我們只需要調(diào)用函數(shù)就可以實(shí)現(xiàn)求和的功能。
但大部分時(shí)候函數(shù)不會(huì)這么簡(jiǎn)單,尤其對(duì)于非單機(jī)的分布式系統(tǒng),遠(yuǎn)程調(diào)用就尤為重要。
1.2 RPC業(yè)務(wù)場(chǎng)景
RPC的應(yīng)用場(chǎng)景很廣泛:
?所有的分布式機(jī)都需要進(jìn)行登陸的驗(yàn)證,對(duì)于所有的主機(jī)都實(shí)現(xiàn)相同的登陸驗(yàn)證邏輯維護(hù)極差,同時(shí)也失去部分分布式意義,所以從解耦的角度考慮,我們需要定義一個(gè)統(tǒng)一的登陸驗(yàn)證業(yè)務(wù)來(lái)做。?C/S架構(gòu)的傳輸業(yè)務(wù),如股票軟件,每天需要用戶登陸的時(shí)候去服務(wù)器拉取最新的數(shù)據(jù),或者較簡(jiǎn)單的文件傳輸業(yè)務(wù),登陸驗(yàn)證業(yè)務(wù),證書業(yè)務(wù)都可以使用rpc的方式?跨語(yǔ)言開發(fā)的項(xiàng)目,比如web業(yè)務(wù)使用golang進(jìn)行開發(fā),底層使用cpp或c,部分腳本使用py,跨語(yǔ)言通信可以通過RPC提供的不同語(yǔ)言的開發(fā)機(jī)制進(jìn)行實(shí)現(xiàn)。
因而實(shí)際上,RPC就是一個(gè)遠(yuǎn)程的函數(shù),只不過RPC協(xié)議做的就是把整個(gè)過程透明化,以使得從開發(fā)角度來(lái)看,和本地函數(shù)調(diào)用沒有區(qū)別。
1.3 主流RPC框架
目前主流的RPC,有ali的Dubbo,還有g(shù)oogle的gRPC(本文主題)等
一般RPC框架如下所示:
?客戶端:客戶端作為整個(gè)RPC業(yè)務(wù)的發(fā)起者,如上所說的股票軟件,需要客戶端主動(dòng)發(fā)起請(qǐng)求去拉取最新的股票數(shù)據(jù)。?服務(wù)端:服務(wù)端接受客戶端的請(qǐng)求,并做出相應(yīng)的回應(yīng)。簡(jiǎn)單來(lái)說,函數(shù)實(shí)體在服務(wù)端,數(shù)據(jù)處理在服務(wù)端。
服務(wù)端和客戶端是每個(gè)RPC框架,開發(fā)者可見度最高的部分,實(shí)現(xiàn)RPC業(yè)務(wù)的重點(diǎn)就在于對(duì)C/S的設(shè)計(jì)和理解。首先,客戶端一定是率先發(fā)起請(qǐng)求的部分,服務(wù)端一定是具體處理請(qǐng)求的部分。比如之前我們說的求和函數(shù),函數(shù)主體一定是在服務(wù)端,客戶端有兩個(gè)數(shù)字num1,num2,向服務(wù)端發(fā)起RPC遠(yuǎn)程調(diào)用,并最后拿到求和結(jié)果。
分清C/S很重要!!!!!
?客戶端stub,服務(wù)端stub,可以變相的理解為應(yīng)用層。主要是對(duì)客戶端的rpc調(diào)用和服務(wù)端的返回進(jìn)行序列化和反序列化,并進(jìn)行傳輸,即把rpc業(yè)務(wù)抽象成tcp socket的send和receive。(gRPC使用的就是tcp,http2.0協(xié)議,建立在傳輸層)

2 gRPC
2.1 什么是gRPC
gRPC是google的開源RPC框架,引用官網(wǎng)的一句話
A high-performance, open-source universal RPC framework
如圖,展示了gRPC跨語(yǔ)言開發(fā)的結(jié)構(gòu)圖,本文將描述golang使用grpc的過程。
嚴(yán)格來(lái)說,grpc通過tcp進(jìn)行通信,使用http2.0協(xié)議,同時(shí)使用protobuf定義接口,因而相對(duì)于傳統(tǒng)的restful api來(lái)說,速度更快,數(shù)據(jù)更小,接口要求更嚴(yán)謹(jǐn)。(protobuf此處不做詳細(xì)介紹,Google Protobuf[1])

2.2 四種gRPC服務(wù)類型
準(zhǔn)確來(lái)說不應(yīng)稱為四種,實(shí)際上是因?yàn)閞pc入?yún)⒑统鰠⒍伎蓪?shí)現(xiàn)流式或非流式,進(jìn)而排列組合形成四種常用的gRPC模式。
?簡(jiǎn)單RPC

即客戶端發(fā)起一次請(qǐng)求,服務(wù)端進(jìn)行響應(yīng)(類似restful api)。這種模式下,rpc調(diào)用和本地函數(shù)基本相同,常常用于登陸驗(yàn)證,握手協(xié)議,簡(jiǎn)單業(yè)務(wù)等。
?????客戶端流RPC

即客戶端流式發(fā)送請(qǐng)求,有序發(fā)送很多req包(如文件流上傳),server接收到所有的req包后會(huì)檢測(cè)到EOF,回發(fā)一個(gè)res并關(guān)閉連接。比如云計(jì)算應(yīng)用,客戶端傳輸眾多基礎(chǔ)數(shù)據(jù),等待服務(wù)端計(jì)算完成并返回結(jié)果。
?????服務(wù)端流RPC

即客戶端發(fā)起一次請(qǐng)求,服務(wù)端會(huì)發(fā)很多res包(如文件流下載),server發(fā)送完成后關(guān)閉連接。常用于數(shù)據(jù)的拉取,如請(qǐng)求大量數(shù)據(jù),無(wú)法及時(shí)進(jìn)行反饋,進(jìn)而通過流式進(jìn)行反饋。
?????雙端流RPC

即雙方對(duì)話,可以實(shí)現(xiàn)一問一答,一問多答,多問一答等,常用于聊天室等及時(shí)通訊業(yè)務(wù)。
3 gRPC實(shí)操
3.1 環(huán)境配置
3.1.1 首先使用go get獲取grpc的官方軟件包
go get google.golang.org/grpc
3.1.2 下載protobuf編譯器
protobuf代碼生成工具[2],通過proto文件生成對(duì)應(yīng)的代碼。
(此處需要加入環(huán)境變量,各個(gè)系統(tǒng)操作不同,不贅述,protoc命令能夠正常使用即可)
3.1.3 安裝golang編譯插件
我們需要.proto最終生成可用的golang代碼,因而需要獨(dú)立安裝golang grpc的插件
go get -u github.com/golang/protobuf/protoc-gen-go
3.2 編寫proto文件
protobuf的詳細(xì)語(yǔ)法見官方文檔,此處主要介紹rpc相關(guān)的內(nèi)容
proto中rpc業(yè)務(wù)實(shí)際上就是一個(gè)函數(shù),由服務(wù)端重寫(overwrite)的函數(shù),一般網(wǎng)上的文章會(huì)把gRPC分為四種:簡(jiǎn)單RPC,服務(wù)端流RPC,客戶端流RPC,雙端流RPC。實(shí)際上區(qū)別就在于rpc函數(shù)的入?yún)⒑统鰠ⅲ酉聛?lái)詳細(xì)介紹一下四種情況,和一般的應(yīng)用場(chǎng)景。
3.2.1 簡(jiǎn)單RPC
//指定使用proto3(proto2,3有很多不同,不可混寫)
syntax = "proto3";
//指定生成的go_package,簡(jiǎn)單來(lái)說就是生成的go代碼使用什么包,即package proto
option go_package = ".;proto";
//定義rpc服務(wù)
//此處rpc服務(wù)的定義,一定要從服務(wù)端的角度考慮,即接受請(qǐng)求,處理請(qǐng)求并返回響應(yīng)的一端
//請(qǐng)求接受一個(gè)LoginReq(username+password)
//響應(yīng)回發(fā)一條msg("true" or "false")
service Login{
rpc Login(LoginReq)returns(LoginRes){}
}
message LoginReq {
string username = 1;
string password = 2;
}
message LoginRes {
string msg = 1;
}
以上就是一個(gè)簡(jiǎn)單的RPC業(yè)務(wù),功能是進(jìn)行登陸驗(yàn)證。
但實(shí)際上業(yè)務(wù)不會(huì)這么簡(jiǎn)單,比如請(qǐng)求或者響應(yīng)體特別大,肯定不能封裝到一個(gè)protobuf包進(jìn)行傳輸,因而需要使用流式傳輸,如請(qǐng)求視頻資源,或者上傳文件等,此時(shí)就引出了兩種單向流類型,即客戶端流和服務(wù)端流。
3.2.2 客戶端流RPC
簡(jiǎn)單來(lái)說,就是客戶端請(qǐng)求是個(gè)流,其他和簡(jiǎn)單RPC類似。
syntax = "proto3";
option go_package = ".;proto";
//下載服務(wù)
//請(qǐng)求接受一個(gè)UploadReq(username+password)
//響應(yīng)回發(fā)多條數(shù)據(jù)("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;
}
這里展示的應(yīng)用場(chǎng)景為上傳文件,即客戶端指定文件路徑,數(shù)據(jù)偏移量和大小,以及傳輸?shù)亩M(jìn)制數(shù)據(jù),打包通過protobuf發(fā)送給服務(wù)端,服務(wù)端不停接受req并寫文件,最終寫完之后給客戶端一個(gè)反饋res。
RPC的流指的是客戶端流式發(fā)送數(shù)據(jù),本質(zhì)上是分塊寫的思想。即每個(gè)數(shù)據(jù)包指定路徑,偏移和寫入大小,同時(shí)包含數(shù)據(jù)內(nèi)容,每次寫一個(gè)固定大小的塊(如2M),流式指的是流式發(fā)送很多個(gè)塊,如1G為512個(gè)2M的塊。
3.2.3 服務(wù)端流RPC
同上~
syntax = "proto3";
option go_package = ".;proto";
//下載服務(wù)
//請(qǐng)求接受一個(gè)DownloadReq(username+password)
//響應(yīng)回發(fā)多條數(shù)據(jù)("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;
}
理解了客戶端流,服務(wù)端流也一樣的道理,客戶端發(fā)送一個(gè)請(qǐng)求,服務(wù)端不停的發(fā)送響應(yīng),直到全部發(fā)送完成。
上述代碼的場(chǎng)景即為下載文件,發(fā)送一次請(qǐng)求,請(qǐng)求讀取某個(gè)路徑下的文件,比如讀取6M大小,從2M的位置開始讀,響應(yīng)即分為三個(gè)塊,分別包含2-4,4-6,6-8的數(shù)據(jù)(塊大小可以定制,僅以2M舉例)。
3.2.4 雙端流RPC
雙端流RPC就是入?yún)ⅲ鰠⒔詾榱鳌R话愕膽?yīng)用場(chǎng)景,如聊天室,聊天室需要維持一個(gè)長(zhǎng)鏈接,連接過程中雙方進(jìn)行通信,都是流式的信息,類似應(yīng)用場(chǎng)景使用雙端流式的RPC。
綜上,其實(shí)分類的四種RPC本質(zhì)上只是RPC函數(shù)在入?yún)⒑统鰠⑸嫌幸恍┎煌举|(zhì)上沒有太大區(qū)別。但go中具體每個(gè)rpc業(yè)務(wù)的復(fù)寫,針對(duì)流式和非流式處理不同,下面會(huì)詳細(xì)描述,golang中如何實(shí)現(xiàn)除雙端流之外的三種RPC(雙端流同理)。
3.3 生成go rpc代碼
編寫完proto文件就可以通過proto去生成對(duì)應(yīng)的go語(yǔ)言代碼了~
protoc --go_out=plugins=grpc:. *.proto
protoc為編譯器的命令,指定使用插件為grpc,輸出目錄為.(grpc:.)當(dāng)前目錄,待編譯文件為*.proto。此處可以指定某個(gè)文件編譯,也可以指定輸出目錄,這條命令會(huì)編譯當(dāng)前目錄下的所有proto文件并生成到當(dāng)前目錄。
以login為例子,生成的pb.go,rpc的核心就在Client和Server的兩個(gè)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)
}
客戶端調(diào)用Client interface的方法,服務(wù)端重寫Server interface的方法
一定要理解上述這句話!!!!!
例如這個(gè)列出服務(wù)器目錄的rpc方法,客戶端只需要?jiǎng)?chuàng)建客戶端實(shí)例對(duì)象,然后調(diào)用這個(gè)方法就可以,傳入req,接受res。因而我們說,對(duì)于客戶端來(lái)說,此次調(diào)用和本地函數(shù)沒有區(qū)別,但實(shí)際上是gRPC實(shí)現(xiàn)的遠(yuǎn)程調(diào)用,對(duì)于客戶端開發(fā)是不可見的。
再說服務(wù)端,服務(wù)端需要重寫Server中的方法,即服務(wù)端需要實(shí)現(xiàn)Server接口,對(duì)req進(jìn)行處理,并生成res,同時(shí)提供ctx上下文用作并發(fā)處理。
綜上!!!!客戶端是這個(gè)函數(shù)的調(diào)用者,需要調(diào)用這個(gè)函數(shù),服務(wù)端是這個(gè)函數(shù)的定義者,需要重寫這個(gè)函數(shù)
3.4 服務(wù)端
下述代碼皆可從我的github庫(kù)中獲得源碼grpc-example[3]
3.4.1 重寫Server interface
3.4.1.1 簡(jiǎn)單RPC
package main
import (
"context"
"grpcExample/simple_rpc/proto"
)
type LoginServer struct {}
//判斷用戶名,密碼是否為root,123456,驗(yàn)證正確即返回
func (*LoginServer)Login(ctx context.Context, req *proto.LoginReq) (*proto.LoginRes, error) {
//為降低復(fù)雜度,此處不對(duì)ctx進(jìn)行處理
if req.Username == "root" && req.Password == "123456" {
return &proto.LoginRes{Msg: "true"},nil
} else {
return &proto.LoginRes{Msg: "false"},nil
}
}
此處的login函數(shù)即為server端重寫的server interface的login函數(shù),目的是處理req,生成res并返回。整個(gè)rpc業(yè)務(wù)的核心就在于服務(wù)端重寫的方法,此處驗(yàn)證用戶名和密碼并返回提示信息。(僅用于grpc演示,忽略網(wǎng)絡(luò)安全相關(guān)內(nèi)容)
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 {
//循環(huán)接受客戶端傳的流數(shù)據(jù)
recv, err := uploadServer.Recv()
//檢測(cè)到EOF(客戶端調(diào)用close)
if err == io.EOF {
//發(fā)送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的入?yún)⑹且粋€(gè)server對(duì)象,可以通過這個(gè)server對(duì)象調(diào)用Recv函數(shù)獲取客戶端發(fā)送的每一個(gè)流。此處如果客戶端關(guān)閉連接,服務(wù)端會(huì)收到一個(gè)io.EOF的error,因而此處需要對(duì)err進(jìn)行判斷處理,如果客戶端方傳輸完成關(guān)閉連接等待響應(yīng),服務(wù)端檢測(cè)到EOF,應(yīng)調(diào)用SendAndClose發(fā)送res響應(yīng)信息并關(guān)閉連接,進(jìn)而完成客戶端流的傳輸。
3.4.1.3 服務(wù)端流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
//循環(huán)發(fā)送數(shù)據(jù)
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 注冊(cè)服務(wù)
func main() {
lis, err := net.Listen("tcp", ":6012")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
//構(gòu)建一個(gè)新的服務(wù)端對(duì)象
s := grpc.NewServer()
//向這個(gè)服務(wù)端對(duì)象注冊(cè)服務(wù)
proto.RegisterDownloadServer(s,&DownloadServer{})
//注冊(cè)服務(wù)端反射服務(wù)
reflection.Register(s)
//啟動(dòng)服務(wù)
s.Serve(lis)
//可配合ctx實(shí)現(xiàn)服務(wù)端的動(dòng)態(tài)終止
//s.Stop()
}
實(shí)際使用中,可以將這部分獨(dú)立為一個(gè)模塊,通過ctx控制server的啟動(dòng)和停止,進(jìn)而靈活的控制grpc服務(wù)。
3.5 客戶端
3.5.1 調(diào)用Client func
3.5.1.1 簡(jiǎn)單RPC
package main
import (
"context"
"google.golang.org/grpc"
"grpcExample/simple_rpc/proto"
"log"
"time"
)
func main() {
//創(chuàng)立grpc連接
grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithInsecure())
if err != nil {
log.Fatalln(err)
}
//通過grpc連接創(chuàng)建一個(gè)客戶端實(shí)例對(duì)象
client := proto.NewLoginClient(grpcConn)
//設(shè)置ctx超時(shí)(根據(jù)情況設(shè)定)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
//通過client客戶端對(duì)象,調(diào)用Login函數(shù)
res, err := client.Login(ctx, &proto.LoginReq{
Username: "root",
Password: "123456",
})
if err != nil {
log.Fatalln(err)
}
//輸出登陸結(jié)果
log.Println("the login answer is", res.Msg)
}
所以,客戶端只需要維持一個(gè)實(shí)例化的client對(duì)象,通過client調(diào)用方法就可以使用RPC服務(wù),注意和服務(wù)端不同的是,每個(gè)服務(wù)都需要一個(gè)客戶端,即服務(wù)端是在一個(gè)對(duì)象上注冊(cè)很多個(gè)服務(wù),而客戶端調(diào)用每個(gè)RPC業(yè)務(wù)都需要一個(gè)對(duì)應(yīng)函數(shù)的Client對(duì)象。
3.5.1.2 客戶端流RPC
package main
import (
"context"
"google.golang.org/grpc"
"grpcExample/client_stream_rpc/proto"
"log"
"time"
)
func main(){
//創(chuàng)立grpc連接
grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithInsecure())
if err != nil {
log.Fatalln(err)
}
//通過grpc連接創(chuàng)建一個(gè)客戶端實(shí)例對(duì)象
client := proto.NewUploadClient(grpcConn)
//設(shè)置ctx超時(shí)(根據(jù)情況設(shè)定)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
//和簡(jiǎn)單rpc不同,此時(shí)獲得的不是res,而是一個(gè)client的對(duì)象,通過這個(gè)連接對(duì)象去發(fā)送數(shù)據(jù)
uploadClient,err := client.Upload(ctx)
if err != nil {
log.Fatalln(err)
}
var offset int64
var size int64
size = 4 * 1024
//循環(huán)處理數(shù)據(jù),當(dāng)大于64kb退出
for {
err := uploadClient.Send(&proto.UploadReq{
Path: "../test.txt",
Offset: offset,
Size: size,
Data: nil,
})
if err != nil {
log.Fatalln(err)
}
offset += size
//發(fā)送超過64KB,調(diào)用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
}
}
}
客戶端流在調(diào)用函數(shù)的時(shí)候獲得的不是單純的res對(duì)象,而是一個(gè)client對(duì)象,通過這個(gè)對(duì)象控制流的發(fā)送,并且在發(fā)送完成后主動(dòng)調(diào)用CloseAndRecv去關(guān)閉連接并接受服務(wù)端的返回res。
3.5.1.3 服務(wù)端流RPC
package main
import (
"context"
"google.golang.org/grpc"
"grpcExample/server_stream_rpc/proto"
"log"
"time"
)
func main(){
//創(chuàng)立grpc連接
grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithInsecure())
if err != nil {
log.Fatalln(err)
}
//通過grpc連接創(chuàng)建一個(gè)客戶端實(shí)例對(duì)象
client := proto.NewDownloadClient(grpcConn)
//設(shè)置ctx超時(shí)(根據(jù)情況設(shè)定)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
//和簡(jiǎn)單rpc不同,此時(shí)獲得的不是res,而是一個(gè)client的對(duì)象,通過這個(gè)連接對(duì)象去讀取數(shù)據(jù)
downloadClient,err := client.Download(ctx,&proto.DownloadReq{
Path: "../test.txt",
Offset: 0,
Size: 64 * 1024,
})
if err != nil {
log.Fatalln(err)
}
//循環(huán)處理數(shù)據(jù),當(dāng)監(jiān)測(cè)到讀取完成后退出
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~")
}
此處獲取的也是一個(gè)讀取數(shù)據(jù)需要的對(duì)象,即客戶端發(fā)送請(qǐng)求后得到該對(duì)象,通過該對(duì)象調(diào)用Recv來(lái)讀取服務(wù)端流式發(fā)送的數(shù)據(jù)。
4 寫在最后
建議先理解grpc的C/S架構(gòu)
建議閱讀:
?Go gRPC教程[4]?gRPC-go example[5]
github(vx):cjq99419 歡迎提問和批評(píng)指正!
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
推薦閱讀
