Rpc基本概念
RPC(Remote Procedure Call)遠程過程調用是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議,簡單的理解是一個節點請求另一個節點提供的服務。RPC只是一套協議,基於這套協議規範來實現的框架都可以稱爲 RPC 框架,比較典型的有 Dubbo、Thrift 和 gRPC。
RPC 是遠程過程調用的方式之一,涉及調用方和被調用方兩個進程的交互。因爲 RPC 提供類似於本地方法調用的形式,所以對於調用方來說,調用 RPC 方法和調用本地方法並沒有明顯區別。
一個完整的 RPC 框架包含了服務註冊發現、負載、容錯、序列化、協議編碼和網絡傳輸等組件。不同的 RPC 框架包含的組件可能會有所不同,但是一定都包含 RPC 協議相關的組件,RPC 協議包括序列化、協議編解碼器和網絡傳輸棧,如下圖所示:
RPC 協議一般分爲公有協議和私有協議。例如,HTTP、SMPP、WebService 等都是公有協議。如果是某個公司或者組織內部自定義、自己使用的,沒有被國際標準化組織接納和認可的協議,往往劃爲私有協議,例如 Thrift 協議和螞蟻金服的 Bolt 協議。
RPC和HTTP區別
RPC 和 HTTP都是微服務間通信較爲常用的方案之一,其實RPC 和 HTTP 並不完全是同一個層次的概念,它們之間還是有所區別的。
- RPC 是遠程過程調用,其調用協議通常包括序列化協議和傳輸協議。序列化協議有基於純文本的 XML 和 JSON、二進制編碼的Protobuf和Hessian。傳輸協議是指其底層網絡傳輸所使用的協議,比如 TCP、HTTP。
- 可以看出HTTP是RPC的傳輸協議的一個可選方案,比如說 gRPC 的網絡傳輸協議就是 HTTP。HTTP 既可以和 RPC 一樣作爲服務間通信的解決方案,也可以作爲 RPC 中通信層的傳輸協議(此時與之對比的是 TCP 協議)。
Go語言原生有RPC包,RPC過程調用實現起來非常簡單。服務端只需實現對外提供的遠程過程方法和結構體,然後將其註冊到 RPC 服務中,客戶端就可以通過其服務名稱和方法名稱進行 RPC 方法調用。
gRPC特點
在gRPC的客戶端應用可以想調用本地對象一樣直接調用另一臺不同的機器上的服務端的應用的對象或者方法,這樣在創建分佈式應用的時候更容易。
- 語言無關,支持多種語言;
- 基於 IDL 文件定義服務,gRPC使用protocol buffer 作爲接口定義語言(IDL)來描述服務接口和有效負載消息的結構。通過 proto3 工具生成指定語言的數據結構、服務端接口以及客戶端 Stub。
- 通信協議基於標準的 HTTP/2 設計,支持雙向流、消息頭壓縮、單 TCP 的多路複用、服務端推送等特性,這些特性使得 gRPC 在移動端設備上更加省電和節省網絡流量;
- 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一種語言無關的高性能序列化框架,基於 HTTP/2 + PB, 保障了 RPC 調用的高性能。
gRPC使用和上面RPC使用方法類似,首先定義服務,指定其能夠被遠程調用的方法,包括參數和返回類型,這裏使用protobuf來定義服務。在服務端實現定義的服務接口,並運行一個gRPC服務器來處理客戶端調用。
gRPC代碼結構
- 先使用protobuf定義服務。
syntax = "proto3" ;
//package myproto ;
option go_package = ".;protoes";
//定義服務
service HelloServer {
rpc SayHello (HelloReq) returns (HelloRsp){}
rpc SayName (NameReq) returns (NameRsp){}
}
//客戶端發送給服務端
message HelloReq {
string name = 1 ;
}
//服務端返回給客戶端
message HelloRsp {
string msg = 1 ;
}
//客戶端發送給服務端
message NameReq {
string name = 1 ;
}
//服務端返回給客戶端
message NameRsp {
string msg = 1 ;
}
定義了兩個服務SayHello,SayName及對應的四個消息(message)。然後在執行命令生成pd.go文件
protoc --go_out=plugins=grpc:./ *.proto #添加grpc插件
- 編寫服務端server.go
package main
import (
"fmt"
"net"
"google.golang.org/grpc"
pd "demo/myproto" //導入proto
"context"
)
type server struct {}
func (this *server) SayHello(ctx context.Context, in *pd.HelloReq) (out *pd.HelloRsp,err error) {
return &pd.HelloRsp{Msg:"hello"}, nil
}
func (this *server) SayName(ctx context.Context, in *pd.NameReq) (out *pd.NameRsp,err error){
return &pd.NameRsp{Msg:in.Name + "it is name"}, nil
}
func main() {
ln, err := net.Listen("tcp", ":10088")
if err != nil {
fmt.Println("network error", err)
}
//創建grpc服務
srv := grpc.NewServer()
//註冊服務
pd.RegisterHelloServerServer(srv, &server{})
err = srv.Serve(ln)
if err != nil {
fmt.Println("Serve error", err)
}
}
- 編寫客戶端client.go
package main
import (
"fmt"
"google.golang.org/grpc"
pd "demo/myproto" //導入proto
"context"
)
func main() {
//客戶端連接服務端
conn, err := grpc.Dial("127.0.0.1:10088", grpc.WithInsecure())
if err != nil {
fmt.Println("network error", err)
}
//網絡延遲關閉
defer conn.Close()
//獲得grpc句柄
c := pd.NewHelloServerClient(conn)
//通過句柄進行調用服務端函數SayHello
re1, err := c.SayHello(context.Background(),&pd.HelloReq{Name:"zhangsan"})
if err != nil {
fmt.Println("calling SayHello() error", err)
}
fmt.Println(re1.Msg)
//通過句柄進行調用服務端函數SayName
re2, err := c.SayName(context.Background(),&pd.NameReq{Name:"zhangsan"})
if err != nil {
fmt.Println("calling SayName() error", err)
}
fmt.Println(re2.Msg)
}
執行代碼:
go run server.go
go run client.go
gRPC四種通信方式
gRPC 允許你定義四類服務方法:
- 簡單RPC(Simple RPC):即客戶端發送一個請求給服務端,從服務端獲取一個應答,就像一次普通的函數調用。
rpc SayHello(HelloRequest) returns (HelloResponse){
}
- 服務端流式RPC(Server-side streaming RPC):一個請求對象,服務端可以傳回多個結果對象。即客戶端發送一個請求給服務端,可獲取一個數據流用來讀取一系列消息。客戶端從返回的數據流裏一直讀取直到沒有更多消息爲止。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
- 客戶端流式RPC(Client-side streaming RPC):客戶端傳入多個請求對象,服務端返回一個響應結果。即客戶端用提供的一個數據流寫入併發送一系列消息給服務端。一旦客戶端完成消息寫入,就等待服務端讀取這些消息並返回應答。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
- 雙向流式RPC(Bidirectional streaming RPC):結合客戶端流式rpc和服務端流式rpc,可以傳入多個對象,返回多個響應對象。即兩邊都可以分別通過一個讀寫數據流來發送一系列消息。這兩個數據流操作是相互獨立的,所以客戶端和服務端能按其希望的任意順序讀寫,例如:服務端可以在寫應答前等待所有的客戶端消息,或者它可以先讀一個消息再寫一個消息,或者是讀寫相結合的其他方式。每個數據流裏消息的順序會被保持。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}
protobuf簡介
protocol buffers是google推出的一種數據序列化格式,簡稱protobuf。
優點:
- 支持多種編程語言
- 序列化數據體積小
- 反序列化速度快
- 序列化和反序列化代碼自動生成
缺點:
- 可讀性差,缺乏自描述性
下圖(左JSON,右Protobuf)是同樣的一段數據,用json和protobuf分別描述(僅表示描述方式,並不是最終生成的序列化數據)。可以看出,protobuf是把json中的key去掉了,用數字代替key,從而實現了減小了序列化後的數據大小。而protobuf反序列化過程中,無需做字符串解析,所以速度也很快,綜合性能優於json很多。
protobuf使用,主要分爲以下步驟:
-
定義消息格式,編寫.proto文件
-
選擇合適的protobuf實現框架,對.proto文件進行編譯,生成對應的源代碼文件
-
在代碼中調用生成的源代碼文件,完成序列化和反序列化功能
消息格式:
protobuf數據是連續的key-value組成,每個key-value對錶示一個字段,value可以是基礎類型,也可以是一個子消息。
其中,key表示了該字段數據類型,字段id信息,而value則是該字段的原始數據。若字段類型是string/bytes/子message(長度不固定),則在key和value之間還有一個值表示該字段的數據長度,如下圖所示:
key值的計算方式爲:key=(id<<3)|type,其中id是在消息定義時的字段id,而type表示數據類型,取值範圍0-5,如下表所示:
例:假如proto文件定義如下,其中消息取值爲code=10,msg="abc",嘗試計算序列化後的消息數據。
message Response{
required int32 code = 1;
required string msg = 2;
}
a. 消息的最終結構爲key1-value1-key2-length2-value2;
b. 其中,key1=(id<<3)|type = (1<<3)|0=0x08,value1=0x0a,key2=(id<<3)|type=(2<<3)|2=0x12,length2=0x03,value2=0x616263;
c. 因此,最終的消息數據爲:0x080a1203616263.
數據壓縮:
protobuf引入了varint編碼和zigzag編碼,解決數值表示過程中的冗餘問題。