Thrift 簡易入門與實戰

簡介

thrift是一個軟件框架, 用來進行可擴展且跨語言的服務的開發. 它結合了功能強大的軟件堆棧和代碼生成引擎, 以構建在 C++, Java, Go,Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 這些編程語言間無縫結合的、高效的服務.
官網地址: thrift.apache.orgjava

安裝

Thrift 的安裝比較簡單, 在 Mac 下能夠直接使用 brew 快速安裝.git

brew install thrift

Window 或 Linux 能夠經過官網 下載, 這裏就再也不多說了.github

當下載安裝完畢後, 咱們就會獲得一個名爲 thrift (Window 下是 thrift.exe) 的工具, 經過它就能夠生成各個語言的 thrift 代碼.apache

基礎

數據類型

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

union 類型

和 C/C++ 中的 union 相似.

容器類型:

  • list: 對應 Java 的 ArrayList

  • set: 對應 Java 的 HashSet

  • map: 對應 Java 的 HashMap

exception 類型

對應 Java 的 Exception

service 類型

對應服務的類.

service 類型能夠被繼承, 例如:

service PeopleDirectory {
   oneway void log(1: string message),
   void reloadDatabase()
}
service EmployeeDirectory extends PeopleDirectory {
   Employee findEmployee(1: i32employee_id) throws (1: MyError error),
   bool createEmployee(1: Employee new_employee)
}

注意到, 在定義 PeopleDirectory 服務的 log 方法時, 咱們使用到了 oneway 關鍵字, 這個關鍵字的做用是告訴 thrift, 咱們不關心函數的返回值, 不須要等待函數執行完畢就能夠直接返回.
oneway 關鍵字一般用於修飾無返回值(void)的函數, 可是它和直接的無返回值的函數仍是有區別的, 例如上面的 log 函數和 reloadDatabase 函數, 當客戶端經過 thrift 進行遠程調用服務端的 log 函數時, 不須要等待服務端的 log 函數執行結束就能夠直接返回; 可是當客戶端調用 reloadDatabase 方法時, 雖然這個方法也是無返回值的, 但客戶端必需要阻塞等待, 直到服務端通知客戶端此調用已結束後, 客戶端的遠程調用才能夠返回.

枚舉類型

和 Java 中的 enum 類型同樣, 例如:

enum Fruit {
    Apple,
    Banana,
}

例子

下面是一個在 IDL 文件中使用各類類型的例子:

enum ResponseStatus {
  OK = 0,
  ERROR = 1,
}

struct ListResponse {
  1: required ResponseStatus status,
  2: optional list<i32> ids,
  3: optional list<double> scores,
  10: optional string strategy,
  11: optional string globalId,
  12: optional map<string, string> extraInfo,
}

service Hello {
    string helloString(1:string para)
    i32 helloInt(1:i32 para)
    bool helloBoolean(1:bool para)
    void helloVoid()
    string helloNull()
}

關於 IDL 文件

所謂 IDL, 即 接口描述語言, 在使用 thrift 前, 須要提供一個 .thrift 後綴的文件, 其內容是使用 IDL 描述的服務接口信息.
例如以下的一個接口描述:

namespace java com.xys.thrift

service HelloWorldService {
    string sayHello(string name);
}

這裏咱們定義了一個名爲 HelloWorldService 的接口, 它有一個方法, 即 sayHello. 當經過 thrift --gen java test.thrift 來生成 thrift 接口服務時, 會產生一個 HelloWorldService.java 的文件, 在此文件中會定義一個 HelloWorldService.Iface 接口, 咱們在服務器端實現此接口便可.

服務器端編碼基本步驟

  • 實現服務處理接口 impl

  • 建立 Processor

  • 建立 Transport

  • 建立 Protocol

  • 建立 Server

  • 啓動 Server

例如:

public class HelloServer {
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws Exception {
        HelloServer server = new HelloServer();
        server.startServer();
    }

