本人是個初入互聯網行業的菜鳥,本篇博客純屬個人記錄學習筆記,以方便以后查找復習,其中部分內容是粘貼他人博客的內容,如有冒犯請見諒!
一、thrift 簡介
它是一款RPC通信框架,采用C/S架構,且擁有高效的序列化機制。要使用Thrift,首先我們需要在遠端服務器上開啟Thrift服務,之后,服務器端進程保持睡眠狀態,直到客戶端代碼的調用。
Thrift應用廣泛的一個主要原因是它支持多種主流的語言,且使用它的用戶不需要關注服務器和客戶端是怎樣實現通信,怎樣實現序列化的,只需要去考慮怎樣實現自己需要的業務邏輯。
Thrift使用接口語言定義數據結構和服務,包含了最常用的數據類型,並一一對應各種語言的基本類型,還可以定義枚舉和異常等等。
1.1、 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為服務器端程序提供了很多的工作模式,例如:線程池模型、非阻塞模型等等,可以根據自己的實際應用場景選擇一種工作模式高效地對外提供服務;
1.2、 Thrift的相關網址和資料:
(1) Thrift的官方網站:http://thrift.apache.org/
(2) Thrift官方下載地址:http://thrift.apache.org/download
(3) Thrift官方的IDL示例文件(自己寫IDL文件時可以此為參考):
https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob_plain;f=test/ThriftTest.thrift;hb=HEAD
(4) 各種環境下搭建Thrift的方法:
http://thrift.apache.org/docs/install/
該頁面中共提供了CentOS\Ubuntu\OS X\Windows幾種環境下的搭建Thrift環境的方法。
二、thrift 環境搭建
1.1 windows 環境搭建
thrift.exe下載地址:http://archive.apache.org/dist/thrift/0.9.0/,版本可自選。
下載*.exe后,新建文件夾thrift,將*.exe放入文件夾,將文件夾的路徑添加到環境變量。(說明:文件夾名稱隨意,只是為了方便后期操作起名為thrift)
打開cmd窗口,輸入thrift -version,顯示如下圖說明安裝成功:
1.2 java環境搭建
在java環境下開發thrift的客戶端或者服務器程序非常簡單,只需在工程文件中加上下面三個jar包
需要下載:
libthrift.jar 、slf4j-api.jar 、slf4j-simple.jar
注意:libthrift.jar的版本要與windows環境變量添加的*.exe版本一致,否則運行時會出現錯誤。
下面以IDEA為例,說明如何添加jar:
file → Project Structure
Modules → “+” → “JARs or Directories”
1.3 python 環境搭建
安裝thrift:pip install thrift
卸載thrift:pip uninstall thrift
安裝特定版本的package
通過使用==, >=, <=, >, <來指定一個版本號。
$ pip install 'thrift<2.0'
$ pip install 'thrift>2.0,<2.0.3'
三、使用thrift
Thrift把它定義的相當簡潔,以致於我們的使用過程也是異常的方便,簡單來說,使用Thrift的過程只是需要以下的四個步驟:
1.設計交互式的數據格式(strucy、enum等)和具體服務(servce),定義thrift接口描述文件。
2.利用thrift工具,根據之前定義的接口文件生成目標語言文件。
3.實現服務代碼,並把實現的業務邏輯定義為thrift服務器的處理層,選擇端口,服務器啟動監聽,等待客戶端的連接請求。
4.客戶端使用相同的端口連接服務器請求服務
下面簡單的介紹下thrift接口描述語言(IDL)的類型:
IDL包含基礎類型、結構、容器、異常和服務這樣幾種類型:
基礎類型 : 包括了 bool,byte、i16,i32,i64,double,string,每一種都對應各種語言的基礎類型
結構 : 在thrift中定義為struct,它類似於C語言中的結構體,是基礎類型的集合體,每一個結構都會生成一個單獨的類,在java中類似於class
容器 : thrift中定義了常用的三種容器 – list,set,map,在Java中各自的對應實現是 ArrayList、HashSet、HashMap,其中模板類型可以是基礎類型或者結構類型
異常 : 異常的定義類似於struct,只是換成了exception
服務 : 服務類似於java中的接口,需要對服務中的每一個方法簽名定義返回類型、參數聲明、拋出的異常,對於方法拋出的異常,除了自己聲明的之外,每個方法還都會拋出TException,對於返回值是void類型的方法,我們可以在方法簽名的前面加上oneway標識符,將這個方法標記為異步的模式,即調用之后會立即返回
為了更好的理解thrift,下面按照如上四個步驟,給出一個實例(java語言):
1.定義接口描述文件(.thrift)
定義服務描述文件 test_server.thrift
1 namespace java main 2 3 include "thrift_datatype.thrift" 4 service TestThriftService 5 { 6 /** 7 *value 中存放兩個字符串拼接之后的字符串 8 */ 9 thrift_datatype.ResultStr getStr(1:string srcStr1,2:string srcStr2), 10 11 thrift_datatype.ResultInt getInt(1:i32 val) 12 13 }
定義數據結構描述文件 thrift_datatype.thrift
namespace java main /**為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 /*內部錯誤*/ } /*int類型返回結果*/ struct ResultInt { 1: ThriftResult result, 2: i32 value } /*String類型返回結果*/ struct ResultStr { 1: ThriftResult result, 2: string value }
編寫IDL時需要注意的問題:
[1]函數的參數要用數字依序標好,序號從1開始,形式為:“序號:參數名”;
[2]每個函數的最后要加上“,”,最后一個函數不加;
[3]在IDL中可以使用/*……*/添加注釋
[4]枚舉類型里沒有序號
[5]struct中可以設置默認值
2.使用thrift工具利用IDL生成目標代碼
使用命令:thrift --gen <language> <Thrift filename>
注意:使用該命令時智能編譯一個thrift文件,所以多個thrift文件時需要依次編譯。
編譯完成時生成如下目錄及文件:
目錄是由接口定義文件中,命名空間 namespace定義的。TestThriftService.java
3.實現服務業務邏輯並開始服務監聽
實現業務邏輯只需要實現TestThriftService.java的接口,業務實現的邏輯文件是:TestThriftServiceImpl.java
package main; import org.apache.thrift.TException; public class TestThriftServiceImpl implements TestThriftService.Iface { @Override public ResultStr getStr(String srcStr1, String srcStr2) throws TException { long startTime = System.currentTimeMillis(); //System.out.println("test"); String res = srcStr1 + srcStr2; long stopTime = System.currentTimeMillis(); System.out.println("[getStr]time interval: " + (stopTime-startTime)); ResultStr ret = new ResultStr(ThriftResult.SUCCESS,res); return ret; } @Override public ResultInt 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)); ResultInt rett = new ResultInt(ThriftResult.NO_CONTENT,res); return rett; } }
接下來,我們服務器端需要做最后一步工作,開啟服務器端的監聽
package main; 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 = 12358; private static TestThriftServiceImpl m_myService = new TestThriftServiceImpl(); private static TServer m_server = null; 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); } } }
在服務器端啟動thrift框架的部分代碼比較簡單,不過在寫這些啟動代碼之前需要先確定服務器采用哪種工作模式對外提供服務,Thrift對外提供幾種工作模式,例如:TSimpleServer、TNonblockingServer、TThreadPoolServer、TThreadedSelectorServer等模式,每種服務模式的通信方式不一樣,因此在服務啟動時使用了那種服務模式,客戶端程序也需要采用對應的通信方式。
另外,Thrift支持多種通信協議格式:TCompactProtocol、TBinaryProtocol、TJSONProtocol等,因此,在使用Thrift框架時,客戶端程序與服務器端程序所使用的通信協議一定要一致,否則便無法正常通信。
以上述代碼采用的TNonblockingServer為例,說明服務器端如何使用Thrift框架,在服務器端創建並啟動Thrift服務框架的過程為:
[1]為自己的服務實現類定義一個對象,如代碼中的:
TestThriftServiceImpl m_myService =newTestThriftServiceImpl();
這里的TestThriftServiceImpl類就是代碼中我們自己定義的服務器端對各服務接口的實現類。
[2]定義一個TProcess對象,在根據Thrift文件生成java源碼接口文件TestThriftService.java中,Thrift已經自動為我們定義了一個Processor;后續節中將對這個TProcess類的功能進行詳細描述;如代碼中的:
TProcessor tProcessor = NewTestThriftService.Processor<TestThriftService.Iface>(m_myService);
[3]定義一個TNonblockingServerSocket對象,用於tcp的socket通信,如代碼中的:
TNonblockingServerSocketnioSocket = newTNonblockingServerSocket(m_thriftPort);
在創建server端socket時需要指明監聽端口號,即上面的變量:m_thriftPort。
[4]定義TNonblockingServer所需的參數對象TNonblockingServer.Args;並設置所需的參數,如代碼中的:
[5]定義TNonblockingServer對象,並啟動該服務,如代碼中的:
m_server = new TNonblockingServer(tnbArgs);
…
m_server.serve();
4.客戶端連接服務器請求服務
客戶端的實現也非常的簡單,我們只需要獲得一個thrift為我們定義好的Client,然后調用需要的業務邏輯就可以了
package Client; import main.ResultInt; import main.ResultStr; import org.apache.thrift.TException; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.TFramedTransport; import org.apache.thrift.transport.TSocket; import main.TestThriftService; import java.util.logging.Logger; public class testClient { public static void main(String[] args) { TFramedTransport m_transport = new TFramedTransport(new TSocket("localhost", 12358, 5000)); TProtocol protocol = new TBinaryProtocol(m_transport); TestThriftService.Client testClient = new TestThriftService.Client(protocol); //System.out.println("test"); try { m_transport.open(); ResultStr res = testClient.getStr("test1","test2"); ResultInt ret = testClient.getInt(5); System.out.println("ret = " + ret); System.out.println("status = " + res.result + ' ' + "value = " + res.value); m_transport.close(); //System.out.println("test3"); } catch (TException e){ // TODO Auto-generated catch block e.printStackTrace(); } } }
這樣,我們就完成了thrift過程的四個步驟,接下來,可以開始測試RPC過程了,首先,我們需要運行服務器端代碼,會看到控制台會打印出一條輸出:Start server on port 12358,之后,運行客戶端代碼,等待客戶端進程終結,我們回到服務器端的控制台,可以看到業務邏輯中定義的輸出。
如下是python寫的客戶端請求:
# -*- coding: utf-8 -*- from thrift.transport import TSocket from test_service import TestThriftService from test_service.constants import * def get(): transport = TSocket.TSocket("localhost",12358) mytransport = TTransport.TFramedTransport(transport) mytransport.open() protocol = TBinaryProtocol.TBinaryProtocol(mytransport) client = TestThriftService.Client(protocol) ret1 = client.getStr("test1","test2") #print "eeeee" ret2 = client.getInt(7) print ret1 print ret2 if __name__=="__main__": get()
Thrift整體架構
其實寫這個部分難免有些心有余而力不足,這個部分是整個thrift框架的組成,我對它的理解也只是基礎中的基礎,不過,由於是學習筆記,還是記錄在這里吧。
Thrift是由四層架構組成的,這樣設計的優點是可以自由的選擇每一層的實現方式應對不同的服務需求,比如我在上面的例子中服務器端采用的是單線程阻塞式IO模型(這個只是Thrift實現的玩具,生產過程不可能會使用這種服務模式),你也可以根據需要換成其他的實現模型,而且代碼部分的變動也是微乎其微的,分離的架構設計使得每一層之間都是透明的,不用考慮底層的實現,只需要一個接口就可以完成調用。下面,我將從最底層開始粗略的介紹Thrift中的每一層。
-
TTransport層
傳輸層使用TCP、Http等協議實現,它包含了各種socket調用中的方法,如open,close,read,write。由於是框架中的最后一層,所以,最重要的實現部分當然是數據的讀出和寫入(read 和 write),它有阻塞和非阻塞的實現方式。 -
TProtocol層
協議層是定義數據會以怎樣的形式到達傳輸層。它首先對IDL中的各個數據結構進行了定義,且對每一種類型都定義了read和write方法。我們需要在服務器端和客戶端聲明相同的實現協議來作為內存和網絡傳輸格式之間的映射。
常用的協議有 TBinaryProtocol:它定義了數據會以二進制的形式傳輸,它是最簡單的實現協議,同時也是最常用的實現協議,非常的高效;TCompactProtocol:它的名字叫做壓縮二進制協議,與TBinaryProtocol相比,它會采用壓縮算法對數據進行再壓縮,減少實際傳輸的數據量,提高傳輸效率。 -
TProcessor層
處理層就是服務器端定義的處理業務邏輯,它的主要代碼是**Service.java文件中的Iface接口和Processor類。
Iface接口:這個接口中的所有方法都是用戶定義在IDL文件中的service的方法,它需要拋出TException這個檢查異常,服務器端需要定義相應的實現類去 implements **.Iface 接口,完成服務的業務邏輯。
Processor類:這個類中定義了一個processMap,里面包含了service中定義的方法,服務器端在構造這個Processor對象的時候,唯一需要做的就是把實現service(Iface)的對象作為參數傳遞給Processor的構造函數。 -
Server層
server是Thrift框架中的最高層,它創建並管理下面的三層,同時提供了客戶端調用時的線程調度邏輯。
服務層的基類是TServer,它相當於一個容器,里面包含了TProcessor,TTransport,TProtocol,並實現對它們的管理和調度。TServer有多種實現方式,對於本例中使用的是TSimpleServer,這是一個單線程阻塞式IO模型,實際的生產中大多用到的是TThreadSelectorServer – 多線程非阻塞式IO模型。
thrift調用過程
Thrift調用過程中,Thrift客戶端和服務器之間主要用到傳輸層類、協議層類和處理類三個主要的核心類,這三個類的相互協作共同完成rpc的整個調用過程。在調用過程中將按照以下順序進行協同工作:
(1) 將客戶端程序調用的函數名和參數傳遞給協議層(TProtocol),協議層將函數名和參數按照協議格式進行封裝,然后封裝的結果交給下層的傳輸層。此處需要注意:要與Thrift服務器程序所使用的協議類型一樣,否則Thrift服務器程序便無法在其協議層進行數據解析;
(2) 傳輸層(TTransport)將協議層傳遞過來的數據進行處理,例如傳輸層的實現類TFramedTransport就是將數據封裝成幀的形式,即“數據長度+數據內容”,然后將處理之后的數據通過網絡發送給Thrift服務器;此處也需要注意:要與Thrift服務器程序所采用的傳輸層的實現類一致,否則Thrift的傳輸層也無法將數據進行逆向的處理;
(3) Thrift服務器通過傳輸層(TTransport)接收網絡上傳輸過來的調用請求數據,然后將接收到的數據進行逆向的處理,例如傳輸層的實現類TFramedTransport就是將“數據長度+數據內容”形式的網絡數據,轉成只有數據內容的形式,然后再交付給Thrift服務器的協議類(TProtocol);
(4) Thrift服務端的協議類(TProtocol)將傳輸層處理之后的數據按照協議進行解封裝,並將解封裝之后的數據交個Processor類進行處理;
(5) Thrift服務端的Processor類根據協議層(TProtocol)解析的結果,按照函數名找到函數名所對應的函數對象;
(6) Thrift服務端使用傳過來的參數調用這個找到的函數對象;
(7) Thrift服務端將函數對象執行的結果交給協議層;
(8) Thrift服務器端的協議層將函數的執行結果進行協議封裝;
(9) Thrift服務器端的傳輸層將協議層封裝的結果進行處理,例如封裝成幀,然后發送給Thrift客戶端程序;
(10) Thrift客戶端程序的傳輸層將收到的網絡結果進行逆向處理,得到實際的協議數據;
(11) Thrift客戶端的協議層將數據按照協議格式進行解封裝,然后得到具體的函數執行結果,並將其交付給調用函數;
上述過程如圖4.1所示: