Thrift 入門教程

發表於2016-11-17

【引子】

在英語裡,thrift是個名詞,表示的是“節儉、節約”,給個例句會印象更深些:

Farmers know a lot about value and thrift。(譯:農場主深諳價值與節儉之道。)

然而,這篇文章並非是一篇英語學習教程,而是要和大家介紹計算機技術裡一款著名的通訊框架 – thrift框架。

好,現在有請thrift登場。

【thrift是什麼】

thrift的全名叫做Apache thrift,是一款軟體開發RPC框架,可以很高效地實現跨語言的RPC服務。

如果你還不瞭解RPC是什麼,趕快看看這裡

如果想參觀參觀thrift的官方網站,請點選這裡

【thrift生於何地】

thrift最初生於Facebook,並茁壯成長,在2007年由Facebook正式開源出來,2008年由Apache軟體基金會開始負責專案孵化直至今日。

【還有哪些RPC框架】

protobuf、Avro、MessagePack等,如果你有興趣,可以搜尋一下他們,也有利於你更好的瞭解RPC這個領域的發展情況。

【下載thrift】

目前的最新版本是thrift-0.9.1

【安裝thrift】

首先建議你安裝如下這些軟體包:

然後從官網下載thrift原始碼包,並進行編譯連結:

安裝完成後,你會看到thrift其實包含了三部分:一個bin程式、一坨標頭檔案和若干庫檔案

【為什麼需要thrift】

如果你之前沒有接觸過RPC框架的話,可能理解起來會比較困難。為了照顧這些新使用者的感受,我嘗試著用一種好理解的思路來解釋:

研發工程師小吳接到了一個新任務,給“托福考試成績資料庫”增加一個“成績查詢”的功能,客戶端提供“使用者ID”向伺服器端發起查詢請求,伺服器端接到查詢請求後從資料庫中取回此使用者ID對應的姓名和成績,並返回給客戶端。

就是這樣一個簡單的Client-Server通訊過程,其實就形成了一個典型的RPC場景。伺服器端提供“成績查詢服務”,客戶端會通過約定的方法來查詢成績。

小吳設計的方法呼叫和資料傳輸是這樣的:

thrift example

thrift example

通過上圖可以看到,伺服器端處於監聽狀態(等待請求的到來),客戶端發起一個名為Search的動作,引數是使用者ID,而這個動作的返回是一個結構體struct UserGradeInfo,其中包含了使用者的名字(UserName)和使用者的成績(UserGrade)。

設計做完了,小吳要開始編碼了。如果按照“手工作坊”的思路,小吳至少需要完成如下幾個方面:

(1)“客戶端向伺服器端傳送資料”的程式碼

(2)“客戶端接收伺服器端查詢結果”的程式碼

(3)“伺服器端接收客戶端資料”的程式碼

(4)“伺服器端向客戶端傳送查詢結果”的程式碼

(5)如果客戶端會大批量發起查詢,那可能還需要考慮改成多執行緒模型或非同步模型

(6)而且還有可能因為某種原因,要求客戶端和伺服器端使用不同的語言進行開發

照此思路,小吳至少要3周時間來開發和自測。(時間很長,對吧)

但,自從thrift出現後(更準確的說,是自從RPC開發框架出現後),上述工作量被大大簡化了。我們只要呼叫一個thrift工具就可以自動生成上述的所有程式碼,即便伺服器端和客戶端使用不同的語言,thrift也照樣支援。

至此,我相信你應該大概理解thrift能幫我們做什麼了吧。

【thrift到底怎麼用】

依然拿上面的“成績資料庫”的例子來說,thrift的使用可以被分為四步:

第1步: 明確要互動的資料格式(如上例中的UserGradeInfo)和具體的方法(如上例中的Search),定義出thrift介面描述檔案(英文叫做Inteface Description File);

第2步: 呼叫thrift工具,依據thrift介面檔案,生成RPC程式碼;