    public void startServer() throws Exception {
        // 建立 TProcessor
        TProcessor tprocessor = 
                new HelloWorldService.Processor<HelloWorldService.Iface>(new HelloWorldImpl());

        // 建立 TServerTransport, TServerSocket 繼承於 TServerTransport
        TServerSocket serverTransport = new TServerSocket(SERVER_PORT);
        
        // 建立 TProtocol
        TProtocolFactory protocolFactory = new TBinaryProtocol.Factory();
        
        TServer.Args tArgs = new TServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.protocolFactory(protocolFactory);

        // 建立 TServer
        TServer server = new TSimpleServer(tArgs);
        // 啓動 Server
        server.serve();
    }
}

客戶端編碼基本步驟

  • 建立 Transport

  • 建立 Protocol

  • 基於 Potocol 建立 Client

  • 打開 Transport

  • 調用服務相應的方法.

public class HelloClient {
    public static final String SERVER_IP = "localhost";
    public static final int SERVER_PORT = 8080;
    public static final int TIMEOUT = 30000;

    public static void main(String[] args) throws Exception {
        HelloClient client = new HelloClient();
        client.startClient("XYS");
    }

    public void startClient(String userName) throws Exception {
        // 建立 TTransport
        TTransport transport = new TSocket(SERVER_IP, SERVER_PORT, TIMEOUT);
        // 建立 TProtocol
        TProtocol protocol = new TBinaryProtocol(transport);

        // 建立客戶端.
        HelloWorldService.Client client = new HelloWorldService.Client(protocol);
        
        // 打開 TTransport
        transport.open();
        
        // 調用服務方法
        String result = client.sayHello(userName);
        System.out.println("Result: " + result);

        transport.close();
    }
}

Thrift 的網絡棧

clipboard.png

如上圖所示, thrift 的網絡棧包含了 transport 層, protocol 層, processor 層和 Server/Client 層.

Transport 層

Transport 層提供了從網絡中讀取數據或將數據寫入網絡的抽象.
Transport 層和 Protocol 層相互獨立, 咱們能夠根據本身須要選擇不一樣的 Transport 層, 而對上層的邏輯不形成任何影響.

Thrift 的 Java 實現中, 咱們使用接口 TTransport 來描述傳輸層對象, 這個接口提供的經常使用方法有:

open
close
read
write
flush

而在服務器端, 咱們一般會使用 TServerTransport 來監聽客戶端的請求, 並生成相對應的 Transport 對象, 這個接口提供的經常使用方法有:

open
listen
accept
close

爲了使用上的方便, Thrift 提供了以下幾個經常使用 Transport:

  • TSocket: 這個 transport 使用阻塞 socket 來收發數據.

  • TFramedTransport: 以幀的形式發送數據, 每幀前面是一個長度. 當服務方使用 non-blocking IO 時(即服務器端使用的是 TNonblockingServerSocket), 那麼就必須使用 TFramedTransport.

  • TMemoryTransport: 使用內存 I/O. Java 實現中在內部使用了 ByteArrayOutputStream

  • TZlibTransport: 使用 Zlib 壓縮傳輸的數據. 在 Java 中未實現.

Protocol 層(數據傳輸協議層)

這一層的做用是內存中的數據結構轉換爲可經過 Transport 傳輸的數據流或者反操做, 即咱們所謂的 序列化反序列化.

經常使用的協議有:

  • TBinaryProtocol, 二進制格式

  • TCompactProtocol, 壓縮格式

  • TJSONProtocol, JSON 格式

  • TSimpleJSONProtocol, 提供 JSON 只寫協議, 生成的文件很容易經過腳本語言解析.

  • TDebugProtocoal, 使用人類可讀的 Text 格式, 幫助調試

注意, 客戶端和服務器的協議要同樣.

Processor 層

Processor 層對象由 Thrift 根據用戶的 IDL 文件所生成, 咱們一般不能隨意指定.
這一層主要有兩個功能:

  • 從 Protocol 層讀取數據, 而後轉交給對應的 handler 處理

  • 將 handler 處理的結構發送 Prootcol 層.

Server 層

