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 什麼是gRPCgRPC是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:. *.protoprotoc為編譯器的命令,指定使用插件為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 簡單RPCpackage 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 客戶端流RPCpackage 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 服務端流RPCpackage 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 簡單RPCpackage 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 客戶端流RPCpackage 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 服務端流RPCpackage 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