Thrift的簡介
Thrift是Facebook於2007年開發的跨語言的RPC服框架,提供多語言的編譯功能,並提供多種服務器工作模式;用戶通過Thrift的IDL(接口定義語言)來描述接口函數及數據類型,然後通過Thrift的編譯環境生成各種語言類型的接口文件,用戶可以根據自己的需要採用不同的語言開發客戶端代碼和服務器端代碼。
例如,我想開發一個快速計算的RPC服務,它主要通過接口函數getInt對外提供服務,這個RPC服務的getInt函數使用用戶傳入的參數,經過複雜的計算,計算出一個整形值返回給用戶;服務器端使用java語言開發,而調用客戶端可以是java、c、python等語言開發的程序,在這種應用場景下,我們只需要使用Thrift的IDL描述一下getInt函數(以.thrift爲後綴的文件),然後使用Thrift的多語言編譯功能,將這個IDL文件編譯成C、java、python幾種語言對應的“特定語言接口文件”(每種語言只需要一條簡單的命令即可編譯完成),這樣拿到對應語言的“特定語言接口文件”之後,就可以開發客戶端和服務器端的代碼了,開發過程中只要接口不變,客戶端和服務器端的開發可以獨立的進行。
Thrift爲服務器端程序提供了很多的工作模式,例如:線程池模型、非阻塞模型等等,可以根據自己的實際應用場景選擇一種工作模式高效地對外提供服務;
RPC是Remote Procedure Call,意爲遠程過程調用,具體自行百度。
Thrift是一種c/s的架構體系.在最上層是用戶自行實現的業務邏輯代碼.第二層是由thrift編譯器自動生成的代碼,主要用於結構化數據的解析,發送和接收。TServer主要任務是高效的接受客戶端請求,並將請求轉發給Processor處理。Processor負責對客戶端的請求做出響應,包括RPC請求轉發,調用參數解析和用戶邏輯調用,返回值寫回等處理。從TProtocol以下部分是thirft的傳輸協議和底層I/O通信。TProtocol是用於數據類型解析的,將結構化數據轉化爲字節流給TTransport進行傳輸。TTransport是與底層數據傳輸密切相關的傳輸層,負責以字節流方式接收和發送消息體,不關注是什麼數據類型。底層IO負責實際的數據傳輸,包括socket、文件和壓縮數據流等。
具體見下圖
下面請看Thrift的一個小例子,每個步驟在看完小例子後都會有具體的講解
1,創建一個服務Hello,創建文件Hello.thrift,代碼如下:
namespace java service.demo
service Hello{
string helloString(1:string para)
}
這裏定義了一個名爲helloString的方法,入參和返回值都是一個string類型的參數.
2,終端進入Hello.thrift所在目錄,執行命令:
thrift -r -gen java Hello.thrift
發現在當前目錄下多了一個gen-java的目錄,裏面的有一個Hello.java的文件.這個java文件包含Hello服務的接口定義Hello.Iface,以及服務調用的底層通信細節,包括客戶端的調用邏輯Hello.Client以及服務端的處理邏輯Hello.Processor,
3,創建一個Maven管理的Java項目,pom.xml中添加相關的依賴,並將Hello.java文件複製到項目中:
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.10.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
</dependency>
4,創建HelloServiceImpl實現Hello.Iface接口:
package service.demo;
import org.apache.thrift.TException;
/**
* @author yogo.wang
* @date 2017/02/21-下午2:13.
*/
public class HelloServiceImpl implements Hello.Iface {
public String helloString(String para) throws TException {
return "result:"+para;
}
}
5,創建服務端實現代碼HelloServiceServer,把HelloServiceImpl作爲一個具體的處理器傳遞給Thrift服務器:
package service.demo;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TTransportException;
/**
* @author yogo.wang
* @date 2017/02/21-下午2:15.
*/
public class HelloServiceServer {
/**
* 啓動thrift服務器
* @param args
*/
public static void main(String[] args) {
try {
System.out.println("服務端開啓....");
TProcessor tprocessor = new Hello.Processor<Hello.Iface>(new HelloServiceImpl());
// 簡單的單線程服務模型
TServerSocket serverTransport = new TServerSocket(9898);
TServer.Args tArgs = new TServer.Args(serverTransport);
tArgs.processor(tprocessor);
tArgs.protocolFactory(new TBinaryProtocol.Factory());
TServer server = new TSimpleServer(tArgs);
server.serve();
}catch (TTransportException e) {
e.printStackTrace();
}
}
}
6,創建客戶端實現代碼HelloServiceClient,調用Hello.client訪問服務端的邏輯實現:
package service.demo;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;
/**
* @author yogo.wang
* @date 2017/02/21-下午2:35.
*/
public class HelloServiceClient {
public static void main(String[] args) {
System.out.println("客戶端啓動....");
TTransport transport = null;
try {
transport = new TSocket("localhost", 9898, 30000);
// 協議要和服務端一致
TProtocol protocol = new TBinaryProtocol(transport);
Hello.Client client = new Hello.Client(protocol);
transport.open();
String result = client.helloString("哈哈");
System.out.println(result);
} catch (TTransportException e) {
e.printStackTrace();
} catch (TException e) {
e.printStackTrace();
} finally {
if (null != transport) {
transport.close();
}
}
}
}
全部工作完成後,下面來測試一下,先執行服務端main方法,在執行客戶端main方法,會在客戶端控制檯打印出:result:哈哈.
下面是剛剛每個步驟的詳解
首先是IDL描述的Thrift文件
使用IDL對接口進行描述的thrift文件命名一般都是以“.thrift”作爲後綴:XXX.thrift,可以在該文件的開頭爲該文件加上命名空間限制,格式爲:namespace語言 命名空間的名字;例如:
namespace javacom.test.service
IDL文件中對所有接口函數的描述都放在service中,service的名字可以自己指定,該名字也將被用作生成的特定語言接口文件的名字,接口函數需要對參數使用序號標號,除最後一個接口函數外,要以“,”結束對函數的描述。
例如,下面一個IDL描述的Thrift文件(該Thrift文件的文件名爲:test_service.thrift)的全部內容:
namespace java com.test.service
include "thrift_datatype.thrift"
service TestThriftService
{
/**
*value 中存放兩個字符串拼接之後的字符串
*/
thrift_datatype.ResultStr getStr(1:string srcStr1, 2:string srcStr2),
thrift_datatype.ResultInt getInt(1:i32 val)
}
代碼2.1
這裏的TestThriftService就被用作生成的特定語言的文件名,例如我想用該Thrift文件生成一個java版本的接口文件,那麼生成的java文件名就是:TestThriftService.java。
(1) 編寫IDL文件時需要注意的問題
[1]函數的參數要用數字依序標好,序號從1開始,形式爲:“序號:參數名”;
[2]每個函數的最後要加上“,”,最後一個函數不加;
[3]在IDL中可以使用/*……*/添加註釋
(2) IDL支持的數據類型
IDL大小寫敏感,它共支持以下幾種基本的數據類型:
[1]string, 字符串類型,注意是全部小寫形式;例如:string aString
[2]i16, 16位整形類型,例如:i16 aI16Val;
[3]i32,32位整形類型,對應C/C++/java中的int類型;例如: I32 aIntVal
[4]i64,64位整形,對應C/C++/java中的long類型;例如:I64 aLongVal
[5]byte,8位的字符類型,對應C/C++中的char,java中的byte類型;例如:byte aByteVal
[6]bool, 布爾類型,對應C/C++中的bool,java中的boolean類型; 例如:bool aBoolVal
[7]double,雙精度浮點類型,對應C/C++/java中的double類型;例如:double aDoubleVal
[8]void,空類型,對應C/C++/java中的void類型;該類型主要用作函數的返回值,例如:void testVoid(),
除上述基本類型外,ID還支持以下類型:
[1]map,map類型,例如,定義一個map對象:map<i32, i32> newmap;
[2]set,集合類型,例如,定義set<i32>對象:set<i32> aSet;
[3]list,鏈表類型,例如,定義一個list<i32>對象:list<i32> aList;
(3) 在Thrift文件中自定義數據類型
在IDL中支持兩種自定義類型:枚舉類型和結構體類型,具體如下:
[1]enum, 枚舉類型,例如,定義一個枚舉類型:
enum Numberz
{
ONE = 1,
TWO,
THREE,
FIVE = 5,
SIX,
EIGHT = 8
}
注意,枚舉類型裏沒有序號
[2]struct,自定義結構體類型,在IDL中可以自己定義結構體,對應C中的struct,c++中的struct和class,java中的class。例如:
struct TestV1 {
1: i32 begin_in_both,
3: string old_string,
12: i32 end_in_both
}
注意,在struct定義結構體時需要對每個結構體成員用序號標識:“序號: ”。
(4) 定義類型別名
Thrift的IDL支持C/C++中類似typedef的功能,例如:
typedefi32 Integer
就可以爲i32類型重新起個名字Integer。
下面講解將IDL文件編譯成對應語言的接口文件
搭建Thrift編譯環境之後,使用下面命令即可將IDL文件編譯成對應語言的接口文件:
thrift --gen <language> <Thrift filename>
例如:如果使用上面的thrift文件(見上面的代碼2.1):test_service.thrift生成一個java語言的接口文件,則只需在搭建好thrift編譯環境的機子上,執行如下命令即可:
thrift --gen java test_service.thrift
這裏,我直接在test_service.thrift文件所在的目錄下執行的命令,所以直接使用文件名即可(如圖2.1的標號1所示),如果不在test_service.thrift所在的目錄中,則需要具體指明該文件所在的路徑。
圖2.1
如圖2.1 中標號2所示,生成的gen-java的目錄,目錄下面有com、test、service三級目錄,這三級目錄也是根據test_service.thrift文件中命名空間的名字:com.test.service生成的,進入目錄之後可以看到生成的java語言的接口文件名爲:TestThriftService.java,這個文件的名字也是根據test_service.thrift文件的service名字來生成的(見代碼2.1)。
接下來講解服務端代碼
訪問器程序需實現TestThriftService.Iface接口,在實現接口中完成自己要提供的服務,服務器端對服務接口實現的代碼如下所示:
package com.test.service;
import org.apache.thrift.TException;
public class TestThriftServiceImpl implements TestThriftService.Iface
{
@Override
public String getStr(String srcStr1, String srcStr2) throws TException {
long startTime = System.currentTimeMillis();
String res = srcStr1 + srcStr2;
long stopTime = System.currentTimeMillis();
System.out.println("[getStr]time interval: " + (stopTime-startTime));
return res;
}
@Override
public int getInt(int val) throws TException {
long startTime = System.currentTimeMillis();
int res = val * 10;
long stopTime = System.currentTimeMillis();
System.out.println("[getInt]time interval: " + (stopTime-startTime));
return res;
}
}
代碼2.2
服務器端啓動thrift服務框架的程序如下所示,在本例中服務器採用TNonblockingServer工作模式:
package com.test.service;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TNonblockingServer;
import org.apache.thrift.server.TServer;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TNonblockingServerSocket;
import org.apache.thrift.transport.TTransportException;
public class testMain {
private static int m_thriftPort = 12356;
private static TestThriftServiceImpl m_myService = new TestThriftServiceImpl();
private static TServer m_server = ;
private static void createNonblockingServer() throws TTransportException
{
TProcessor tProcessor = new TestThriftService.Processor<TestThriftService.Iface>(m_myService);
TNonblockingServerSocket nioSocket = new TNonblockingServerSocket(m_thriftPort);
TNonblockingServer.Args tnbArgs = new TNonblockingServer.Args(nioSocket);
tnbArgs.processor(tProcessor);
tnbArgs.transportFactory(new TFramedTransport.Factory());
tnbArgs.protocolFactory(new TBinaryProtocol.Factory());
// 使用非阻塞式IO,服務端和客戶端需要指定TFramedTransport數據傳輸的方式
m_server = new TNonblockingServer(tnbArgs);
}
public static boolean start()
{
try {
createNonblockingServer();
} catch (TTransportException e) {
System.out.println("start server error!" + e);
return false;
}
System.out.println("service at port: " + m_thriftPort);
m_server.serve();
return true;
}
public static void main(String[] args)
{
if(!start())
{
System.exit(0);
}
}
}
代碼2.3
在服務器端啓動thrift框架的部分代碼比較簡單,不過在寫這些啓動代碼之前需要先確定服務器採用哪種工作模式對外提供服務,Thrift對外提供幾種工作模式,例如:TSimpleServer、TNonblockingServer、TThreadPoolServer、TThreadedSelectorServer等模式,每種服務模式的通信方式不一樣,因此在服務啓動時使用了那種服務模式,客戶端程序也需要採用對應的通信方式。
另外,Thrift支持多種通信協議格式:TCompactProtocol、TBinaryProtocol、TJSONProtocol等,因此,在使用Thrift框架時,客戶端程序與服務器端程序所使用的通信協議一定要一致,否則便無法正常通信。
以上述代碼2.3採用的TNonblockingServer爲例,說明服務器端如何使用Thrift框架,在服務器端創建並啓動Thrift服務框架的過程爲:
[1]爲自己的服務實現類定義一個對象,如代碼2.3中的:
TestThriftServiceImpl m_myService =new TestThriftServiceImpl();
這裏的TestThriftServiceImpl類就是代碼2.2中我們自己定義的服務器端對各服務接口的實現類。
[2]定義一個TProcess對象,在根據Thrift文件生成java源碼接口文件TestThriftService.java中,Thrift已經自動爲我們定義了一個Processor;後續節中將對這個TProcess類的功能進行詳細描述;如代碼2.3中的:
TProcessor tProcessor = NewTestThriftService.Processor<TestThriftService.Iface>(m_myService);
[3]定義一個TNonblockingServerSocket對象,用於tcp的socket通信,如代碼2.3中的:
TNonblockingServerSocket nioSocket = new TNonblockingServerSocket(m_thriftPort);
在創建server端socket時需要指明監聽端口號,即上面的變量:m_thriftPort。
[4]定義TNonblockingServer所需的參數對象TNonblockingServer.Args;並設置所需的參數,如代碼2.3中的:
TNonblockingServer.Args tnbArgs = new TNonblockingServer.Args(nioSocket);
tnbArgs.processor(tProcessor);
tnbArgs.transportFactory(new TFramedTransport.Factory());
tnbArgs.protocolFactory(new TBinaryProtocol.Factory());
在TNonblockingServer模式下我們使用二進制協議:TBinaryProtocol,通信方式採用TFramedTransport,即以幀的方式對數據進行傳輸。
[5]定義TNonblockingServer對象,並啓動該服務,如代碼2.3中的:
m_server = new TNonblockingServer(tnbArgs);
…
m_server.serve();
接下來講解客戶端代碼
Thrift的客戶端代碼同樣需要服務器開頭的那兩步:添加三個jar包和生成的java接口文件TestThriftService.java。
m_transport = new TSocket(THRIFT_HOST, THRIFT_PORT,2000);
TProtocol protocol = new TBinaryProtocol(m_transport);
TestThriftService.Client testClient = new TestThriftService.Client(protocol);
try {
m_transport.open();
String res = testClient.getStr("test1", "test2");
System.out.println("res = " + res);
m_transport.close();
} catch (TException e){
// TODO Auto-generated catch block
e.printStackTrace();
}
代碼2.4
由代碼2.4可以看到編寫客戶端代碼非常簡單,只需下面幾步即可:
[1]創建一個傳輸層對象(TTransport),具體採用的傳輸方式是TFramedTransport,要與服務器端保持一致,即:
m_transport =new TFramedTransport(newTSocket(THRIFT_HOST,THRIFT_PORT, 2000));
這裏的THRIFT_HOST, THRIFT_PORT分別是Thrift服務器程序的主機地址和監聽端口號,這裏的2000是socket的通信超時時間;
[2]創建一個通信協議對象(TProtocol),具體採用的通信協議是二進制協議,這裏要與服務器端保持一致,即:
TProtocolprotocol =new TBinaryProtocol(m_transport);
[3]創建一個Thrift客戶端對象(TestThriftService.Client),Thrift的客戶端類TestThriftService.Client已經在文件TestThriftService.java中,由Thrift編譯器自動爲我們生成,即:
TestThriftService.ClienttestClient =new TestThriftService.Client(protocol);
[4]打開socket,建立與服務器直接的socket連接,即:
m_transport.open();
[5]通過客戶端對象調用服務器服務函數getStr,即:
String res = testClient.getStr("test1","test2");
System.out.println("res = " +res);
[6]使用完成關閉socket,即:
m_transport.close();
這裏有以下幾點需要說明:
[1]在同步方式使用客戶端和服務器的時候,socket是被一個函數調用獨佔的,不能多個調用同時使用一個socket,例如通過m_transport.open()打開一個socket,此時創建多個線程同時進行函數調用,這時就會報錯,因爲socket在被一個調用佔着的時候不能再使用;
[2]可以分時多次使用同一個socket進行多次函數調用,即通過m_transport.open()打開一個socket之後,你可以發起一個調用,在這個次調用完成之後,再繼續調用其他函數而不需要再次通過m_transport.open()打開socket;
————————————————————————————————————————————
轉載整理自