Thrift 提供的 Server 層實現有:

  • TNonblockingServer: 這個是一個基於多線程, 非阻塞 IO 的 Server 層實現, 它專門用於處理大量的併發請求的

  • THsHaServer: 辦同步/半異步服務器模型, 基於 TNonblockingServer 實現.

  • TThreadPoolServer: 基於多線程, 阻塞 IO 的 Server 層實現, 它所消耗的系統資源比 TNonblockingServer 高, 不過能夠提供更高的吞吐量.

  • TSimpleServer: 這個實現主要是用於測試目的. 它只有一個線程, 而且是阻塞 IO, 所以在同一時間只能處理一個鏈接.

使用例子

下面的例子在個人 Github 上有源碼, 直接 clone 便可.

依賴

<dependency>
    <groupId>org.apache.thrift</groupId>
    <artifactId>libthrift</artifactId>
    <version>0.10.0</version>
</dependency>

thrift 版本: 0.10.0
注意, jar 包的版本須要和 thrift 版本一致, 否則可能會有一些編譯錯誤

thrift 文件

test.thrift

namespace java com.xys.thrift

service HelloWorldService {
    string sayHello(string name);
}

編譯

cd src/main/resources/
thrift --gen java test.thrift
mv gen-java/com/xys/thrift/HelloWorldService.java ../java/com/xys/thrift

當執行 thrift --gen java test.thrift 命令後, 會在當前目錄下生成一個 gen-java 目錄, 其中會以包路徑格式存放着生成的服務器端 thrift 代碼, 咱們將其拷貝到工程對應的目錄下便可.

服務實現

public class HelloWorldImpl implements HelloWorldService.Iface {
    public HelloWorldImpl() {
    }

    @Override
    public String sayHello(String name) throws TException {
        return "Hello, " + name;
    }
}

服務端/客戶端實現

下面咱們分別根據服務器端的幾種不一樣類型, 來分別實現它們, 並對比這些模型的異同點.

TSimpleServer 服務器模型

TSimpleServer 是一個簡單的服務器端模型, 它只有一個線程, 而且使用的是阻塞 IO 模型, 所以通常用於測試環境中.

服務器端實現
public class SimpleHelloServer {
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws Exception {
        SimpleHelloServer server = new SimpleHelloServer();
        server.startServer();
    }

    public void startServer() throws Exception {
        TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>(
                new HelloWorldImpl());

        TServerSocket serverTransport = new TServerSocket(SERVER_PORT);
        TSimpleServer.Args tArgs = new TSimpleServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.protocolFactory(new TBinaryProtocol.Factory());

        TServer server = new TSimpleServer(tArgs);

        server.serve();
    }
}

咱們在服務器端的代碼中, 沒有顯示地指定 Transport 的類型, 這個是由於 TSimpleServer.Args 在構造時, 會指定一個默認的 TransportFactory, 當有新的客戶端鏈接時, 就會生成一個 TSocket 的 Transport 實例. 因爲這一點, 咱們在客戶端實現時, 也就須要指定客戶端的 Transport 爲 TSocket 才行.

客戶端實現
public class SimpleHelloClient {
    public static final String SERVER_IP = "localhost";
    public static final int SERVER_PORT = 8080;
    public static final int TIMEOUT = 30000;

    public void startClient(String userName) throws Exception {
        TTransport transport = null;

        transport = new TSocket(SERVER_IP, SERVER_PORT, TIMEOUT);
        // 協議要和服務端一致
        TProtocol protocol = new TBinaryProtocol(transport);
        HelloWorldService.Client client = new HelloWorldService.Client(
                protocol);
        transport.open();
        String result = client.sayHello(userName);
        System.out.println("Result: " + result);

        transport.close();
    }

    public static void main(String[] args) throws Exception {
        SimpleHelloClient client = new SimpleHelloClient();
        client.startClient("XYS");
    }
}

TThreadPoolServer 服務器模型

TThreadPoolServer 是一個基於線程池和傳統的阻塞 IO 模型實現, 每一個線程對應着一個鏈接.

