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 的使用,而且提供豐富的實例代碼加以解釋說明,幫助使用者快速構建服務。 java


一個簡單的 Thrift 實例

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

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

清單 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 方法實如今控制檯打印字符串,沒有返回值,因此客戶端調用方法後沒有返回值輸出,讀者能夠本身嘗試其餘有返回值方法的調用,其結果能夠打印在客戶端的控制檯窗口 )。 框架

回頁首 eclipse

Thrift 架構

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

圖 1. 架構圖
圖 1. 架構圖

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

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

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

圖 2. Server 端啓動、服務時序圖(查看大圖
圖 2. Server 端啓動、服務時序圖

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

圖 3. Client 端調用服務時序圖(查看大圖
圖 3. Client 端調用服務時序圖

該圖所示是 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) 傳輸協議,爲節約帶寬,提升傳輸效率,通常狀況下使用二進制類型的傳輸協議爲多數,有時還會使用基於文本類型的協議,這須要根據項目 / 產品中的實際需求。經常使用協議有如下幾種:

  • TBinaryProtocol —— 二進制編碼格式進行數據傳輸

    使用方法如清單 3 和清單 4 所示。

  • TCompactProtocol —— 高效率的、密集的二進制編碼格式進行數據傳輸

    構建 TCompactProtocol 協議的服務器和客戶端只需替換清單 3 和清單 4 中 TBinaryProtocol 協議部分便可,替換成以下代碼:

    清單 5. 使用 TCompactProtocol 協議構建的 HelloServiceServer.java
    TCompactProtocol.Factory proFactory = new TCompactProtocol.Factory();
    清單 6. 使用 TCompactProtocol 協議的 HelloServiceClient.java
    TCompactProtocol protocol = new TCompactProtocol(transport);
  • TJSONProtocol —— 使用 JSON 的數據編碼協議進行數據傳輸

    構建 TJSONProtocol 協議的服務器和客戶端只需替換清單 3 和清單 4 中 TBinaryProtocol 協議部分便可,替換成以下代碼:

    清單 7. 使用 TJSONProtocol 協議構建的 HelloServiceServer.java
    TJSONProtocol.Factory proFactory = new TJSONProtocol.Factory();
    清單 8. 使用 TJSONProtocol 協議的 HelloServiceClient.java
    TJSONProtocol protocol = new TJSONProtocol(transport);
  • TSimpleJSONProtocol —— 只提供 JSON 只寫的協議,適用於經過腳本語言解析

回頁首

傳輸層

經常使用的傳輸層有如下幾種:

  • 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 異常
圖 4. TApplicationException 異常

爲了處理返回 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. 部署圖
圖 5. 部署圖

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

回頁首

總結

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

相關文章
相關標籤/搜索