第3步: 你的伺服器端程式引用thrift生成的RPC程式碼,並實現其中的Search動作的邏輯,然後啟動監聽,等待客戶端發來請求。

第4步: 客戶端同樣引入並呼叫RPC程式碼來與伺服器端通訊;

(如果你覺得這樣描述太空虛,別急,稍後會有完整的例子)

【thrift介面描述檔案怎麼編寫】

如果你是學院派,那麼我推薦你研究thrift IDL(Interface Definition Language)規範,在這裡。雖然有些晦澀,但你可以從中瞭解到一個介面檔案可以如何來寫。

在編寫介面檔案時,需要對你要傳輸的資料設定資料型別,比如UserName是字串型,UserGrade是整型等。因為thrift是支援眾多開發語言的,所以thrift提供了一套自己的資料型別編寫規範,只有用這套獨立於任何語言的型別規範來編寫介面檔案,thrift才能把它轉換成你指定的那種開發語言的程式碼。

thrift中的型別包括基礎型別、結構、容器、異常、服務等幾個部分。(官網中有專門介紹資料型別的頁面,在這裡

【型別 之 基礎型別】

基礎型別,其實非常簡單和明確:

(1)bool:布林型別(true或false)

(2)byte:8位有符號整數

(3)i16:16位有符號整數

(4)i32:32位有符號整數

(5)i64:64位有符號整數

(6)double:64位浮點數

(7)string:文字字串,使用UTF-8編碼

(有些細心的同學會詢問“為什麼不支援無符號整數型別呢?”,這是因為在很多開發語言中並沒有原生的無符號整型。)

【型別 之 容器】

thrift容器包括了各種語言中最常用的容器,共三種:

(1)list容器:一個元素可重複的有序列表。會被轉換成C++中的vector,Java中的ArrayList,指令碼語言中的陣列等。

(2)set容器:一個元素不可重複的無序集合。會轉換成C++中的set,Java中的HashSet、Python中的Set等。(熟悉PHP的同學可能會問“PHP並不支援set型別,怎麼辦”,在PHP語言中,thrift會將set容器轉換成List。)

(3)map容器:一個含有多個key:value鍵值對的結構。會被轉換成C++中的map,Java中的HashMap,PHP中的關聯陣列,Python/Ruby中的dictionary等。

對於上述三種容器,其元素的型別原則上可以是任何一種thrift型別。但是值得注意的是,map的key型別需要是基礎型別,因為很多開發語言並不支援map的key型別為複雜資料型別。

【型別 之 結構體】

結構體型別,在形式上和C/C++中的結構體型別非常相似,就是一坨型別的組合,比如上文圖中的UserGradeInfo便是一個thrift結構體型別。

thrift介面檔案中的結構體型別,都會被轉換成一個獨立的類(Class)。類的屬性便是結構體中的各個型別,而類的方法便是對這些型別進行處理的相關函式。

我們來看一個結構體定義的例子:

可以看到,結構體中每一個域都有一個正整數識別符號,這個識別符號並不要求連續,但一旦定義,不建議再進行修改。

另外,每個域前都會有required或optional的限定,前者表示是必填域,後者則表示是可選域。域是可以有預設值的,比如上例中的“Anonymous”和0。

(1)如果一個域設定了required,但是在實際構造結構體時又沒有給這個域賦值,那麼thrift會認為這是一個異常。

(2)如果一個域設定為optional且在構造結構體時沒有給這個域賦值,那麼在使用這個結構體時,就會忽略掉這個optional的域。

【型別 之 異常】

除了使用exception來替代struct以外,“異常”這個型別,在語法上和剛才介紹過的結構體的用法是完全一致的。但是從語義上講,exception和struct卻大相徑庭。exception是在遠端呼叫發生異常時用來丟擲異常用的。

【型別 之 服務】

服務的定義,與物件導向技術中定義一個介面很類似,而這些介面其實就是純虛擬函式。thrift編譯工具會根據服務的定義來產生相應的方法和函式。

每個服務,都包括了若干個函式,每個函式包括了若干引數和一個返回值(返回值可以是void)。

(小技巧:返回值為void的函式,你可以在函式名前加上oneway識別符號,將此函式以非同步模式執行,這樣在呼叫此函式後,函式會立即返回。)

對於返回void的函式,thrift仍然會確保函式返回,這樣就表示這個函式已被正確執行,且伺服器端已有返回資訊了。但是如果給void的函式前加上oneway,那麼此函式的返回只能表示資料已經進入傳輸層,並不能表示伺服器端已經收到並返回了資料。

【我們來看一個thrift介面描述檔案的例子吧】

【使用thrift編譯工具】

在我們編寫好thrift介面描述檔案之後,thrift編譯工具就要派上用場了,它的作用就是根據thrift介面描述檔案來生成相應開發語言的RPC程式碼,以便使用者可以在自己的程式中呼叫。

thrift編譯工具的名稱就是thrift,其最常見的使用方式是這樣的:

【thrift會自動生成哪些程式碼呢】

在編譯之後,thrift會生成這些檔案:(我們以mytime.thrift為例)

【一起用thrift來做個專案!】

從我的學習經驗來看,框架的學習路線是“瞭解應用場景 -> 瞭解用法 -> 看例子 -> 深入使用者 -> 自己寫例子”。我相信,如果你能和我一起走完這個例子,一定會消除對thrift的恐懼,愛上這款RPC框架的。

我們的例子很簡單,就是一個“時間問答”機器人,英文叫做WhatTime,客戶會向伺服器端詢問現在幾點啦,伺服器端會把現在的時間回答給客戶端。就像這樣:

我們會在伺服器端使用C++來實現,而在客戶端會使用C++語言來實現一版,還會使用最近很流行的Go語言實現一版。(Go語言可是未來可能撼動IT界的語言之一哦)

thrift介面描述檔案WhatTime.thrift:

需要經過thrift編譯工具編譯:

然後,我們把server的樣例檔案重新命名一下:

我們將server.cpp中的TellMeTime方法做一些修改,加入報告時間的邏輯:

好了,server.cpp完工,我們對server.cpp進行編譯連結:

如果提示找不到thrift動態連結庫,那就需要把thrift的lib路徑(如/home/roc/program/thrift/lib)加入到ld.so.conf中,然後執行ldconfig命令在重新將動態連結庫裝載到cache中。

然後就可以直接執行./server了,可以看到9090埠開啟,已經開始服務了。

下面,我們繼續編寫客戶端的程式碼。thrift並沒有給出客戶端的程式碼樣例,所以需要自己來開發。

開發完成後,我們對client進行編譯連結:

好了,伺服器端程式server和客戶端程式client都生成好了,可以試著執行這個例子:

在一個終端執行伺服器端程式:

在另一個終端開啟客戶端程式:

順利的話,你應該可以看到執行server的終端視窗會輸出“Now is Fri Nov 1 12:14:06 2013”啦!RPC通訊成功了!

至此,C++版本的客戶端和伺服器端都已經實現了。是不是並沒有那麼的難呢!下面,我們來看看Go語言的客戶端如何實現。

【Go語言版客戶端】

首先通過thrift來生成go的程式碼:

會生成gen-go資料夾,進入其中,可以看到這裡面有什麼東東:

在gen-go資料夾中,我們作如下的操作:

下一步,我們需要對$GOPATH/src/WhatTime中的constants.go、time_service.go和ttypes.go三個檔案做一下小的修改:

說實話,Go語言的準備工作確實有些繁瑣,希望你還有耐心看最關鍵的內容,那就是編寫client.go的程式碼!

我們在src的同級目錄中來編寫,client.go的程式碼如下:

然後進行編譯連結和執行:

至此,我們的Go語言版本也大功告成了!

【結語】

如果你耐心地看到了這裡,說明你完成了thrift的入門。但是“紙上得來終覺淺,自己動手才是真”。

後面還會有Thrift進階篇,敬請期待。