服務器端實現
public class ThreadPoolHelloServer {
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws Exception {
        ThreadPoolHelloServer server = new ThreadPoolHelloServer();
        server.startServer();
    }

    public void startServer() throws Exception {
        TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>(
                new HelloWorldImpl());

        TServerSocket serverTransport = new TServerSocket(SERVER_PORT);
        TThreadPoolServer.Args tArgs = new TThreadPoolServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.protocolFactory(new TBinaryProtocol.Factory());

        TServer server = new TThreadPoolServer(tArgs);
        server.serve();
    }
}

TThreadPoolServer 的服務器端實現和 TSimpleServer 的沒有很大區別, 只不過是在對應的地方把 TSimpleServer 改成 TThreadPoolServer 便可.

一樣地, 咱們在 TThreadPoolServer 服務器端的代碼中, 和 TSimpleServer 同樣, 沒有顯示地指定 Transport 的類型, 這裏的緣由和 TSimpleServer 的同樣, 就再也不贅述了.

客戶端實現

代碼實現和 SimpleHelloClient 同樣.

TNonblockingServer 服務器模型

TNonblockingServer 是基於線程池的, 而且使用了 Java 提供的 NIO 機制實現非阻塞 IO, 這個模型能夠併發處理大量的客戶端鏈接.
注意, 當使用 TNonblockingServer 模型是, 服務器端和客戶端的 Transport 層須要指定爲 TFramedTransportTFastFramedTransport.

服務器端實現
public class NonblockingHelloServer {
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws Exception {
        NonblockingHelloServer server = new NonblockingHelloServer();
        server.startServer();
    }

    public void startServer() throws Exception {
        TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>(
                new HelloWorldImpl());

        TNonblockingServerSocket serverTransport = new TNonblockingServerSocket(SERVER_PORT);
        TNonblockingServer.Args tArgs = new TNonblockingServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.protocolFactory(new TBinaryProtocol.Factory());
        // 下面這個設置 TransportFactory 的語句能夠去掉
        tArgs.transportFactory(new TFramedTransport.Factory());

        TServer server = new TNonblockingServer(tArgs);
        server.serve();
    }
}

前面咱們提到, 在 TThreadPoolServerTSimpleServer 的服務器端代碼實現中, 咱們並無顯示地爲服務器端設置 Transport, 由於 TSimpleServer.ArgsTThreadPoolServer.Args 設置了默認的 TransportFactory, 其最終生成的 Transport 是一個 TSocket 實例.

那麼在 TNonblockingServer 中又會是怎樣的狀況呢?
經過查看代碼咱們能夠發現, TNonblockingServer.Args 構造時, 會調用父類 AbstractNonblockingServerArgs 的構造器, 其源碼以下:

public AbstractNonblockingServerArgs(TNonblockingServerTransport transport) {
    super(transport);
    this.transportFactory(new TFramedTransport.Factory());
}

能夠看到, TNonblockingServer.Args 也會設置一個默認的 TransportFactory, 它的類型是 TFramedTransport#Factory, 所以最終 TNonblockingServer 所使用的 Transport 實際上是 TFramedTransport 類型的, 這也就是爲何客戶端也必須使用 TFramedTransport(或TFastFramedTransport) 類型的 Transport 的緣由了.

分析到這裏, 回過頭來看代碼實現, 咱們就發現其實代碼中 tArgs.transportFactory(new TFramedTransport.Factory()) 這一句是多餘的, 不過爲了強調一下, 我仍是保留了.

客戶端實現
public class NonblockingHelloClient {
    public static final String SERVER_IP = "localhost";
    public static final int SERVER_PORT = 8080;
    public static final int TIMEOUT = 30000;

    public void startClient(String userName) throws Exception {
        TTransport transport = null;

        // 客戶端使用 TFastFramedTransport 也是能夠的.
        transport = new TFramedTransport(new TSocket(SERVER_IP, SERVER_PORT, TIMEOUT));
        // 協議要和服務端一致
        TProtocol protocol = new TBinaryProtocol(transport);
        HelloWorldService.Client client = new HelloWorldService.Client(
                protocol);
        transport.open();
        String result = client.sayHello(userName);
        System.out.println("Result: " + result);

        transport.close();
    }

