RPC框架實現 - 通信協議篇


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 0010010 1100  000 0010

Varints字節序使用little-endian,以上數字用big-endian並轉換成10進制有:

000 0010  010 1100000 0010 ++ 010 1100100101100256 + 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 0001000 0001  ++  001 0110 (丟棄最高位的bit並轉為big-endian)
       → 100101102 + 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

                  Protocol Buffers Encoding

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM