RPC(Remote Procedure Call,遠端過程呼叫)框架是分散式服務的基石,實現RPC框架需要考慮方方面面。其對業務隱藏了底層通訊過程(TCP/UDP、打包/解包、序列化/反序列化),使上層專注於功能實現;框架層面,提供各類可選架構(多程式/多執行緒/協程);應對裝置故障(高負載/當機)、網路故障(擁塞/網路分化),提供相應容災措施。
RPC節點間為了協同工作、實現資訊交換,需要協商一定的規則和約定,例如位元組序、壓縮或加密演算法、各欄位型別。通訊協議的應用隨處可見,例如我們對可選資訊或欄位經常使用TLV進行編碼,HTTP、FTP等協議基於可讀文字的 "Field: Value" 格式,各種系統也經常使用json、XML格式完成相互間通訊。
不同的通訊協議適用於不同的應用場景,比如內部系統的互動我們選擇json,一來可讀性較好,二來各種語言都提供瞭解析json的庫、方便編碼。Google Protocol Buffers是生成環境中常用的通訊協議,除了可以設定Client/Server間通訊格式,Protocol Buffers還對資料進行壓縮,節省傳輸流量、加快傳輸速度。下面我們來了解Google Protocol Buffers。
Protocol Buffers
我們看如何使用Protocol Buffers(以下簡稱PB),首先在.proto檔案中定義資料格式,下面以Person.proto為例:
message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; }
message型別內,可以定義int、string、bool、string等型別的欄位,也可以巢狀定義messages型別。每個欄位可以是required、optional或repeated型別,分別表示必須每次通訊必須填充該欄位、可選或可重複。每個message型別內的每個欄位被賦值唯一的數字值,PB以二進位制格式進行資料傳輸,數字值在二進位制中作為該欄位的標識。關於PB資料格式的更多內容可參考Protocol Buffers Language Guide。
完成資料定義後,接下來可以使用protoc工具解析Person.proto檔案,生成Person類:
protoc --cpp_out=/home/bangerlee/PB ./Person.proto
執行以上命令後,可以看到 /home/bangerlee/PB 目錄下生成了兩個檔案:
person.pb.cc person.pb.h
其中定義了操作(get/set)Person類各個欄位的函式。
有了介面,我們就可以在程式碼中這樣使用Person類,寫入操作如下:
Person person; person.set_name("bangerlee"); person.set_id(1234); person.set_email("bangerlee@gmail.com"); fstream output("myfile", ios::out | ios::binary); person.SerializeToOstream(&output);
讀取操作如下:
fstream input("myfile", ios::in | ios::binary); Person person; person.ParseFromIstream(&input); cout << "Name: " << person.name() << endl; cout << "E-mail: " << person.email() << endl;
以上我們初步瞭解瞭如何使用PB,PB運用了一些編碼規則,使得需要傳輸的資料(二進位制格式)更小,下面我們就來了解PB如何對不同資料型別的編碼規則。
編碼(Encoding)
對整形int、字串型別string等,PB有不同的編碼方式。對整型int,PB使用了Varints編碼方式,Varints編碼的優勢是使用了更少的bytes來表示很小的int型別值。
Varints編碼方式中,每個byte的最高位bit有特殊含義,如果為1,表示後續的byte也是這個數字的一部分;如果為0,則表示結束。剩餘的7個bit用於表示資料。數字300用Varints編碼方式表示為:
1010 1100 0000 0010
由Varints編碼規則,去掉第一個byte的最高位1,去掉第二個byte的最高位0,則有:
1010 1100 0000 0010 → 010 1100 000 0010
Varints位元組序使用little-endian,以上數字用big-endian並轉換成10進位制有:
000 0010 010 1100 → 000 0010 ++ 010 1100 → 100101100 → 256 + 32 + 8 + 4 = 300
以上了解了Varints對int整型的編碼方式,我們再來看PB如何編碼更多資料型別:
PB編碼中,資料以key-value的形式表示,第一個byte即為key。以上表格中不同資料型別對應指定type值,假設message中各欄位的數字標識為tag,則key、type和tag有以下對應關係:
key = tag << 3 | type
即key的最後3個bit用於儲存type,有了這層關係,我們試著演算PB中對int和string的編碼。
假設我們截獲到以下PB資料:
08 96 01
這段資料具體表示什麼?我們用以上對應關係演算一下,首先該資料key是08,二進位制表示即:
0000 1000
最後3個bit表示type,即type為0(Varint格式資料),左移3位得到tag值為1。有了這些資訊,我們可以知道這個資料應該是這樣定義的:
message Test1 { xxx int32 a = 1; }
繼續地,我們用Varint格式來解析 96 01,有以下演算過程:
96 01 = 1001 0110 0000 0001 → 000 0001 ++ 001 0110 (丟棄最高位的bit並轉為big-endian) → 10010110 → 2 + 4 + 16 + 128 = 150
因此我們可以知道這段資料表示150這個數。
又假設我們截獲到以下一段PB編碼:
12 07 74 65 73 74 69 6e 67
同樣套用以上關係,key是12,二進位制表示即:
0001 0010
最後3個bit表示type,即type為2(Length-delimited),左移3位得到tag值為2。有了這些資訊,我們知道這個資料可能是這樣定義的:
message Test2 { xxx string b = 2; }
資料型別具體是string、bytes或其他,這並不影響我們解析這段資料,對於Length-delimited格式資料,第2個byte表示資料長度(Len),對應以上編碼即Len為7,這實質是TLV編碼格式。
後續的7個bytes表示有效的傳輸資料,為UTF-8編碼下的"testing"字串。
小結
以上介紹了通訊協議 - Google Protocol Buffers,瞭解了其基本使用方法和編碼方式。PB支援前向相容,可以在不修改Client/Server程式的情況下修改其中一端的資料格式,在各種RPC框架中經常可以看到它的身影。
Reference: Protocol Buffers Developer Guide