Thrift
初識Thrift
Thrift是一個跨語言通信的RPC軟件,最初是由FaceBook開發的,現在是Apache的一個頂級項目。
Thrift概念:
Thrift 最初是由 Facebook 開發用做系統內各語言之間的 RPC 通信的一個可擴展且跨語言的軟件框架,它結合了功能強大的軟件堆棧和代碼生成引擎,允許定義一個簡單的定義文件中的數據類型和服務接口,以作爲輸入文件,編譯器生成代碼用來方便地生成RPC客戶端和服務器通信的無縫跨編程語言。Thrift 是 IDL 描述性語言的一個具體實現,適用於程序對程序靜態的數據交換,需要先確定好數據結構。Thrift 是完全靜態化的,當數據結構發生變化時,必須重新編輯IDL文件、代碼生成再編譯載入的流程,跟其他IDL工具相比較可以視爲是 Thrift 的弱項。Thrift 適用於搭建大型數據交換及存儲的通用工具,在大型系統中的內部數據傳輸上相對於 JSON 和 XML 無論在性能、傳輸大小上有明顯的優勢。
什麼是RPC
Remote Procedure Call Protocol 遠程過程調用協議,我們 舉個例子說明:
本地調用
public void invoke() {
String param1="string1";
String param2="string2";
String res = getStr(param1,param2);
System.out.println("res = " + res);
}
private String getStr(String str1, String str2) {
return str1 + str2;
}
調用方和被調用方都在一個程序內部,屬於進程內調用。CPU 在執行調用時切換去執行被調用函數,執行完後再切換回來執行後續的代碼。對調用方而言,執行被調用函數時會阻塞(非異步情況下)直到調用函數執行完畢。
RPC調用
public void test() {
TestQry.Client client = getClient("192.168.4.222", 7800, 5000);
String param1="string1";
String param2="string2";
String res = client.getStr(param1,param2);
System.out.println("res = " + res);
}
這裏進程間調用,但是調用方和被調用方不再同一個進程,甚至不同的服務器和機房。進程間調用需要通過網絡來傳輸數據,調用方在執行 RPC 調用時會阻塞(非異步情況下)直到調用結果返回才繼續執行後續代碼。
小結
現在應該知道什麼是RPC了,概括一下就是:RPC是一種通過網絡從遠程計算機程序上請求服務的方式,它使得開發包括網絡分佈式多程序在內的應用程序更加容易。
Thrift結構
-
代碼框架層
Thrift 定義的服務接口描述文件生成的客戶端和服務器端代碼框架。
-
數據讀寫操作層
是根據Thrift 文件生成代碼實現數據的讀寫操作。Thrift允許定義一個簡單的定義文件中的數據類型和服務接口,以作爲輸入文件,編譯器生成代碼用來方便地生成RPC客戶端和服務器通信的無縫跨編程語言。
Thrift通信
一張圖說明服務端啓動與提供服務的流程:
程序調用了 TThreadPoolServer 的 serve() 方法後,server 進入阻塞監聽狀態,其阻塞在 TServerSocket 的accept()方法上。當接收到來自客戶端的消息後,服務器發起一個新線程處理這個消息請求,原線程再次進入阻塞狀態。在新線程中,服務器通過 TBinaryProtocol 協議讀取消息內容,調用 HelloServiceImpl 的helloVoid() 方法,並將結果寫入 helloVoid_result 中傳回客戶端。
再來一張圖說明服務端啓動服務以後,客戶端請求服務的流程:
程序調用了 Hello.Client 的 helloVoid() 方法,在 helloVoid() 方法中,通過 send_helloVoid() 方法發送對服務的調用請求,通過 recv_helloVoid() 方法接收服務處理請求後返回的結果。
Thrift數據類型
它支持這幾大數據結構:基本類型、結構體和異常類型、容器類型、服務類型。
基本類型
-
bool: 布爾值(true or false)
-
byte: 有符號字節
-
i16: 16位有符號整型
-
i32: 32位有符號整型
-
i64: 64位有符號整型
-
double:64位浮點型
-
string: 二進制字符串
結構體類型
struck UserDemo{
1: i32 id;
2: string name;
3: i32 age = 25;
4: string phone
}
結構體有幾點要注意:
1:在java中,結構體會被轉換成對象的類;
2:其成員都有明確的類型;
3:成員都是被正整數編號過的,這個編號不能重複;
4:在結構體中,可以直接設置默認值;
服務類型
service querySrv {
/** 根據名字和年齡來查找用戶 */
UserDemo qryUser(1:string name, 2:i32 age);
/** 根據id查找對應的手機號 */
string queryPhone(1:i32 id);
}
服務類型說明:服務類型語法等同於Java中的接口。
小結
除了上面所提到的四大類型外,Thrift還支持枚舉和常量類型。當然,它也有不支持的類型,典型的就是Date類型不支持。
Thrit組件
下面我們將圍繞這三個組件展開介紹,內容略顯枯燥,但是是使用Thrift的關鍵。
Protocol:數據通信協議
定義了消息是怎樣序列化的,主要有二進制、文本。
常見的有:
◆TBinaryProtocol:這是Thrift的默認協議,使用二進制編碼格式進行數據傳輸。
◆TCompactProtocol:壓縮格式。
◆TJSONProtocol:以JSON數據編碼協議進行數據傳輸。
◆TDebugProtocol:常常用以編碼人員測試,以文本的形式展現方便閱讀。
用戶可以根據自己的實際需求選擇合適的類型,生產環境一般用二進制類型的多
Transport:傳輸方式
定義了消息是怎樣在客戶端和服務器端之間通信的。
常見的有:
◆TSocket::採用TCP Socket進行數據傳輸。
◆ TFileTransport:文件(日誌)傳輸類,允許client將文件傳給server,允許server 將收到的數據寫到文件中。
◆ THttpTransport:採用Http傳輸協議進行數據傳輸
◆ TZlibTransport:壓縮後對數據進行傳輸,或者將收到的數據解壓
下面幾個類主要是對上面幾個類的修飾(採用裝飾模式),提高傳輸效率
◆ TBufferedTransport:對某個Transport對象操作的數據進行buffer,即從buffer中讀取數據進行傳輸,或者將數據直接寫入buffer◆ TFramedTransport:以frame(幀)爲單位進行傳輸,非阻塞式服務中使用。同 TBufferedTransport類似,也會對相關數據進行buffer,同時,它支持定長數據發送和接收。◆ TMemoryBuffer:從一個緩衝區中讀寫數據◆ TTransport是所有Transport類的父類,爲上層提供了統一的接口而且通過 TTransport 即可訪問各個子類不同實現,類似多態。
Server Type: 服務端類型
server 用於從 transport 接收序列化的消息,根據 protocol 反序列化之,調用用戶定義的消息處理器,並序列化消息處理器(接口實現類)的響應,然後再將它們寫回 transport。
常用的server實現有:
◆ TSimpleServer:接受一個連接,處理連接請求,直到客戶端關閉了連接,它纔回去接受一個新的連接。正因爲它只在一個單獨的線程中以阻塞 I/O 的方式完成這些工作,所以它只能服務一個客戶端連接,其他所有客戶端在被服務器端接受之前都只能等待。
◆ TSimpleServer:主要用於測試目的,不要在生產環境中使用它。
◆ TNonblockingServer:阻塞在多個連接上,而不是阻塞在單一的連接上。 server 可同時服務多個客戶端。
缺點:在業務處理上還是採用單線程順序來完成,在業務處理比較複雜、耗時的時候,例如某些接口函數需要讀取數據庫執行時間較長,此時該模式效率也不高,因爲多個調用請求任務依然是順序一個接一個執行。
◆ THsHaServer:是TNonblockingServer類的子類,主線程負責數據讀取,然後引入一個線程池來專門進行業務處理,緩解TNonblockingServer類的缺點。
缺點:主線程需要完成對所有socket的監聽以及數據讀寫的工作,當併發請求數較大時,且發送數據量較多時,監聽socket上新連接請求不能被及時接受。
◆ TThreadPoolServer: TThreadPoolServer模式採用阻塞socket方式工作,主線程負責阻塞式監聽“監聽socket”中是否有新socket到來,數據讀取和業務處理都交由線程池完成,主線程只負責監聽新連接。
缺點:線程池模式的處理能力受限於線程池的工作能力,當併發請求數大於線程池中的線程數時,新請求也只能排隊等待。
◆ TThreadedSelectorServer: TThreadedSelectorServer模式是目前Thrift提供的最高級的模式。看圖
TThreadedSelectorServer對於大部分應用場景性能都不會差,因此,如果實在不知道選擇哪種工作模式,使用TThreadedSelectorServer就可以。
如何使用Thrift
Step1
使用IDL語言建立.thrift文件,示例:
/**
文件名 TestQry.thrift
實現的功能:創建一個查詢結果struct和一個服務接口service。
基於:thrift-0.9.2
*/
namespace java com.thrift
struct QryResult {
/**
返回碼:1成功,0失敗
*/
1:i32 code;
/**
響應信息
*/
2:string msg;
}
service TestQry {
/**
測試查詢的接口,當qryCode值爲1時候返回”成功”的響應信息,qryCode值爲其他值時返回”失敗”
*/
QryResult qryTest(1:i32 qryCode)
}
Step2
利用Thrift代碼生成程序生成相應的java代碼
1:將我們建立的TestQry.thrift文件與thrift-0.9.2.exe放在同一目錄,如下:
2:執行命令,生成相應的java代碼: thrift.exe -gen java TestQry.thrift
Step3
生成的java代碼導入到你的maven項目中,添加相應的依賴包
Step4
創建QueryImp.java實現TestQry.Iface接口,這裏寫業務。
/**
文件名 TestQry.thrift
實現的功能:創建一個查詢結果struct和一個服務接口service。
基於:thrift-0.9.2
*/
namespace java com.thrift
struct QryResult {
/**
返回碼:1成功,0失敗
*/
1:i32 code;
/**
響應信息
*/
2:string msg;
}
service TestQry {
/**
測試查詢的接口,當qryCode值爲1時候返回”成功”的響應信息,qryCode值爲其他值時返回”失敗”
*/
QryResult qryTest(1:i32 qryCode)
}
Step5
創建ThriftServerDemo實現服務端,這裏用TNonblockingServerSocket
public class ThriftServerDemo {
private final static int DEFAULT_PORT = 30001;
private static TServer server = null;
public static void main(String[] args) {
try {
TNonblockingServerSocket socket = new TNonblockingServerSocket(DEFAULT_PORT);
//1. 基於Handler創建Processer
TestQry.Processor processor = new TestQry.Processor(new QueryImp());
TNonblockingServer.Args arg = new TNonblockingServer.Args(socket);
//2.創建protocol
arg.protocolFactory(new TBinaryProtocol.Factory());
//3.創建tansport
arg.transportFactory(new TFramedTransport.Factory());
arg.processorFactory(new TProcessorFactory(processor));
//4.基於三者創建server
server = new TNonblockingServer(arg);
//5.運行server
server.serve();
} catch (TTransportException e) {
e.printStackTrace();
}
}
}
Step6
public class ThriftClientDemo {
private final static int DEFAULT_QRY_CODE = 1;
public static void main(String[] args) {
//1.創建transport
TTransport tTransport = getTTransport();
//2.創建protocol
TProtocol protocol = new TBinaryProtocol(tTransport);
//3.創建client
TestQry.Client client = new TestQry.Client(protocol);
QryResult result = null;
try {
//運行client的方法
result = client.qryTest(DEFAULT_QRY_CODE);
} catch (TException e) {
e.printStackTrace();
}
System.out.println("code="+result.code+" "+"msg="+result.msg);
}
public static TTransport getTTransport() {
TTransport tTransport = getTTransport("127.0.0.1", 30001, 5000);
if (!tTransport.isOpen()){
try {
tTransport.open();
} catch (TTransportException e) {
e.printStackTrace();
}
}
return tTransport;
}
private static TTransport getTTransport(String host, int port, int timeout) {
final TSocket tSocket = new TSocket(host, port, timeout);
final TTransport transport = new TFramedTransport(tSocket);
return transport;
}
}
Step7
所有準備工作都已經做好了,接下來我們就來進行 Client 和 Server 的通信。
先運行 ThriftServerDemo 啓動 Server,然後運行 ThriftClientDemo.java 創建 Client進行調用,以上步驟都是我自己寫過的,可以得到結果。
結語
Thrift有什麼優勢?
Thrift是一個跨語言通信的服務框架,不同語言開發的程序可以通過Thrift來進行通信:通過編譯一個後綴名爲.thrift的文件來生成指定語言的代碼,通過生成的代碼我們就可以編寫出跨語言通信的代碼了:如服務端是用Thrift生成的Java代碼,客戶端使用Thrift生成的C++/C#代碼,用Thtift可以完成C++代碼到Java代碼的調用,而不需要關心其他如網絡通信等內容,可以讓開發人員專注於業務實現。
Thrift爲什麼這麼好用?
網絡編程需要關注很多數據傳輸中的細節,比如數據如何序列化、如何在字節數組裏建立結構、如何在兩端解析字節數組、如何處理Handler裏的事件狀態、如何把多個Handler按順序串起來,Thrift掩蓋了數據傳輸這件事情,開發者使用的時候就是純純的RPC的使用感受。