微服務設計筆記——幾種遠程過程調用方法
微服務設計中提到服務間常見的PRC 有如下幾種:SOAP、Thrift、Protocol Buffers. 為了搞清楚幾種RPC背後的機理以及應用場景,特意研究了一番:
SOAP(Simple Object Access Protocol)
簡單對象訪問協議是在分散或分布式的環境中交換信息的簡單的協議,是一個基於XML的協議,它包括四個部分:
SOAP封裝(envelop),封裝定義了一個描述消息中的內容是什麽,是誰發送的,誰應當接受並處理它以及如何處理它們的框架;
SOAP編碼規則(encoding rules),用於表示應用程序需要使用的數據類型的實例;
SOAP RPC表示(RPC representation),表示遠程過程調用和應答的協定;
SOAP綁定(binding),使用底層協議交
雖然這四個部分都作為SOAP的一部分,作為一個整體定義的,但他們在功能上是相交的、彼此獨立的。特別的,封裝和編碼規則是被定義在不同的XML命名空間(namespace)中,這樣使得定義更加簡單。
SOAP的兩個主要設計目標是簡單性和可擴展性。
一個 SOAP 實例
在下面的例子中,一個 GetStockPrice 請求被發送到了服務器。此請求有一個 StockName 參數,而在響應中則會返回一個 Price 參數。此功能的命名空間被定義在此地址中: "http://www.example.org/stock"
SOAP 請求:
1 POST /InStock HTTP/1.12 Host: www.example.org 3 Content-Type: application/soap+xml; charset=utf-8 4 Content-Length: nnn 5 6 <?xml version="1.0"?> 7 <soap:Envelope 8 xmlns:soap="http://www.w3.org/2001/12/soap-envelope" 9 soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding"> 10 11 <soap:Body xmlns:m="http://www.example.org/stock"> 12 <m:GetStockPrice> 13 <m:StockName>IBM</m:StockName> 14 </m:GetStockPrice> 15 </soap:Body> 16 17 </soap:Envelope>
SOAP 響應:
1 HTTP/1.1 200 OK 2 Content-Type: application/soap+xml; charset=utf-8 3 Content-Length: nnn 4 5 <?xml version="1.0"?> 6 <soap:Envelope 7 xmlns:soap="http://www.w3.org/2001/12/soap-envelope" 8 soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding"> 9 10 <soap:Body xmlns:m="http://www.example.org/stock"> 11 <m:GetStockPriceResponse> 12 <m:Price>34.5</m:Price> 13 </m:GetStockPriceResponse> 14 </soap:Body> 15 16 </soap:Envelope>
SOAP簡單的理解,就是這樣的一個開放協議SOAP=RPC+HTTP+XML:采用HTTP作為底層通訊協議;RPC作為一致性的調用途徑,XML作為數據傳送的格式,允許服務提供者和服務客戶經過防火墻在INTERNET進行通訊交互。RPC的描敘可能不大準確,因為SOAP一開始構思就是要實現平臺與環境的無關性和獨立性,每一個通過網絡的遠程調用都可以通過SOAP封裝起來,包括DCE(Distributed Computing Environment ) RPC CALLS,COM/DCOM CALLS, CORBA CALLS, JAVA CALLS,etc。
SOAP 使用 HTTP 傳送 XML,盡管HTTP 不是有效率的通訊協議,而且 XML 還需要額外的文件解析(parse),兩者使得交易的速度大大低於其它方案。但是XML 是一個開放、健全、有語義的訊息機制,而 HTTP 是一個廣泛又能避免許多關於防火墻的問題,從而使SOAP得到了廣泛的應用。但是如果效率對你來說很重要,那麽你應該多考慮其它的方式,而不要用 SOAP。
為了更好的理解SOAP,HTTP,XML如何工作的,不妨先考慮一下COM/DCOM的運行機制,DCOM處理網絡協議的低層次的細節問題,如PROXY/STUB間的通訊,生命周期的管理,對象的標識。在客戶端與服務器端進行交互的時候,DCOM采用NDR(Network Data Representation)作為數據表示,它是低層次的與平臺無關的數據表現形式。
DCOM是有效的,靈活的,但也是很復雜的。而SOAP的一個主要優點就在於它的簡單性,SOAP使用HTTP作為網絡通訊協議,接受和傳送數據參數時采用XML作為數據格式,從而代替了DCOM中的NDR格式,SOAP和 DCOM執行過程是類似的,如下圖,但是用XML取代 NDR作為編碼表現形式,提供了更高層次上的抽象,與平臺和環境無關。
客戶端發送請求時,不管客戶端是什麽平臺的,首先把請求轉換成XML格式,SOAP網關可自動執行這個轉換。為了保證傳送時參數,方法名,返回值的唯一性,SOAP協議使用了一個私有標記表,從而服務端的SOAP網關可以正確的解析,這有點類似於COM/DCOM中的樁(STUB)。轉化成XML格式後,SOAP終端名(遠程調用方法名)及其他的一些協議標識信息被封裝成HTTP請求,然後發送給服務器。如果應用程序要求,服務器返回一個HTTP應答信息給客戶端。與通常對HTML頁面的HTTP GET請求不同的是,此請求設置了一些HTTP HEADER,標識著一個SOAP服務激發,和HTTP包一起傳送。
SOAP是一個協議,與編程語言無關。實際上,許多語言已經開始支持SOAP,如:java,c/c++,vb,c#,perl,php.下面列出了在Java/C++/Perl/ADA/Python環境下SOAP的執行工具:
Java: Apache SOAP, DevelopMentor‘s implementation, IdooXoap from ZVON
- Python: PythonWare (client side only)
- C++: IdooXoap from ZVON
- Perl: SOAP::Lite
Protocol Buffers
Google Protocol Buffer( 簡稱 Protobuf) 是 Google 公司內部的混合語言數據標準,用於 RPC 系統和持續數據存儲系統。Protocol Buffers 是一種輕便高效的結構化數據存儲格式,可以用於結構化數據串行化,或者說序列化。它很適合做數據存儲或 RPC 數據交換格式。可用
於通訊協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。目前提供了 C++、Java、Python 三種語言的 API。
一個簡單的例子
安裝 Google Protocol Buffer
在網站 http://code.google.com/p/protobuf/downloads/list上可以下載 Protobuf 的源代碼。然後解壓編譯安裝便可以使用它了。
安裝步驟如下所示:
1 tar -xzf protobuf-2.1.0.tar.gz 2 cd protobuf-2.1.0 3 ./configure --prefix=$INSTALL_DIR 4 make 5 make check 6 make install
使用 Protobuf 和 C++ 開發一個十分簡單的例子程序。該程序由兩部分組成。第一部分被稱為 Writer,第二部分叫做 Reader。
Writer 負責將一些結構化的數據寫入一個磁盤文件,Reader 則負責從該磁盤文件中讀取結構化數據並打印到屏幕上。
準備用於演示的結構化數據是 HelloWorld,它包含兩個基本數據:
- ID,為一個整數類型的數據
- Str,這是一個字符串
書寫 .proto 文件
首先我們需要編寫一個 proto 文件,定義我們程序中需要處理的結構化數據,在 protobuf 的術語中,結構化數據被稱為 Message。proto 文件非常類似 java 或者 C 語言的數據定義。代碼清單 1 顯示了例子應用中的 proto 文件內容。
1 清單 1. proto 文件 2 package lm; 3 message helloworld 4 { 5 required int32 id = 1; // ID 6 required string str = 2; // str 7 optional int32 opt = 3; //optional field 8 }
一個比較好的習慣是認真對待 proto 文件的文件名。比如將命名規則定於如下:
packageName.MessageName.proto
在上例中,package 名字叫做 lm,定義了一個消息 helloworld,該消息有三個成員,類型為 int32 的 id,另一個為類型為 string 的成員 str。opt 是一個可選的成員,即消息中可以不包含該成員。
編譯 .proto 文件
寫好 proto 文件之後就可以用 Protobuf 編譯器將該文件編譯成目標語言了。本例中我們將使用 C++。
假設您的 proto 文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一個目錄下,則可以使用如下命令:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
命令將生成兩個文件:
lm.helloworld.pb.h , 定義了 C++ 類的頭文件
lm.helloworld.pb.cc , C++ 類的實現文件
在生成的頭文件中,定義了一個 C++ 類 helloworld,後面的 Writer 和 Reader 將使用這個類來對消息進行操作。諸如對消息的成員進行賦值,將消息序列化等等都有相應的方法。
編寫 writer 和 Reader
如前所述,Writer 將把一個結構化數據寫入磁盤,以便其他人來讀取。假如我們不使用 Protobuf,其實也有許多的選擇。一個可能的方法是將數據轉換為字符串,然後將字符串寫入磁盤。轉換為字符串的方法可以使用 sprintf(),這非常簡單。數字 123 可以變成字符串”123”。
這樣做似乎沒有什麽不妥,但是仔細考慮一下就會發現,這樣的做法對寫 Reader 的那個人的要求比較高,Reader 的作者必須了 Writer 的細節。比如”123”可以是單個數字 123,但也可以是三個數字 1,2 和 3,等等。這麽說來,我們還必須讓 Writer 定義一種分隔符一樣的字符,以便 Reader 可以正確讀取。但分隔符也許還會引起其他的什麽問題。最後我們發現一個簡單的 Helloworld 也需要寫許多處理消息格式的代碼。
如果使用 Protobuf,那麽這些細節就可以不需要應用程序來考慮了。
使用 Protobuf,Writer 的工作很簡單,需要處理的結構化數據由 .proto 文件描述,經過上一節中的編譯過程後,該數據化結構對應了一個 C++ 的類,並定義在 lm.helloworld.pb.h 中。對於本例,類名為 lm::helloworld。
Writer 需要 include 該頭文件,然後便可以使用這個類了。
現在,在 Writer 代碼中,將要存入磁盤的結構化數據由一個 lm::helloworld 類的對象表示,它提供了一系列的 get/set 函數用來修改和讀取結構化數據中的數據成員,或者叫 field。
當我們需要將該結構化數據保存到磁盤上時,類 lm::helloworld 已經提供相應的方法來把一個復雜的數據變成一個字節序列,我們可以將這個字節序列寫入磁盤。
對於想要讀取這個數據的程序來說,也只需要使用類 lm::helloworld 的相應反序列化方法來將這個字節序列重新轉換會結構化數據。這同我們開始時那個“123”的想法類似,不過 Protobuf 想的遠遠比我們那個粗糙的字符串轉換要全面,因此,我們不如放心將這類事情交給 Protobuf 吧。
程序清單 2 演示了 Writer 的主要代碼,您一定會覺得很簡單吧?
清單 2. Writer 的主要代碼 #include "lm.helloworld.pb.h" … int main(void) { lm::helloworld msg1; msg1.set_id(101); msg1.set_str(“hello”); // Write the new address book back to disk. fstream output("./log", ios::out | ios::trunc | ios::binary); if (!msg1.SerializeToOstream(&output)) { cerr << "Failed to write msg." << endl; return -1; } return 0; }
Msg1 是一個 helloworld 類的對象,set_id() 用來設置 id 的值。SerializeToOstream 將對象序列化後寫入一個 fstream 流。
代碼清單 3 列出了 reader 的主要代碼。
清單 3. Reader #include "lm.helloworld.pb.h" … void ListMsg(const lm::helloworld & msg) { cout << msg.id() << endl; cout << msg.str() << endl; } int main(int argc, char* argv[]) { lm::helloworld msg1; { fstream input("./log", ios::in | ios::binary); if (!msg1.ParseFromIstream(&input)) { cerr << "Failed to parse address book." << endl; return -1; } } ListMsg(msg1); … }
同樣,Reader 聲明類 helloworld 的對象 msg1,然後利用 ParseFromIstream 從一個 fstream 流中讀取信息並反序列化。此後,ListMsg 中采用 get 方法讀取消息的內部信息,並進行打印輸出操作。
運行結果
運行 Writer 和 Reader 的結果如下:
>writer >reader 101 Hello
這個例子本身並無意義,但只要您稍加修改就可以將它變成更加有用的程序。比如將磁盤替換為網絡 socket,那麽就可以實現基於網絡的數據交換任務。而存儲和交換正是 Protobuf 最有效的應用領域。
和其他類似技術的比較
看完這個簡單的例子之後,希望您已經能理解 Protobuf 能做什麽了,那麽您可能會說,世上還有很多其他的類似技術啊,比如 XML,JSON,Thrift 等等。和他們相比,Protobuf 有什麽不同呢?
簡單說來 Protobuf 的主要優點就是:簡單,快。
Protobuf 的優點
Protobuf 有如 XML,不過它更小、更快、也更簡單。你可以定義自己的數據結構,然後使用代碼生成器生成的代碼來讀寫這個數據結構。你甚至可以在無需重新部署程序的情況下更新數據結構。只需使用 Protobuf 對數據結構進行一次描述,即可利用各種不同語言或從各種不同數據流中對你的結構化數據輕松讀寫。
它有一個非常棒的特性,即“向後”兼容性好,人們不必破壞已部署的、依靠“老”數據格式的程序就可以對數據結構進行升級。這樣您的程序就可以不必擔心因為消息結構的改變而造成的大規模的代碼重構或者遷移的問題。因為添加新的消息中的 field 並不會引起已經發布的程序的任何改變。
Protobuf 語義更清晰,無需類似 XML 解析器的東西(因為 Protobuf 編譯器會將 .proto 文件編譯生成對應的數據訪問類以對 Protobuf 數據進行序列化、反序列化操作)。
使用 Protobuf 無需學習復雜的文檔對象模型,Protobuf 的編程模式比較友好,簡單易學,同時它擁有良好的文檔和示例,對於喜歡簡單事物的人們而言,Protobuf 比其他的技術更加有吸引力。
Protobuf 的不足
Protbuf 與 XML 相比也有不足之處。它功能簡單,無法用來表示復雜的概念。
XML 已經成為多種行業標準的編寫工具,Protobuf 只是 Google 公司內部使用的工具,在通用性上還差很多。
由於文本並不適合用來描述數據結構,所以 Protobuf 也不適合用來對基於文本的標記文檔(如 HTML)建模。另外,由於 XML 具有某種程度上的自解釋性,它可以被人直接讀取編輯,在這一點上 Protobuf 不行,它以二進制的方式存儲,除非你有 .proto 定義,否則你沒法直接讀出 Protobuf 的任何內容
高級應用話題
更復雜的 Message
到這裏為止,我們只給出了一個簡單的沒有任何用處的例子。在實際應用中,人們往往需要定義更加復雜的 Message。我們用“復雜”這個詞,不僅僅是指從個數上說有更多的 fields 或者更多類型的 fields,而是指更加復雜的數據結構:
嵌套 Message
嵌套是一個神奇的概念,一旦擁有嵌套能力,消息的表達能力就會非常強大。
代碼清單 4 給出一個嵌套 Message 的例子。
清單 4. 嵌套 Message 的例子 message Person { required string name = 1; required int32 id = 2; // Unique ID number for this person. 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 Person 中,定義了嵌套消息 PhoneNumber,並用來定義 Person 消息中的 phone 域。這使得人們可以定義更加復雜的數據結構。
4.1.2 Import Message
在一個 .proto 文件中,還可以用 Import 關鍵字引入在其他 .proto 文件中定義的消息,這可以稱做 Import Message,或者 Dependency Message。
比如下例:
清單 5. 代碼
import common.header; message youMsg{ required common.info_header header = 1; required string youPrivateData = 2; }
其中 ,
common.info_header定義在
common.header包內。
Import Message 的用處主要在於提供了方便的代碼管理機制,類似 C 語言中的頭文件。您可以將一些公用的 Message 定義在一個 package 中,然後在別的 .proto 文件中引入該 package,進而使用其中的消息定義。
Google Protocol Buffer 可以很好地支持嵌套 Message 和引入 Message,從而讓定義復雜的數據結構的工作變得非常輕松愉快。
動態編譯
一般情況下,使用 Protobuf 的人們都會先寫好 .proto 文件,再用 Protobuf 編譯器生成目標語言所需要的源代碼文件。將這些生成的代碼和應用程序一起編譯。
可是在某且情況下,人們無法預先知道 .proto 文件,他們需要動態處理一些未知的 .proto 文件。比如一個通用的消息轉發中間件,它不可能預知需要處理怎樣的消息。這需要動態編譯 .proto 文件,並使用其中的 Message。
Protobuf 提供了 google::protobuf::compiler 包來完成動態編譯的功能。主要的類叫做 importer,定義在 importer.h 中。使用 Importer 非常簡單,下圖展示了與 Import 和其它幾個重要的類的關系。
圖 2. Importer 類
Import 類對象中包含三個主要的對象,分別為處理錯誤的 MultiFileErrorCollector 類,定義 .proto 文件源目錄的 SourceTree 類。
下面還是通過實例說明這些類的關系和使用吧。
對於給定的 proto 文件,比如 lm.helloworld.proto,在程序中動態編譯它只需要很少的一些代碼。如代碼清單 6 所示。
清單 6. 代碼 google::protobuf::compiler::MultiFileErrorCollector errorCollector; google::protobuf::compiler::DiskSourceTree sourceTree; google::protobuf::compiler::Importer importer(&sourceTree, &errorCollector); sourceTree.MapPath("", protosrc); importer.import(“lm.helloworld.proto”);
首先構造一個 importer 對象。構造函數需要兩個入口參數,一個是 source Tree 對象,該對象指定了存放 .proto 文件的源目錄。第二個參數是一個 error collector 對象,該對象有一個 AddError 方法,用來處理解析 .proto 文件時遇到的語法錯誤。
之後,需要動態編譯一個 .proto 文件時,只需調用 importer 對象的 import 方法。非常簡單。
那麽我們如何使用動態編譯後的 Message 呢?我們需要首先了解幾個其他的類
Package google::protobuf::compiler 中提供了以下幾個類,用來表示一個 .proto 文件中定義的 message,以及 Message 中的 field,如圖所
圖 3. 各個 Compiler 類之間的關系
類 FileDescriptor 表示一個編譯後的 .proto 文件;類 Descriptor 對應該文件中的一個 Message;類 FieldDescriptor 描述一個 Message 中的一個具體 Field。
比如編譯完 lm.helloworld.proto 之後,可以通過如下代碼得到 lm.helloworld.id 的定義:
清單 7. 得到 lm.helloworld.id 的定義的代碼 const protobuf::Descriptor *desc = importer_.pool()->FindMessageTypeByName(“lm.helloworld”); const protobuf::FieldDescriptor* field = desc->pool()->FindFileByName (“id”);
通過 Descriptor,FieldDescriptor 的各種方法和屬性,應用程序可以獲得各種關於 Message 定義的信息。比如通過 field->name() 得到 field 的名字。這樣,您就可以使用一個動態定義的消息了。
微服務設計筆記——幾種遠程過程調用方法