Apache Thrift - 可伸縮的跨語言服務開發框架

目前流行的服務調用方式有很多種,例如基於 SOAP 消息格式的 Web Service,基於 JSON 消息格式的 RESTful 服務等。其中所用到的數據傳輸方式包括 XML,JSON 等,然而 XML 相對體積太大,傳輸效率低,JSON 體積較小,新穎,但還不夠完善。本文將介紹由 Facebook 開發的遠程服務調用框架 Apache Thrift,它採用接口描述語言定義並創建服務,支持可擴展的跨語言服務開發,所包含的代碼生成引擎可以在多種語言中,如 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk 等創建高效的、無縫的服務,其傳輸數據採用二進制格式,相對 XML 和 JSON 體積更小,對於高併發、大數據量和多語言的環境更有優勢。本文將詳細介紹 Thrift 的使用,並且提供豐富的實例代碼加以解釋說明,幫助使用者快速構建服務。

一個簡單的 Thrift 實例

本文首先介紹一個簡單的 Thrift 實現實例,使讀者能夠快速直觀地瞭解什麼是 Thrift 以及如何使用 Thrift 構建服務。

創建一個簡單的服務 Hello。首先根據 Thrift 的語法規範編寫腳本文件 Hello.thrift,代碼如下:


清單 1. Hello.thrift 
				 
 namespace java service.demo 
 service Hello{ 
  string helloString(1:string para) 
  i32 helloInt(1:i32 para) 
  bool helloBoolean(1:bool para) 
  void helloVoid() 
  string helloNull() 
 } 

其中定義了服務 Hello 的五個方法,每個方法包含一個方法名,參數列表和返回類型。每個參數包括參數序號,參數類型以及參數名。 Thrift 是對 IDL(Interface Definition Language) 描述性語言的一種具體實現。因此,以上的服務描述文件使用 IDL 語法編寫。使用 Thrift 工具編譯 Hello.thrift,就會生成相應的 Hello.java 文件。該文件包含了在 Hello.thrift 文件中描述的服務 Hello 的接口定義,即 Hello.Iface 接口,以及服務調用的底層通信細節,包括客戶端的調用邏輯 Hello.Client 以及服務器端的處理邏輯 Hello.Processor,用於構建客戶端和服務器端的功能。

創建 HelloServiceImpl.java 文件並實現 Hello.java 文件中的 Hello.Iface 接口,代碼如下:


清單 2. HelloServiceImpl.java 
				 
 package service.demo; 
 import org.apache.thrift.TException; 
 public class HelloServiceImpl implements Hello.Iface { 
    @Override 
    public boolean helloBoolean(boolean para) throws TException { 
        return para; 
    } 
    @Override 
    public int helloInt(int para) throws TException { 
        try { 
            Thread.sleep(20000); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
        return para; 
    } 
    @Override 
    public String helloNull() throws TException { 
        return null; 
    } 
    @Override 
    public String helloString(String para) throws TException { 
        return para; 
    } 
    @Override 
    public void helloVoid() throws TException { 
        System.out.println("Hello World"); 
    } 
 } 

創建服務器端實現代碼,將 HelloServiceImpl 作爲具體的處理器傳遞給 Thrift 服務器,代碼如下:


清單 3. HelloServiceServer.java 
				 
 package service.server; 
 import org.apache.thrift.TProcessor; 
 import org.apache.thrift.protocol.TBinaryProtocol; 
 import org.apache.thrift.protocol.TBinaryProtocol.Factory; 
 import org.apache.thrift.server.TServer; 
 import org.apache.thrift.server.TThreadPoolServer; 
 import org.apache.thrift.transport.TServerSocket; 
 import org.apache.thrift.transport.TTransportException; 
 import service.demo.Hello; 
 import service.demo.HelloServiceImpl; 

 public class HelloServiceServer { 
    /** 
     * 啓動 Thrift 服務器
     * @param args 
     */ 
    public static void main(String[] args) { 
        try { 
            // 設置服務端口爲 7911 
            TServerSocket serverTransport = new TServerSocket(7911); 
            // 設置協議工廠爲 TBinaryProtocol.Factory 
            Factory proFactory = new TBinaryProtocol.Factory(); 
            // 關聯處理器與 Hello 服務的實現
            TProcessor processor = new Hello.Processor(new HelloServiceImpl()); 
            TServer server = new TThreadPoolServer(processor, serverTransport, 
                    proFactory); 
            System.out.println("Start server on port 7911..."); 
            server.serve(); 
        } catch (TTransportException e) { 
            e.printStackTrace(); 
        } 
    } 
 } 

創建客戶端實現代碼,調用 Hello.client 訪問服務端的邏輯實現,代碼如下:


清單 4. HelloServiceClient.java 
				 
 package service.client; 
 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; 
 import service.demo.Hello; 

 public class HelloServiceClient { 
 /** 
     * 調用 Hello 服務
     * @param args 
     */ 
    public static void main(String[] args) { 
        try { 
            // 設置調用的服務地址爲本地,端口爲 7911 
            TTransport transport = new TSocket("localhost", 7911); 
            transport.open(); 
            // 設置傳輸協議爲 TBinaryProtocol 
            TProtocol protocol = new TBinaryProtocol(transport); 
            Hello.Client client = new Hello.Client(protocol); 
            // 調用服務的 helloVoid 方法
            client.helloVoid(); 
            transport.close(); 
        } catch (TTransportException e) { 
            e.printStackTrace(); 
        } catch (TException e) { 
            e.printStackTrace(); 
        } 
    } 
 } 

代碼編寫完後運行服務器,再啓動客戶端調用服務 Hello 的方法 helloVoid,在服務器端的控制檯窗口輸出“Hello World”(helloVoid 方法實現在控制檯打印字符串,沒有返回值,所以客戶端調用方法後沒有返回值輸出,讀者可以自己嘗試其他有返回值方法的調用,其結果可以打印在客戶端的控制 臺窗口 )。

Thrift 架構

Thrift 包含一個完整的堆棧結構用於構建客戶端和服務器端。下圖描繪了 Thrift 的整體架構。


圖 1. 架構圖 
Apache Thrift - 可伸縮的跨語言服務開發框架 

如圖所示,圖中黃色部分是用戶實現的業務邏輯,褐色部分是根據 Thrift 定義的服務接口描述文件生成的客戶端和服務器端代碼框架,紅色部分是根據 Thrift 文件生成代碼實現數據的讀寫操作。紅色部分以下是 Thrift 的傳輸體系、協議以及底層 I/O 通信,使用 Thrift 可以很方便的定義一個服務並且選擇不同的傳輸協議和傳輸層而不用重新生成代碼。

Thrift 服務器包含用於綁定協議和傳輸層的基礎架構,它提供阻塞、非阻塞、單線程和多線程的模式運行在服務器上,可以配合服務器 / 容器一起運行,可以和現有的 J2EE 服務器 /Web 容器無縫的結合。

服務端和客戶端具體的調用流程如下:


圖 2. Server 端啓動、服務時序圖( 查看大圖) 
Apache Thrift - 可伸縮的跨語言服務開發框架 

該圖所示是 HelloServiceServer 啓動的過程以及服務被客戶端調用時,服務器的響應過程。從圖中我們可以看到,程序調用了 TThreadPoolServer 的 serve 方法後,server 進入阻塞監聽狀態,其阻塞在 TServerSocket 的 accept 方法上。當接收到來自客戶端的消息後,服務器發起一個新線程處理這個消息請求,原線程再次進入阻塞狀態。在新線程中,服務器通過 TBinaryProtocol 協議讀取消息內容,調用 HelloServiceImpl 的 helloVoid 方法,並將結果寫入 helloVoid_result 中傳回客戶端。


圖 3. Client 端調用服務時序圖( 查看大圖) 
Apache Thrift - 可伸縮的跨語言服務開發框架 

該圖所示是 HelloServiceClient 調用服務的過程以及接收到服務器端的返回值後處理結果的過程。從圖中我們可以看到,程序調用了 Hello.Client 的 helloVoid 方法,在 helloVoid 方法中,通過 send_helloVoid 方法發送對服務的調用請求,通過 recv_helloVoid 方法接收服務處理請求後返回的結果。

數據類型

Thrift 腳本可定義的數據類型包括以下幾種類型:

  • 基本類型:
    • bool:布爾值,true 或 false,對應 Java 的 boolean
    • byte:8 位有符號整數,對應 Java 的 byte
    • i16:16 位有符號整數,對應 Java 的 short
    • i32:32 位有符號整數,對應 Java 的 int
    • i64:64 位有符號整數,對應 Java 的 long
    • double:64 位浮點數,對應 Java 的 double
    • string:未知編碼文本或二進制字符串,對應 Java 的 String
  • 結構體類型:
    • struct:定義公共的對象,類似於 C 語言中的結構體定義,在 Java 中是一個 JavaBean
  • 容器類型:
    • list:對應 Java 的 ArrayList
    • set:對應 Java 的 HashSet
    • map:對應 Java 的 HashMap
  • 異常類型:
    • exception:對應 Java 的 Exception
  • 服務類型:
    • service:對應服務的類

協議

Thrift 可以讓用戶選擇客戶端與服務端之間傳輸通信協議的類別,在傳輸協議上總體劃分爲文本 (text) 和二進制 (binary) 傳輸協議,爲節約帶寬,提高傳輸效率,一般情況下使用二進制類型的傳輸協議爲多數,有時還會使用基於文本類型的協議,這需要根據項目 / 產品中的實際需求。常用協議有以下幾種:

傳輸層

常用的傳輸層有以下幾種:

  • TSocket —— 使用阻塞式 I/O 進行傳輸,是最常見的模式

    使用方法如清單 4 所示。

  • TFramedTransport —— 使用非阻塞方式,按塊的大小進行傳輸,類似於 Java 中的 NIO

    若使用 TFramedTransport 傳輸層,其服務器必須修改爲非阻塞的服務類型,客戶端只需替換清單 4 中 TTransport 部分,代碼如下,清單 9 中 TNonblockingServerTransport 類是構建非阻塞 socket 的抽象類,TNonblockingServerSocket 類繼承 TNonblockingServerTransport



    清單 9. 使用 TFramedTransport 傳輸層構建的 HelloServiceServer.java
    				 
     TNonblockingServerTransport serverTransport; 
     serverTransport = new TNonblockingServerSocket(10005); 
     Hello.Processor processor = new Hello.Processor(new HelloServiceImpl()); 
     TServer server = new TNonblockingServer(processor, serverTransport); 
     System.out.println("Start server on port 10005 ..."); 
     server.serve(); 



    清單 10. 使用 TFramedTransport 傳輸層的 HelloServiceClient.java
    				 
     TTransport transport = new TFramedTransport(new TSocket("localhost", 10005)); 

  • TNonblockingTransport —— 使用非阻塞方式,用於構建異步客戶端

    使用方法請參考 Thrift 異步客戶端構建

服務端類型

常見的服務端類型有以下幾種:

  • TSimpleServer —— 單線程服務器端使用標準的阻塞式 I/O

    代碼如下:



    清單 11. 使用 TSimpleServer 服務端構建的 HelloServiceServer.java
    				 
     TServerSocket serverTransport = new TServerSocket(7911); 
     TProcessor processor = new Hello.Processor(new HelloServiceImpl()); 
     TServer server = new TSimpleServer(processor, serverTransport); 
     System.out.println("Start server on port 7911..."); 
     server.serve(); 

    客戶端的構建方式可參考清單 4。

  • TThreadPoolServer —— 多線程服務器端使用標準的阻塞式 I/O

    使用方法如清單 3 所示。

  • TNonblockingServer —— 多線程服務器端使用非阻塞式 I/O

    使用方法請參考 Thrift 異步客戶端構建

Thrift 異步客戶端構建

Thrift 提供非阻塞的調用方式,可構建異步客戶端。在這種方式中,Thrift 提供了新的類 TAsyncClientManager 用於管理客戶端的請求,在一個線程上追蹤請求和響應,同時通過接口 AsyncClient 傳遞標準的參數和 callback 對象,服務調用完成後,callback 提供了處理調用結果和異常的方法。

首先我們看 callback 的實現:


清單 12.CallBack 的實現:MethodCallback.java 
				 
 package service.callback; 
 import org.apache.thrift.async.AsyncMethodCallback; 

 public class MethodCallback implements AsyncMethodCallback { 
    Object response = null; 

    public Object getResult() { 
        // 返回結果值
        return this.response; 
    } 

    // 處理服務返回的結果值
    @Override 
    public void onComplete(Object response) { 
        this.response = response; 
    } 
    // 處理調用服務過程中出現的異常
    @Override 
    public void onError(Throwable throwable) { 

    } 
 } 

如代碼所示,onComplete 方法接收服務處理後的結果,此處我們將結果 response 直接賦值給 callback 的私有屬性 response。onError 方法接收服務處理過程中拋出的異常,此處未對異常進行處理。

創建非阻塞服務器端實現代碼,將 HelloServiceImpl 作爲具體的處理器傳遞給異步 Thrift 服務器,代碼如下:


清單 13.HelloServiceAsyncServer.java 
				 
 package service.server; 
 import org.apache.thrift.server.TNonblockingServer; 
 import org.apache.thrift.server.TServer; 
 import org.apache.thrift.transport.TNonblockingServerSocket; 
 import org.apache.thrift.transport.TNonblockingServerTransport; 
 import org.apache.thrift.transport.TTransportException; 
 import service.demo.Hello; 
 import service.demo.HelloServiceImpl; 

 public class HelloServiceAsyncServer { 
    /** 
     * 啓動 Thrift 異步服務器
     * @param args 
     */ 
    public static void main(String[] args) { 
        TNonblockingServerTransport serverTransport; 
        try { 
            serverTransport = new TNonblockingServerSocket(10005); 
            Hello.Processor processor = new Hello.Processor( 
                    new HelloServiceImpl()); 
            TServer server = new TNonblockingServer(processor, serverTransport); 
            System.out.println("Start server on port 10005 ..."); 
            server.serve(); 
        } catch (TTransportException e) { 
            e.printStackTrace(); 
        } 
    } 
 } 

HelloServiceAsyncServer 通過 java.nio.channels.ServerSocketChannel 創建非阻塞的服務器端等待客戶端的連接。

創建異步客戶端實現代碼,調用 Hello.AsyncClient 訪問服務端的邏輯實現,將 MethodCallback 對象作爲參數傳入調用方法中,代碼如下:


清單 14.HelloServiceAsyncClient.java 
				 
 package service.client; 
 import java.io.IOException; 
 import org.apache.thrift.async.AsyncMethodCallback; 
 import org.apache.thrift.async.TAsyncClientManager; 
 import org.apache.thrift.protocol.TBinaryProtocol; 
 import org.apache.thrift.protocol.TProtocolFactory; 
 import org.apache.thrift.transport.TNonblockingSocket; 
 import org.apache.thrift.transport.TNonblockingTransport; 
 import service.callback.MethodCallback; 
 import service.demo.Hello; 

 public class HelloServiceAsyncClient { 
    /** 
     * 調用 Hello 服務
     * @param args 
     */ 
    public static void main(String[] args) throws Exception { 
        try { 
            TAsyncClientManager clientManager = new TAsyncClientManager(); 
            TNonblockingTransport transport = new TNonblockingSocket( 
                    "localhost", 10005); 
            TProtocolFactory protocol = new TBinaryProtocol.Factory(); 
            Hello.AsyncClient asyncClient = new Hello.AsyncClient(protocol, 
                    clientManager, transport); 
            System.out.println("Client calls ....."); 
            MethodCallback callBack = new MethodCallback(); 
            asyncClient.helloString("Hello World", callBack); 
            Object res = callBack.getResult(); 
            while (res == null) { 
                res = callBack.getResult(); 
            } 
            System.out.println(((Hello.AsyncClient.helloString_call) res) 
                    .getResult()); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
  } 
 } 

HelloServiceAsyncClient 通過 java.nio.channels.Socketchannel 創建異步客戶端與服務器建立連接。在本文中異步客戶端通過以下的循環代碼實現了同步效果,讀者可去除這部分代碼後再運行對比。


清單 15. 異步客戶端實現同步效果代碼段 
Object res = callBack.getResult();
// 等待服務調用後的返回結果
while (res == null) {
   res = callBack.getResult();
}

通過與清單 9 和清單 10 的代碼比較,我們可以構建一個 TNonblockingServer 服務類型的服務端,在客戶端構建一個 TFramedTransport 傳輸層的同步客戶端和一個 TNonblockingTransport 傳輸層的異步客戶端,那麼一個服務就可以通過一個 socket 端口提供兩種不同的調用方式。有興趣的讀者可以嘗試一下。

常見問題

NULL 問題

我們在對服務的某個方法調用時,有時會出現該方法返回 null 值的情況,在 Thrift 中,直接調用一個返回 null 值的方法會拋出 TApplicationException 異常。在清單 2 中,HelloServiceImpl 裏實現了 helloNull 方法,返回 null 值,我們在 HelloServiceClient.java 中加入調用該方法的代碼,出現如下圖所示的異常:


圖 4. TApplicationException 異常 
Apache Thrift - 可伸縮的跨語言服務開發框架 

爲了處理返回 null 值情況,我們要捕獲該異常,並進行相應的處理,具體客戶端代碼實現如下:


清單 16. 處理服務返回值爲 null 的代碼 
				 
 package service.client; 
 import org.apache.thrift.TApplicationException; 
 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; 
 import service.demo.Hello; 

 public class HelloServiceClient { 
    /** 
     * 調用 Hello 服務,並處理 null 值問題
     * @param args 
     */ 
    public static void main(String[] args) { 
        try { 
            TTransport transport = new TSocket("localhost", 7911); 
            transport.open(); 
            TProtocol protocol = new TBinaryProtocol(transport); 
            Hello.Client client = new Hello.Client(protocol); 
            System.out.println(client.helloNull()); 
            transport.close(); 
        } catch (TTransportException e) { 
            e.printStackTrace(); 
        } catch (TException e) { 
            if (e instanceof TApplicationException 
                    && ((TApplicationException) e).getType() ==   
                                 TApplicationException.MISSING_RESULT) { 
                System.out.println("The result of helloNull function is NULL"); 
            } 
        } 
    } 
 } 

調用 helloNull 方法後,會拋出 TApplicationException 異常,並且異常種類爲 MISSING_RESULT,本段代碼顯示,捕獲該異常後,直接在控制檯打印“The result of helloNull function is NULL”信息。

安裝部署

Apache Thrift 的項目主頁爲:http://thrift.apache.org/tutorial/,具體安裝步驟如下:

  1. 下載 thrift 源文件(http://svn.apache.org/repos/asf/thrift/tags/thrift-0.6.1/)
  2. 將 thrift 源文件導入 eclipse,進入 /lib/java 目錄,使用 ant 編譯 build.xml 獲得 libthrift-0.6.1-snapshot.jar
  3. 將 libthrift-0.6.1-snapshot.jar、slf4j-api-1.5.8.jar、slf4j-log4j12-1.5.8.jar 和 log4j-1.2.14.jar 導入 eclipse 開發環境
  4. 下載 thrift 編譯工具,該工具可將 thrift 腳本文件編譯成 java 文件,下載地址:http://apache.etoak.com//thrift/0.6.0/thrift-0.6.1.exe
  5. 創建 Hello.thrift 腳本文件,具體代碼如上一章節所述,進入 thrift-0.6.1.exe 所在目錄,執行命令"thrift-0.6.1.exe -gen java x:\Hello.thrift",在當前運行盤符下,可看見 gen-java 目錄,進入目錄可看到生成的 Java 代碼。更多 thrift 的命令內容,請參考 thrift 自帶的 help 命令
  6. 編寫服務端和客戶端代碼,完成 thrift 的安裝和部署

基於 Apache Thrift 框架生成的服務包括客戶端和服務器端,具體的部署模式如下所示:


圖 5. 部署圖 
Apache Thrift - 可伸縮的跨語言服務開發框架 

從圖中我們可以看到,客戶端和服務器端部署時,需要用到公共的 jar 包和 java 文件,如圖“Common file”區域,其中 Hello.java 由 Hello.thrift 編譯而來。在服務器端,服務必須實現 Hello.Iface 接口,同時要包括服務器的啓動代碼 HelloServiceServer.java。在客戶端,包括客戶端調用服務的代碼 HelloServiceClient.java。客戶端和服務器通過 Hello.java 提供的 API 實現遠程服務調用。

總結

本文介紹了 Apache Thrift 的安裝部署和架構,並通過大量實例介紹了在不同情況下如何使用 Apache Thrift 來構建服務,同時着重介紹了 Thrift 異步客戶端的構建,希望能給讀者帶來一些幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章