    public static void main(String[] args) throws Exception {
        NonblockingHelloClient client = new NonblockingHelloClient();
        client.startClient("XYS");
    }
}
異步客戶端實現

在 TNonblockingServer 服務器模型下, 除了使痛不式的客戶端調用方式, 咱們還能夠在客戶端中使用異步調用的方式, 具體代碼以下:

public class NonblockingAsyncHelloClient {
    public static final String SERVER_IP = "localhost";
    public static final int SERVER_PORT = 8080;
    public static final int TIMEOUT = 30000;

    public void startClient(String userName) throws Exception {
        TAsyncClientManager clientManager = new TAsyncClientManager();
        TNonblockingTransport transport = new TNonblockingSocket(SERVER_IP,
                SERVER_PORT, TIMEOUT);

        // 協議要和服務端一致
        TProtocolFactory protocolFactory = new TBinaryProtocol.Factory();
        HelloWorldService.AsyncClient client = new HelloWorldService.AsyncClient(
                protocolFactory, clientManager, transport);

        client.sayHello(userName, new AsyncHandler());

        Thread.sleep(500);
    }

    class AsyncHandler implements AsyncMethodCallback<String> {
        @Override
        public void onComplete(String response) {
            System.out.println("Got result: " + response);
        }

        @Override
        public void onError(Exception exception) {
            System.out.println("Got error: " + exception.getMessage());
        }
    }

    public static void main(String[] args) throws Exception {
        NonblockingAsyncHelloClient client = new NonblockingAsyncHelloClient();
        client.startClient("XYS");
    }
}

能夠看到, 使用異步的客戶端調用方式實現起來就比較複雜了. 和 NonblockingHelloClient 對比, 咱們能夠看到有幾點不一樣:

  • 異步客戶端中須要定義一個 TAsyncClientManager 實例, 而同步客戶端模式下不須要.

  • 異步客戶端 Transport 層使用的是 TNonblockingSocket, 而同步客戶端使用的是 TFramedTransport

  • 異步客戶端的 Procotol 層對象須要使用 TProtocolFactory 來生成, 而同步客戶端須要用戶手動生成.

  • 異步客戶端的 Client 是 HelloWorldService.AsyncClient, 而同步客戶的 Client 是 HelloWorldService.Client

  • 最後也是最關鍵的不一樣點, 異步客戶端須要提供一個異步處理 Handler, 用於處理服務器的回覆.

咱們再來看一下 AsyncHandler 這個類. 這個類是用於異步回調用的, 當咱們正常收到了服務器的迴應後, Thrift 就會自動回調咱們的 onComplete 方法, 所以咱們在這裏就能夠設置咱們的後續處理邏輯.
當 Thrift 遠程調用服務器端出現異常時(例如服務器未啓動), 那麼就會回調到 onError 方法, 咱們在這個方法中就能夠作相應的錯誤處理.

THsHaServer 服務器模型

Half-Sync/Half-Async, 半同步/半異步服務器模型, 底層的實現其實仍是依賴於 TNonblockingServer, 所以它所須要的 Transport 也是 TFramedTransport.

服務器端實現
public class HsHaHelloServer {
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws Exception {
        HsHaHelloServer server = new HsHaHelloServer();
        server.startServer();
    }

    public void startServer() throws Exception {
        TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>(
                new HelloWorldImpl());

        TNonblockingServerSocket serverTransport = new TNonblockingServerSocket(SERVER_PORT);
        THsHaServer.Args tArgs = new THsHaServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.protocolFactory(new TBinaryProtocol.Factory());

        TServer server = new THsHaServer(tArgs);
        server.serve();
    }
}
客戶端實現

和 NonblockingHelloClient 一致.

參考

相關文章
相關標籤/搜索