RPC 通信
對於單獨部署,獨立運行的微服務實例而言,在業務需要時,需要與其他服務時行通信,這種通信方式是進程之間的通訊方式(簡稱IPC)。
IPC有兩種實現方式,分別為:同步過程調用、異步消息調用。在同步過程調用的具體實現中,有一種實現方式為RPC通信方式,遠程過程調用。(英語:Remote Procedure Call,縮寫為 RPC)。
遠程過程調用(RPC)是一個計算機通信協議。該協議允許運行於一台計算機的程序調用另一台計算機的子程序,而程序員無需額外地為這個交互作用編程。如果涉及的軟件采用面向對象編程,那么遠程過程調用亦可稱作遠程調用或遠程方法調用,例:Java RMI。簡單地說就是能使應用像調用本地方法一樣的調用遠程的過程或服務。很顯然,這是一種client-server的交互形式,調用者(caller)是client,執行者(executor)是server。典型的實現方式就是request-response通訊機制。
RPC 實現步驟
一個正常的RPC過程可以分為以下幾個步驟:
1.client調用client stub,這是一次本地過程調用;
2.client stub將參數打包成一個消息,然后發送這個消息。打包過程也叫做marshalling;
3.client所在的系統將消息發送給server;
4.server的系統將收到的包傳給server stub;
5.server stub解包得到參數,解包也被稱作unmarshalling;
6.server stub調用服務過程。返回結果按照相反的步驟傳給client.。
在上述的步驟實現遠程接口調用時,所需要執行的函數是存在於遠程機器中,即函數是在另外一個進程中執行的。
因此,就帶來幾個新問題:
1.Call ID映射。遠端進程中間可以包含定義的多個函數,本地客戶端該如何告知遠端進程程序調用特定的某個函數呢?因此,在RPC調用過程中,所有的函數都需要有一個自己的ID。開發者在客戶端(調用端)和服務端(被調用端)分別維護一個{函數< - >Call ID}的對應表。兩者的表不一定完全相同,但是相同的函數對應的Call ID必須相同。當客戶端需要進行遠程調用時,調用者通過映射表查詢想要的函數的名稱,找到對應的Call ID,然后傳遞給服務端,服務也通過查表,來確定客戶端所需要調用的函數,然后執行相應函數的代碼。
2.序列化與反序列化。客戶端如何把參數傳遞給遠程調用的函數呢?在本地調用中,我們只需要把參數壓到棧里,然后讓函數自己去棧里讀就行。但是在遠程過程調用時,客戶端跟服務端是不同的進程,不能通過內存來傳遞參數。甚至有時候客戶端和服務端使用的都不是同一種語言。這時候就需要把參數先轉成一個字節流,傳給服務端后,再把字節流轉成自己能讀取的格式。這個過程叫序列化和反序列化。同理,從服務端返回的值也需要序列化反序列化的過程。
3.網絡傳輸。遠程調用往往用在網絡上,客戶端和服務端是通過網絡連接的。所有的數據都需要通過網絡傳輸,因此就需要有一個網絡傳輸層。網絡傳輸層需要把Call ID和序列化后的參數字節流傳遞給服務端,然后在把序列化后的調用結果傳回給客戶端,完成這種數據傳遞功能被稱為傳輸層。大部分的網絡傳輸層都使用TCP協議,屬於長連接。
在上述步驟實現中,可以看到其中有對傳遞的數據進行序列化和反序列化的操作,這就是:Protobuf。
Protobuf簡介
Google Protocol Buffer( 簡稱 Protobuf)是Google公司內部的混合語言數據標准,他們主要用於RPC系統和持續數據存儲系統。
Protobuf應用場景
Protocol Buffers 是一種輕便高效的結構化數據存儲格式,可以用於結構化數據串行化,或者說序列化。它很適合做數據存儲或RPC數據交換格式。可用於通訊協議、數據存儲等領域的語言無關、平台無關、可擴展的序列化結構數據格式。
簡單來說,Protobuf的功能類似於XML,即負責把某種數據結構的信息,以某種格式保存起來。主要用於數據存儲、傳輸協議等使用場景。
為什么已經有了XLM,JSON等已經很普遍的數據傳輸方式,還要設計出Protobuf這樣一種新的數據協議呢?
Protobuf 優點
性能好/效率高
時間維度:采用XML格式對數據進行序列化時,時間消耗上性能尚可;對於使用XML格式對數據進行反序列化時的時間花費上,耗時長,性能差。
空間維度:XML格式為了保持較好的可讀性,引入了一些冗余的文本信息。所以在使用XML格式進行存儲數據時,也會消耗空間。
整體而言,Protobuf以高效的二進制方式存儲,比XML小3到10倍,快20到100倍。
代碼生成機制
代碼生成機制的含義
在Go語言中,可以通過定義結構體封裝描述一個對象,並構造一個新的結構體對象。比如定義Person結構體,並存放於Person.go文件:
type Person struct{
Name string
Age int
Sex int
}
在分布式系統中,因為程序代碼時分開部署,比如分別為A、B。A系統在調用B系統時,無法直接采用代碼的方式進行調用,因為A系統中不存在B系統中的代碼。因此,A系統只負責將調用和通信的數據以二進制數據包的形式傳遞給B系統,由B系統根據獲取到的數據包,自己構建出對應的數據對象,生成數據對象定義代碼文件。這種利用編譯器,根據數據文件自動生成結構體定義和相關方法的文件的機制被稱作代碼生成機制。
代碼生成機制的優點
首先,代碼生成機制能夠極大解放開發者編寫數據協議解析過程的時間,提高工作效率;其次,易於開發者維護和迭代,當需求發生變更時,開發者只需要修改對應的數據傳輸文件內容即可完成所有的修改。
支持“向后兼容”和“向前兼容”
向后兼容:在軟件開發迭代和升級過程中,"后"可以理解為新版本,越新的版本越靠后;而“前”意味着早起的版本或者先前的版本。向“后”兼容即是說當系統升級迭代以后,仍然可以處理老版本的數據業務邏輯。
向前兼容:向前兼容即是系統代碼未升級,但是接受到了新的數據,此時老版本生成的系統代碼可以處理接收到的新類型的數據。
支持前后兼容是非常重要的一個特點,在龐大的系統開發中,往往不可能統一完成所有模塊的升級,為了保證系統功能正常不受影響,應最大限度保證通訊協議的向前兼容和向后兼容。
支持多種編程語言
Protobuf不僅僅Google開源的一個數據協議,還有很多種語言的開源項目實現。在Google官方發布的Protobuf的源代碼中包含了C++、Java、Python三種語言。
Protobuf 缺點
可讀性較差
為了提高性能,Protobuf采用了二進制格式進行編碼。二進制格式編碼對於開發者來說,是沒辦法閱讀的。在進行程序調試時,比較困難。
缺乏自描述
諸如XML語言是一種自描述的標記語言,即字段標記的同時就表達了內容對應的含義。而Protobuf協議不是自描述的,Protobuf是通過二進制格式進行數據傳輸,開發者面對二進制格式的Protobuf,沒有辦法知道所對應的真實的數據結構,因此在使用Protobuf協議傳輸時,必須配備對應的proto配置文件。
Protobuf 協議語法
Protobuf 協議的格式
Protobuf協議規定:使用該協議進行數據序列化和反序列化操作時,首先定義傳輸數據的格式,並命名為以".proto"為擴展名的消息定義文件。
message 定義一個消息
先來看一個非常簡單的例子。假設想定義一個“訂單”的消息格式,每一個“訂單"都含有一個訂單號ID、訂單金額Num、訂單時間TimeStamp字段。可以采用如下的方式來定義消息類型的.proto文件:
message Order{ required string order_id = 1; required int64 num = 2; optional int32 timestamp = 3; }
Order消息格式有3個字段,在消息中承載的數據分別對應每一個字段。其中每個字段都有一個名字和一種類型。
- 指定字段類型:在proto協議中,字段的類型包括字符串(string)、整形(int32、int64…)、枚舉(enum)等數據類型
- 分配標識符:在消息字段中,每個字段都有唯一的一個標識符。最小的標識號可以從1開始,最大到536870911。不可以使用其中的[19000-19999]的標識號, Protobuf協議實現中對這些進行了預留。如果非要在.proto文件中使用這些預留標識號,編譯時就會報警。
- 指定字段規則:字段的修飾符包含三種類型,分別是
-
- required:一個格式良好的消息一定要含有1個這種字段。表示該值是必須要設置的;
- optional:消息格式中該字段可以有0個或1個值(不超過1個)。
- repeated:在一個格式良好的消息中,這種字段可以重復任意多次(包括0次)。重復的值的順序會被保留。表示該值可以重復,相當於Go中的slice。
【注意:】使用required弊多於利;在實際開發中更應該使用optional和repeated而不是required。
- 添加更多消息類型
在同一個.proto文件中,可以定義多個消息類型。多個消息類型分開定義即可。
使用 Protobuf 的步驟
1、創建擴展名為.proto的文件,並編寫代碼。比如創建person.proto文件,內容如下:
syntax = "proto2"; package example; message Person { required string Name = 1; required int32 Age = 2; required string From = 3; }
2、編譯.proto文件,生成Go語言文件。執行如下命令:
protoc --go_out =. test.proto
3、在程序中使用Protobuf
package main import ( "fmt" "ProtocDemo/example" "github.com/golang/protobuf/proto" "os" ) func main() { fmt.Println("Hello World. \n") msg_test := &example.Person{ Name: proto.String("Davie"), Age: proto.Int(18), From: proto.String("China"), } //序列化 msgDataEncoding, err := proto.Marshal(msg_test) if err != nil { panic(err.Error()) return } msgEntity := example.Person{} err = proto.Unmarshal(msgDataEncoding, &msgEntity) if err != nil { fmt.Println(err.Error()) os.Exit(1) return } fmt.Printf("姓名:%s\n\n", msgEntity.GetName()) fmt.Printf("年齡:%d\n\n", msgEntity.GetAge()) fmt.Printf("國籍:%s\n\n", msgEntity.GetFrom()) }
4、執行程序
Protobuf 協議語法
message:
Protobuf中定義一個數據結構需要用到關鍵字message,這一點和Java的class,Go語言中的struct類似。
標識號:
在消息的定義中,每個字段等號后面都有唯一的標識號,用於在反序列化過程中識別各個字段的,一旦開始使用就不能改變。標識號從整數1開始,依次遞增,每次增加1,標識號的范圍為1~2^29 – 1,其中[19000-19999]為Protobuf協議預留字段,開發者不建議使用該范圍的標識號;一旦使用,在編譯時Protoc編譯器會報出警告。
字段規則:
字段規則有三種:
- 1、required:該規則規定,消息體中該字段的值是必須要設置的。
- 2、optional:消息體中該規則的字段的值可以存在,也可以為空,optional的字段可以根據defalut設置默認值。
- repeated:消息體中該規則字段可以存在多個(包括0個),該規則對應java的數組或者go語言的slice。
數據類型:
常見的數據類型與protoc協議中的數據類型映射如下:
枚舉類型:
proto協議支持使用枚舉類型,和正常的編程語言一樣,枚舉類型可以使用enum關鍵字定義在.proto文件中:
enum Age{ male=1; female=2; }
字段默認值:
.proto文件支持在進行message定義時設置字段的默認值,可以通過default進行設置,如下所示:
message Address { required sint32 id = 1 [default = 1]; required string name = 2 [default = '北京']; optional string pinyin = 3 [default = 'beijing']; required string address = 4; required bool flag = 5 [default = true]; }
導入:
如果需要引用的message是寫在別的.proto文件中,可以通過import "xxx.proto"來進行引入:
嵌套:
message與message之間可以嵌套定義,比如如下形式:
syntax = "proto2"; package example; message Person { required string Name = 1; required int32 Age = 2; required string From = 3; optional Address Addr = 4; message Address { required sint32 id = 1; required string name = 2; optional string pinyin = 3; required string address = 4; } }
message更新規則:
message定義以后如果需要進行修改,為了保證之前的序列化和反序列化能夠兼容新的message,message的修改需要滿足以下規則:
不可以修改已存在域中的標識號。
所有新增添的域必須是 optional 或者 repeated。
非required域可以被刪除。但是這些被刪除域的標識號不可以再次被使用。
非required域可以被轉化,轉化時可能發生擴展或者截斷,此時標識號和名稱都是不變的。
sint32和sint64是相互兼容的。
fixed32兼容sfixed32。 fixed64兼容sfixed64。
optional兼容repeated。發送端發送repeated域,用戶使用optional域讀取,將會讀取repeated域的最后一個元素。
Protobuf序列化原理
之前已經做過描述,Protobuf的message中有很多字段,每個字段的格式為:修飾符 字段類型 字段名 = 域號;
Varint是一種緊湊的表示數字的方法。它用一個或多個字節來表示一個數字,值越小的數字使用越少的字節數。這能減少用來表示數字的字節數。
Varint中的每個byte的最高位bit有特殊的含義,如果該位為1,表示后續的byte也是該數字的一部分,如果該位為0,則結束。其他的7個bit都用來表示數字。因此小於128的數字都可以用一個byte表示。大於128的數字,比如300,會用兩個字節來表示:1010 1100 0000 0010。下圖演示了 Google Protocol Buffer 如何解析兩個bytes。注意到最終計算前將兩個byte的位置相互交換過一次,這是因為 Google Protocol Buffer 字節序采用little-endian的方式。
在序列化時,Protobuf按照TLV的格式序列化每一個字段,T即Tag,也叫Key;V是該字段對應的值value;L是Value的長度,如果一個字段是整形,這個L部分會省略。
序列化后的Value是按原樣保存到字符串或者文件中,Key按照一定的轉換條件保存起來,序列化后的結果就是 KeyValueKeyValue…依次類推的樣式,示意圖如下所示: