2、基本知識
Thrift最初由facebook開發用做系統內各語言之間的RPC框架 。2007年由facebook貢獻到apache基金 ,08年5月進入apache孵化器 ,稱為Apache Thrift。和其他RPC實現相比,Apache Thrift主要的有點是:支援的語言多(C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk等多種語言)、併發效能高(還記得上篇文章中,我們提到的影響RPC效能的幾個關鍵點嗎?)。
為了支援多種語言,Apache Thrift有一套自己的介面定義語言,並且透過Apache Thrift的程式碼生成程式,能夠生成各種程式語言的程式碼。這樣是保證各種語言進行通訊的前提條件。為了能夠實現簡單的Apache Thrift例項,首先我們就需要講解一下Apache Thrift的IDL。
2-1、Thrift程式碼生成程式安裝如果您是在windows環境下執行進行Apache Thrift的試驗,那麼您無需安裝任何工具,直接下載Apache Thrift在windows下的程式碼生成程式http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.exe(在這篇文章寫作時,使用的是Apache Thrift的0.9.3版本);如果您執行在Linux系統下,那麼下載http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.tar.gz,並進行編譯、安裝(過程很簡單,這裡就不再贅述了)。安裝後記得新增執行位置到環境變數中。
2-2、IDL格式概要以下是一個簡單的IDL檔案定義:
# 名稱空間的定義 注意‘java’的關鍵字namespace java testThrift.iface# 結構體定義struct Request { 1:required string paramJSON; 2:required string serviceName;}# 另一個結構體定義struct Reponse { 1:required RESCODE responeCode; 2:required string responseJSON;}# 異常描述定義exception ServiceException { 1:required EXCCODE exceptionCode; 2:required string exceptionMess;}# 列舉定義enum RESCODE { _200=200; _500=500; _400=400;}# 另一個列舉enum EXCCODE { PARAMNOTFOUND = 2001; SERVICENOTFOUND = 2002;}# 服務定義service HelloWorldService { Reponse send(1:Request request) throws (1:ServiceException e);}
以上IDL檔案是可以直接用來生成各種語言的程式碼的。下面給出常用的各種不同語言的程式碼生成命令:
# 生成javathrift-0.9.3 -gen java ./demoHello.thrift# 生成c++thrift-0.9.3 -gen cpp ./demoHello.thrift# 生成phpthrift-0.9.3 -gen php ./demoHello.thrift# 生成node.jsthrift-0.9.3 -gen js:node ./demoHello.thrift# 生成c#thrift-0.9.3 -gen csharp ./demoHello.thrift# 您可以透過以下命令檢視生成命令的格式thrift-0.9.3 -help1234567891011121314151617
2-2-1、基本型別
基本型別就是:不管哪一種語言,都支援的資料形式表現。Apache Thrift中支援以下幾種基本型別:
bool: 布林值 (true or false), one bytebyte: 有符號位元組i16: 16位有符號整型i32: 32位有符號整型i64: 64位有符號整型double: 64位浮點型string: 字串/字元陣列binary: 二進位制資料(在java中表現為java.nio.ByteBuffer)2-2-2、struct結構在面嚮物件語言中,表現為“類定義”;在弱型別語言、動態語言中,表現為“結構/結構體”。定義格式如下:
struct <結構體名稱> { <序號>:[欄位性質] <欄位型別> <欄位名稱> [= <預設值>] [;|,]}
例項:
struct Request { 1:required binary paramJSON; 2:required string serviceName 3:optional i32 field1 = 0; 4:optional i64 field2, 5: list<map<string , string>> fields3}
結構體名稱:可以按照您的業務需求,給定不同的名稱(區分大小寫)。但是要注意,一組IDL定義檔案中結構體名稱不能重複,且不能使用IDL已經佔用的關鍵字(例如required 、struct 等單詞)。序號:序號非常重要。正整數,按照順序排列使用。這個屬性在Apache Thrift進行序列化的時候被使用。欄位性質:包括兩種關鍵字:required 和 optional,如果您不指定,那麼系統會預設為required。required表示這個欄位必須有值,並且Apache Thrift在進行序列化時,這個欄位都會被序列化;optional表示這個欄位不一定有值,且Apache Thrift在進行序列化時,這個欄位只有有值的情況下才會被序列化。欄位型別:在struct中,欄位型別可以是某一個基礎型別,也可以是某一個之前定義好的struct,還可以是某種Apache Thrift支援的容器(set、map、list),還可以是定義好的列舉。欄位的型別是必須指定的。欄位名稱:欄位名稱區分大小寫,不能重複,且不能使用IDL已經佔用的關鍵字(例如required 、struct 等單詞)。預設值:您可以為某一個欄位指定預設值(也可以不指定)。結束符:在struct中,支援兩種結束符,您可以使用“;”或者“,”。當然您也可以不使用結束符(Apache Thrift程式碼生成程式,會自己識別到)2-2-3、containers集合/容器Apache Thrift支援三種類型的容器,容器在各種程式語言中普遍存在:
list< T >:有序列表(JAVA中表現為ArrayList),T可以是某種基礎型別,也可以是某一個之前定義好的struct,還可以是某種Apache Thrift支援的容器(set、map、list),還可以是定義好的列舉。有序列表中的元素允許重複。set< T >:無序元素集合(JAVA中表現為HashSet),T可以是某種基礎型別,也可以是某一個之前定義好的struct,還可以是某種Apache Thrift支援的容器(set、map、list),還可以是定義好的列舉。無序元素集合中的元素不允許重複,一旦重複後一個元素將覆蓋前一個元素。map2-2-4、enmu列舉enum <列舉名稱> { <列舉欄位名> = <列舉值>[;|,]}123
示例如下:
enum RESCODE { _200=200; _500=500; _400=400;}12345
2-2-5、常量定義
Apache Thrift允許定義常量。常量的關鍵字為“const”,常量的型別可以是Apache Thrift的基礎型別,也可以是某一個之前定義好的struct,還可以是某種Apache Thrift支援的容器(set、map、list),還可以是定義好的列舉。示例如下:
const i32 MY_INT_CONST = 111111; const i64 MY_LONG_CONST = 11111122222222333333334444444;const RESCODE MY_RESCODE = RESCODE._200;12345
2-2-6、exception 異常
Apache Thrift的exception,主要在定義服務介面時使用。其定義方式類似於struct(您可以理解成,把struct關鍵字換成exception關鍵字即可),示例如下:
exception ServiceException { 1:required EXCCODE exceptionCode; 2:required string exceptionMess;}1234
2-2-7、service 服務介面
Apache Thrift中最重要的IDL定義之一。在後續的程式碼生成階段,透過IDL定義的這些服務將構成Apache Thrift客戶端呼叫Apache Thrift服務端的基本遠端過程。service服務介面的定義形式如下所示:
service <服務名稱> { <void | 返回指型別> <服務方法名>([<入參序號>:[required | optional] <引數型別> <引數名> ...]) [throws ([<異常序號>:[required | optional] <異常型別> <異常引數名>...])]}123
服務名稱:服務名可以按照您的業務需求自行制定,注意服務名是區分大小寫的。IDL中服務名稱只有兩個限制,就是不能重複使用相同的名稱,不能使用IDL已經佔用的關鍵字(例如required 、struct 等單詞)。返回值型別:如果這個呼叫方法沒有返回型別,那麼可以關鍵字“void”; 可以是Apache Thrift的基礎型別,也可以是某一個之前定義好的struct,還可以是某種Apache Thrift支援的容器(set、map、list),還可以是定義好的列舉。服務方法名:服務方法名可以根據您的業務需求自定製定,注意區分大小寫。在同一個服務中,不能重複使用一個服務方法名命名多個方法(一定要注意),不能使用IDL已經佔用的關鍵字。服務方法引數:<入參序號>:[required | optional] <引數型別> <引數名>。注意和struct中的欄位定義相似,可以指定required或者optional;如果不指定則系統預設為required 。如果一個服務方法中有多個引數名,那麼這些引數名稱不能重複。服務方法異常:throws ([<異常序號>:[required | optional] <異常型別> <異常引數名>。throws關鍵字是服務方法異常定義的開始點。在throws關鍵字後面,可以定義1個或者多個不同的異常型別。Apache Thrift服務定義的示例如下:
service HelloWorldService { Reponse send(1:Request request) throws (1:ServiceException e);}123
2-2-8、namespace名稱空間
Apache Thrift支援為不同語言制定不同的名稱空間:
namespace java testThrift.ifacenamespace php testThrift.ifacenamespace cpp testThrift.iface12345
2-2-9、註釋
Apache Thrift 支援多種風格的註釋。這是為了適應不同語言背景的開發者:
/** 註釋方式1:**/// 註釋方式2# 註釋方式31234567
2-2-10、include關鍵字
如果您的整個工程中有多個IDL定義檔案(IDL定義檔案的檔名可以隨便取)。那麼您可以使用include關鍵字,在IDL定義檔案A中,引入一個其他的IDL檔案:
include "other.thrift"1
請注意,一定使用雙引號(不要用成中文的雙引號咯),並且不使用“;”或者“,”結束符。
以上就是IDL基本的語法了,由於篇幅原因不可能把每種語法、每一個細節都講到,但是以上的語法要點已經足夠您編輯一個適應業務的,靈活的IDL定義了。如果您需要了解更詳細的Thrift IDL語法,可以參考官方文件的講述:http://thrift.apache.org/docs/idl
2-3、最簡單的Thrift程式碼定義Thrift中業務介面HelloWorldService.Iface的實現:package testThrift.impl;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.apache.thrift.TException;import testThrift.iface.HelloWorldService.Iface;import testThrift.iface.RESCODE;import testThrift.iface.Reponse;import testThrift.iface.Request;/** * 我們定義了一個HelloWorldService.Iface介面的具體實現。<br> * 注意,這個父級介面:HelloWorldService.Iface,是由thrift的程式碼生成工具生成的<br> * 要執行這段程式碼,請匯入maven-log4j的支援。否則修改LOGGER.info方法 * @author yinwenjie */public class HelloWorldServiceImpl implements Iface { /** * 日誌 */ private static final Log LOGGER = LogFactory.getLog(HelloWorldServiceImpl.class); /** * 在介面定義中,只有一個方法需要實現。<br> * HelloWorldServiceImpl.send(Request request) throws TException <br> * 您可以理解成這個介面的方法接受客戶端的一個Request物件,並且在處理完成後向客戶端返回一個Reponse物件<br> * Request物件和Reponse物件都是由IDL定義的結構,並透過“程式碼生成工具”生成相應的JAVA程式碼。 */ @Override public Reponse send(Request request) throws TException { /* * 這裡就是進行具體的業務處理了。 * */ String json = request.getParamJSON(); String serviceName = request.getServiceName(); HelloWorldServiceImpl.LOGGER.info("得到的json:" + json + " ;得到的serviceName: " + serviceName); // 構造返回資訊 Reponse response = new Reponse(); response.setResponeCode(RESCODE._200); response.setResponseJSON("{\"user\":\"yinwenjie\"}"); return response; } }
各位可以看到,上面一段程式碼中具體業務和過程和普通的業務程式碼沒有任何區別。甚至這段程式碼的實現都不知道自己將被Apache Thrift框架中的客戶端呼叫。
然後我們開始書寫Apache Thrift的伺服器端程式碼:package testThrift.man;import java.util.concurrent.Executors;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.apache.log4j.BasicConfigurator;import org.apache.thrift.TProcessor;import org.apache.thrift.protocol.TBinaryProtocol;import org.apache.thrift.server.TThreadPoolServer;import org.apache.thrift.server.TThreadPoolServer.Args;import org.apache.thrift.transport.TServerSocket;import testThrift.iface.HelloWorldService;import testThrift.iface.HelloWorldService.Iface;import testThrift.impl.HelloWorldServiceImpl;public class HelloBoServerDemo { static { BasicConfigurator.configure(); } /** * 日誌 */ private static final Log LOGGER =LogFactory.getLog(HelloBoServerDemo.class); public static final int SERVER_PORT = 9111; public void startServer() { try { HelloBoServerDemo.LOGGER.info("看到這句就說明thrift服務端準備工作 ...."); // 服務執行控制器(只要是排程服務的具體實現該如何執行) TProcessor tprocessor = new HelloWorldService.Processor<Iface>(new HelloWorldServiceImpl()); // 基於阻塞式同步IO模型的Thrift服務,正式生產環境不建議用這個 TServerSocket serverTransport = new TServerSocket(HelloBoServerDemo.SERVER_PORT); // 為這個伺服器設定對應的IO網路模型、設定使用的訊息格式封裝、設定執行緒池引數 Args tArgs = new Args(serverTransport); tArgs.processor(tprocessor); tArgs.protocolFactory(new TBinaryProtocol.Factory()); tArgs.executorService(Executors.newFixedThreadPool(100)); // 啟動這個thrift服務 TThreadPoolServer server = new TThreadPoolServer(tArgs); server.serve(); } catch (Exception e) { HelloBoServerDemo.LOGGER.error(e); } } /** * @param args */ public static void main(String[] args) { HelloBoServerDemo server = new HelloBoServerDemo(); server.startServer(); }}
以上的程式碼有幾點需要說明:
TBinaryProtocol:這個類程式碼Apache Thrift特有的一種二進位制描述格式。它的特點是傳輸單位資料量所使用的傳輸量更少。Apache Thrift還支援多種資料格式,例如我們熟悉的JSON格式。後文我們將詳細介紹Apache Thrift中的資料格式。tArgs.executorService():是不是覺得這個executorService很熟悉,是的這個就是JAVA JDK 1.5+ 後java.util.concurrent包提供的非同步任務排程服務介面,Java標準執行緒池ThreadPoolExecutor就是它的一個實現。server.serve(),由於是使用的同步阻塞式網路IO模型,所以這個應用程式的主執行緒執行到這句話以後就會保持阻塞狀態了。不過下層網路狀態不出現錯誤,這個執行緒就會一直停在這裡。另外,同HelloWorldServiceImpl 類中的程式碼,請使用Log4j。如果您的測試工程裡面沒有Log4j,請改用System.out。
接下來我們進行最簡單的Apache Thrift Client的程式碼編寫:package testThrift.client;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.apache.log4j.BasicConfigurator;import org.apache.thrift.protocol.TBinaryProtocol;import org.apache.thrift.protocol.TProtocol;import org.apache.thrift.transport.TSocket;import testThrift.iface.HelloWorldService;import testThrift.iface.Reponse;import testThrift.iface.Request;/** * 同樣是基於同步阻塞模型的thrift client。 * @author yinwenjie */public class HelloClient { static { BasicConfigurator.configure(); } /** * 日誌 */ private static final Log LOGGER = LogFactory.getLog(HelloClient.class); public static final void main(String[] args) throws Exception { // 伺服器所在的IP和埠 TSocket transport = new TSocket("127.0.0.1", 9111); TProtocol protocol = new TBinaryProtocol(transport); // 準備呼叫引數 Request request = new Request("{\"param\":\"field1\"}", "\\mySerivce\\queryService"); HelloWorldService.Client client = new HelloWorldService.Client(protocol); // 準備傳輸 transport.open(); // 正式呼叫介面 Reponse reponse = client.send(request); // 一定要記住關閉 transport.close(); HelloClient.LOGGER.info("response = " + reponse); }}
Thrift客戶端所使用的網路IO模型,必須要與Thrift伺服器端所使用的網路IO模型一致。也就是說伺服器端如果使用的是阻塞式同步IO模型,那麼客戶端就必須使用阻塞式同步IO模型。Thrift客戶端所使用的訊息封裝格式,必須要與Thrift伺服器端所使用的訊息封裝格式一直。也就是說伺服器端如果使用的是二進位制流的訊息格式TBinaryProtocol,那麼客戶端就必須同樣使用二進位制劉的訊息格式TBinaryProtocol。其它的程式碼要麼就是由IDL定義並由Thrift的程式碼生成工具生成;要麼就不是重要的程式碼,所以為了節約篇幅就沒有必要再貼出來了。以下是執行效果。伺服器端執行效果伺服器端收到客戶端請求後,取出執行緒池中的執行緒進行執行請注意伺服器端在收到客戶端請求後的執行方式:取出一條執行緒池中的執行緒,並且執行這個服務介面的具體實現。接下來我們馬上介紹Apache Thrift的工作細節。
(接下文)