由淺入深瞭解Thrift(一)——Thrift介紹與用法
2.6、 編寫客戶端程式碼
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;
2.7、 需要注意的問題
(1)Thrift的伺服器端和客戶端使用的通訊方式要一樣,否則便無法進行正常通訊;
Thrift的伺服器端的種模式所使用的通訊方式並不一樣,因此,伺服器端使用哪種通訊方式,客戶端程式也要使用這種方式,否則就無法進行正常通訊了。例如,上面的程式碼2.3中,伺服器端使用的工作模式為TNonblockingServer,在該工作模式下需要採用的傳輸方式為TFramedTransport,也就是在通訊過程中會將tcp的位元組流封裝成一個個的幀,此時就需要客戶端程式也這麼做,否則便會通訊失敗。出現如下問題:
伺服器端會爆出如下出錯log:
2015-01-06 17:14:52.365 ERROR [Thread-11] [AbstractNonblockingServer.java:348] - Read an invalid frame size of -2147418111. Are you using TFramedTransport on the client side?
客戶端則會報出如下出錯log:
org.apache.thrift.transport.TTransportException: java.net.SocketException: Connection reset
at org.apache.thrift.transport.TIOStreamTransport.read(TIOStreamTransport.java:129)
at org.apache.thrift.transport.TTransport.readAll(TTransport.java:84)
at org.apache.thrift.protocol.TBinaryProtocol.readAll(TBinaryProtocol.java:362)
at org.apache.thrift.protocol.TBinaryProtocol.readI32(TBinaryProtocol.java:284)
at org.apache.thrift.protocol.TBinaryProtocol.readMessageBegin(TBinaryProtocol.java:191)
at org.apache.thrift.TServiceClient.receiveBase(TServiceClient.java:69)
at com.browan.freepp.dataproxy.service.DataProxyService$Client.recv_addLiker(DataProxyService.java:877)
at com.browan.freepp.dataproxy.service.DataProxyService$Client.addLiker(DataProxyService.java:862)
at com.browan.freepp.dataproxy.service.DataProxyServiceTest.test_Likers(DataProxyServiceTest.java:59)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
Caused by: java.net.SocketException: Connection reset
at java.net.SocketInputStream.read(SocketInputStream.java:196)
at java.net.SocketInputStream.read(SocketInputStream.java:122)
at java.io.BufferedInputStream.fill(BufferedInputStream.java:235)
at java.io.BufferedInputStream.read1(BufferedInputStream.java:275)
at java.io.BufferedInputStream.read(BufferedInputStream.java:334)
at org.apache.thrift.transport.TIOStreamTransport.read(TIOStreamTransport.java:127)
... 31 more
(2)在伺服器端或者客戶端直接使用IDL生成的介面檔案時,可能會遇到下面兩個問題:
[1]、Cannotreduce the visibility of the inherited method fromProcessFunction<I,TestThriftService.getStr_args>
[2]、The typeTestThriftService.Processor<I>.getStr<I> must implement theinherited abstract methodProcessFunction<I,TestThriftService.getStr_args>.isOneway()
問題產生的原因:
問題[1] 是繼承類的訪問許可權縮小所造成的;
問題[2] 是因為存在抽象函式isOneWay所致;
解決辦法:
問題[1]的訪問許可權由protected修改為public;
問題[2]的解決辦法是為抽象函式新增一個空的函式體即可。
2.8、 一些的應用技巧
(1) 為呼叫加上一個事務ID
在分散式服務開發過程中,一次事件(事務)的執行可能跨越位於不同機子上多個服務程式,在後續維護過程中跟蹤log將變得非常麻煩,因此在系統設計的時候,系統的一個事務產生之處應該產生一個系統唯一的事務ID,該ID在各服務程式之間進行傳遞,讓一次事務在所有服務程式輸出的log都以此ID作為標識。
在使用Thrift開發伺服器程式的時候,也應該為每個介面函式提供一個事務ID的引數,並且在伺服器程式開發過程中,該ID應該在內部函式呼叫過程中也進行傳遞,並且在日誌輸出的時候都加上它,以便問題跟蹤。
(2) 封裝返回結果
Thrift提供的RPC方式的服務,使得呼叫方可以像呼叫自己的函式一樣呼叫Thrift服務提供的函式;在使用Thrift開發過程中,儘量不要直接返回需要的資料,而是將返回結果進行封裝,例如上面的例子中的getStr函式就是直接返回了結果string,見Thrift檔案test_service.thrift中對該函式的描述:
stringgetStr(1:string srcStr1, 2:string srcStr2)
在實際開發過程中,這是一種很不好的行為,在返回結果為null的時候還可能造成呼叫方產生異常,需要對返回結果進行封裝,例如:
/*String型別返回結果*/
struct ResultStr
{
1: ThriftResult result,
2: string value
}
其中ThriftResult是自己定義的列舉型別的返回結果,在這裡可以根據自己的需要新增任何自己需要的返回結果型別:
enum ThriftResult
{
SUCCESS, /*成功*/
SERVER_UNWORKING, /*伺服器處於非Working狀態*/
NO_CONTENT, /*請求結果不存在*/
PARAMETER_ERROR, /*引數錯誤*/
EXCEPTION, /*內部出現異常*/
INDEX_ERROR, /*錯誤的索引或者下標值*/
UNKNOWN_ERROR, /*未知錯誤*/
DATA_NOT_COMPLETE, /*資料不完全*/
INNER_ERROR, /*內部錯誤*/
}
此時可以將上述定義的getStr函式修改為:
ResultStr getStr(1:string srcStr1, 2:string srcStr2)
在此函式中,任何時候都會返回一個ResultStr物件,無論異常還是正常情況,在出錯時還可以通過ThriftResult返回出錯的型別。
(3) 將服務與資料型別分開定義
在使用Thrift開發一些中大型專案的時候,很多情況下都需要自己封裝資料結構,例如前面將返回結果進行封裝的時候就定義了自己的資料型別ResultStr,此時,將資料結構和服務分開定義到不通的檔案中,可以增加thrift檔案的易讀性。例如:
在thrift檔案:thrift_datatype.thrift中定義資料型別,如:
namespace java com.browan.freepp.thriftdatatype
const string VERSION = "1.0.1"
/**為ThriftResult新增資料不完全和內部錯誤兩種型別
*/
/****************************************************************************************************
* 定義返回值,
* 列舉型別ThriftResult,表示返回結果,成功或失敗,如果失敗,還可以表示失敗原因
* 每種返回型別都對應一個封裝的結構體,該結構體其命名遵循規則:"Result" + "具體操作結果型別",結構體都包含兩部分內容:
* 第一部分為列舉型別ThriftResult變數result,表示操作結果,可以 表示成功,或失敗,失敗時可以給出失敗原因
* 第二部分的變數名為value,表示返回結果的內容;
*****************************************************************************************************/
enum ThriftResult
{
SUCCESS, /*成功*/
SERVER_UNWORKING, /*伺服器處於非Working狀態*/
NO_CONTENT, /*請求結果不存在*/
PARAMETER_ERROR, /*引數錯誤*/
EXCEPTION, /*內部出現異常*/
INDEX_ERROR, /*錯誤的索引或者下標值*/
UNKNOWN_ERROR /*未知錯誤*/
DATA_NOT_COMPLETE /*資料不完全*/
INNER_ERROR /*內部錯誤*/
}
/*bool型別返回結果*/
struct ResultBool
{
1: ThriftResult result,
2: bool value
}
/*int型別返回結果*/
struct ResultInt
{
1: ThriftResult result,
2: i32 value
}
/*String型別返回結果*/
struct ResultStr
{
1: ThriftResult result,
2: string value
}
/*long型別返回結果*/
struct ResultLong
{
1: ThriftResult result,
2: i64 value
}
/*double型別返回結果*/
struct ResultDouble
{
1: ThriftResult result,
2: double value
}
/*list<string>型別返回結果*/
struct ResultListStr
{
1: ThriftResult result,
2: list<string> value
}
/*Set<string>型別返回結果*/
struct ResultSetStr
{
1: ThriftResult result,
2: set<string> value
}
/*map<string,string>型別返回結果*/
struct ResultMapStrStr
{
1: ThriftResult result,
2: map<string,string> value
}
程式碼2.5
在另外一個檔案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.6
由於在介面服務定義的thrift檔案test_service.thrift中要用到對資料型別定義的thrift檔案:thrift_datatype.thrift,因此需要在其檔案前通過include把自己所使用的thrift檔案包含進來,另外在使用其他thrift檔案中定義的資料型別時要加上它的檔名,如:thrift_datatype.ResultStr
(4) 為Thrift檔案新增版本號
在實際開發過程中,還可以為Thrift檔案加上版本號,以方便對thrift的版本進行控制,如程式碼2.5所示。