Thrift使用

https://lizongwu.github.io/2017/02/13/Thrift%E4%BD%BF%E7%94%A8/

1. thrift基本使用

很多PRC解決方案都是採用一種中間的IDL語言來描述接口,通過該接口服務端知道應該提供哪些服務,客戶端知道可以使用哪些服務。Java RMI,CORBA,Android AIDL(AIDL爲IPC,不過思路是一樣的)都是採用的這種思想,thrift同樣也使用了這樣的方式,使用一種中間的IDL語言描述接口的好處是可以通過該接口得到各個語言的對應接口,從而可以支持多種語言,服務器與客戶端不必一定使用同種語言。在具體瞭解thrift之前,先看一個阿里Dubbo框架作者樑飛貼出的 簡單RPC實現 ,可以對RPC的功能有直觀的理解。

從簡單RPC實現的博客中可以看到,RPC其實就是將客戶端的相關請求內容(方法名,參數等)序列化,然後通過網絡傳輸到服務端,服務端解序列化得到請求,調用對應方法得到結果,最後將結果序列化通過網絡傳輸到客戶端。thrift的實現也是這樣的,thrift的使用可以分爲以下幾步:

  1. 使用thrift的IDL語言在後綴名爲.thrift的文件中書寫接口
  2. 使用thrfit來生成服務端和客戶端對應語言的接口代碼
  3. 分別實現服務端和客戶端代碼
  4. 啓動服務端代碼供客戶端調用

1) 使用thrift的IDL語言在後綴名爲.thrift的文件中書寫接口

thrift的IDL語法跟C++很類似,下面是一個例子

1
2
3
4
5
6
7
8
9
10
namespace java com.youdao

struct MyStruct {
	1: i32 id;
	2: string name;
}

service MyService {
	MyStruct getMyStruct(1: i32 id);
}

namespace java com.youdao 表示thrift在生成Java代碼時會將類放入com.youdao包中,如果要生成其他語言的命名空間可以將java換成對應的標識,比如C++爲cpp,也可以寫成 namespace * com.youdao ,這樣所以語言都包含了。

注意struct裏屬性前和service方法參數前的數是必須的,並且這些數必須是正數並且唯一的,但不一定連續 。

還有, struct不支持繼承,但是service支持繼承 。

其他更多語法參考 Thrift: The Missing Guide

2) 使用thrift來生成服務端和客戶端對應語言的接口代碼

使用下面的命令生成對應語言的接口代碼,比如生成Java代碼,將 <lang> 換成java。

1
thrift --gen <lang> myfirst.thrift

更多命令參數查看 thrift -help

3) 分別實現服務端和客戶端代碼

在看怎麼實現服務端代碼之前需要先看一下thrift的結構:

 

thrift的結構一共有三層,這三層需要我們在編程的時候根據自己的需求來設置。

Transport. Each language must have a common interface to bidirectional raw data transport. The specifics of how a given transport is implemented should not matter to the service developer. The same application code should be able to run against TCP stream sockets, raw data in memory, or files on disk.

Protocol. Datatypes must have some way of using the Transport layer to encode and decode themselves. Again, the application developer need not be concerned by this layer. Whether the service uses an XML or binary protocol is immaterial to the application code. All that matters is that the data can be read and written in a consistent, deterministic matter.

Processors. Finally, we generate code capable of processing data streams to accomplish remote procedure calls. Section 6 details the generated code and TProcessor paradigm.

這個三層可以對比TCP/IP協議棧來看,Transport相當於傳輸層,定義了雙方通過什麼方式傳輸數據,比如網絡或者文件,我們一般通過網絡傳輸;Protocol相當於應用層,定義了數據以何種格式進行傳輸,二進制還是JSON;Processor相當於處理數據的應用程序(比如WEB程序,瀏覽器),定義瞭如何處理數據,這裏就是實現IDL定義的接口。

通過下面的例子來了解如何定義這三層。

1
2
3
4
5
6
7
8
9
10
11
12
// mythrift.thrift
namespace java com.youdao
service Calc {
    i32 add(1: i32 num1, 2: i32 num2);
}
// 實現接口
public class CalcImpl implements Calc.Iface {
    @Override
    public int add(int num1, int num2) throws TException {
        return num1 + num2;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 服務端
// 定義Transport
TServerTransport serverTransport = new TServerSocket(1111);
// 定義Protocol
TBinaryProtocol.Factory factory = new TBinaryProtocol.Factory(true, true);
// 定義Processor
Calc.Processor processor = new Calc.Processor(new CalcImpl());
// 定義Server
TServer.Args targs = new TServer.Args(serverTransport);
targs.protocolFactory(factory);
targs.processor(processor);
TServer server = new TSimpleServer(targs);
// 服務啓動
server.serve();
1
2
3
4
5
6
7
8
9
10
// 客戶端
// 定義Transport
TTransport transport = new TSocket("localhost", 1111);
// 定義Protocol
TProtocol protocol = new TBinaryProtocol(transport);
// 定義Client
Calc.Client client = new Calc.Client(protocol);
// 打開通道,準備讀寫
transport.open();
System.out.println(client.add(5, 5));

上面就是thrift中定義三層的基本方式,服務端定義了三層,客戶端由於是調用服務而不是提供服務,所以不需要Processor。

4) 啓動服務端代碼供客戶端調用

上面的代碼不僅展示瞭如何定義三層,還展示瞭如何定義Server和Client,是可以完整運行的。需要注意的有幾點:

  • 服務端實現接口實現的是Calc.Iface接口,客戶端是Calc.Client類型實例。
  • 服務端的Transport,Protocol,Processor最終都彙總在TServer.Args中,然後傳遞給Server,而客戶端則是Protocol包着Transport,Client包着Protocol。
  • 服務端其實不必定義Protocol,所以通常寫法爲:
1
2
3
4
TServerTransport serverTransport = new TServerSocket(1111);
Calc.Processor processor = new Calc.Processor(new CalcImpl());
TServer server = new TSimpleServer(new TServer.Args(serverTransport).processor(processor));
server.serve();

2. 其他類型的Transport,Protocol,Server

thrift的三層是自己編程時定義的,根據自己的業務需求來確定,到底有哪些Transport,Protocol,Server呢?

打開thrift的文檔,可以看到thrift爲我們提供了很多類型的三層,比如TTransport的子類就有TFileTransport, TFramedTransport, THttpClient等等。TServerTransport的子類有TNonblockingServerTransport, TServerSocket等。TProtocol子類也有TBinaryProtocol, TCompactProtocol, TJSONProtocol等等。TServer的子類有TSimpleServer, TThreadPoolServer, TNonblockingServer, THsHaServer等。

3. TMultiplexedProcessor複用端口

同步阻塞IO是最傳統的IO模型,但是這種模型由於會阻塞線程或者進程,所以一個線程只能處理一個IO請求,由此出現IO多路複用技術,比如Linux的epoll系統調用,一個進程可以處理多個IO請求。

從上面thrift的使用中可以看到一個Server對應一個Processor和一個Transport,如果有多個服務的話,那必須要啓動多個Server,佔用多個端口,這種方式顯然不是我們想要的,所以thrift爲我們提供了複用端口的方式,通過監聽一個端口就可以提供多種服務,這種方式需要用到兩個類:TMultiplexedProcessor和TMultiplexedProtocol。

 

TMultiplexedProcessor是用在服務端,多個Processor註冊在其上,然後將TMultiplexedProcessor傳入TServer.Args,就可以做到只啓動一個Server提供多項服務。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// mythrift.thrfit
namespace java com.youdao
service Calc {
    i32 add(1: i32 num1, 2: i32 num2);
}
service Echo {
    string echo(1: string str);
}
// Echo服務
public class EchoImpl implements Echo.Iface {
    @Override
    public String echo(String str) throws TException {
        return "echo: " + str;
    }
}

// 服務端
TServerTransport serverTransport = new TServerSocket(1111);
TMultiplexedProcessor multiplexedProcessor = new TMultiplexedProcessor();
multiplexedProcessor.registerProcessor("calc", new Calc.Processor<>(new CalcImpl())); // 第一個參數爲服務名稱,該名稱必須是唯一的,因爲客戶端會通過該名稱來調用對應服務
multiplexedProcessor.registerProcessor("echo", new Echo.Processor<>(new EchoImpl()));
TServer server = new TSimpleServer(new TServer.Args(serverTransport).processor(multiplexedProcessor));
server.serve();

TMultiplexedProtocol是用在客戶端,將原來的Protocol和想要的服務名稱作爲參數實例化TMultiplexedProtocol,替代原來的Protocol傳入Client即可使用對應服務。

1
2
3
4
5
6
7
8
// 客戶端
TTransport transport = new TSocket("localhost", 1111);
TProtocol protocol = new TBinaryProtocol(transport);
Calc.Client calcClient = new Calc.Client(new TMultiplexedProtocol(protocol, "calc"));
Echo.Client echoClient = new Echo.Client(new TMultiplexedProtocol(protocol, "echo"));
transport.open();
System.out.println(calcClient.add(5, 5));
System.out.println(echoClient.echo("hello world!"));

4. thrift可能會丟失類型信息

來試驗一個例子,一個父類Father,一個子類Child,用父類引用指向子類對象,然後將對象序列化,那麼從另一個程序讀出的對象是什麼類型呢?是Father還是Child?

先來看通過Java的對象流來序列化的結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 寫入端
public class Father implements Serializable {
    private static final long serialVersionUID = -1L;
}

public class Child extends Father {
    private static final long serialVersionUID = -2L;
}

public static void main( String[] args ) throws Exception {
    Father father = new Child();
    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("/Users/XXX/a.dat")));
    outputStream.writeObject(father);
    outputStream.close();
}
1
2
3
4
5
// 讀出端
public static void main(String[] args) throws Exception {
    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("/Users/XXX/a.dat")));
    Object o = inputStream.readObject();
}

結果是報錯了,因爲我們沒有將Father和Child類複製到讀出端,但是從錯誤信息還是能看到序列化的過程保留了類型信息,類型是運行時類型Child,而不是Father:

1
2
Exception in thread "main" java.lang.ClassNotFoundException: com.youdao.Child
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)

下面來看thrift:

IDL接口文件:

1
2
3
4
5
6
7
8
9
// testinherit.thrift
namespace java com.youdao.thrift

struct Father {
}

service MyService {
    Father getFather();
}

由於thrift的struct不支持繼承,所以我們不能直接在thrfit文件裏繼承Father。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Father通過thrift自動生成
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"})
@Generated(value = "Autogenerated by Thrift Compiler (0.9.3)", date = "2016-11-22")
public class Father implements org.apache.thrift.TBase<Father, Father._Fields>, java.io.Serializable, Cloneable, Comparable<Father> {
}

public class Child extends Father {
}

// 實現服務接口,這裏返回的是Child類型的對象
public class MyServiceImpl implements MyService.Iface {

    @Override
    public Father getFather() throws TException {
        return new Child();
    }
}
// 啓動服務
public static void main( String[] args ) throws Exception {
    MyService.Processor processor = new MyService.Processor(new MyServiceImpl());
    TServerTransport transport = new TServerSocket(9091);
    TServer server =  new TSimpleServer(new TServer.Args(transport).processor(processor));
    System.out.println("server starting...");
    server.serve();
}
1
2
3
4
5
6
7
8
// 客戶端
public static void main(String[] args) throws Exception {
        TTransport transport = new TSocket("localhost", 9091);
        transport.open();
        TProtocol protocol = new TBinaryProtocol(transport);
        MyService.Client client = new MyService.Client(protocol);
        System.out.println(client.getFather().getClass());
    }

輸出爲:

1
class com.youdao.thrift.Father

客戶端得到的是Father類型,而不是Child類型,但是我們實際返回的是Child類型,說明thrift並不能支持未在thrfit文件裏定義的類型,如果在thrift文件外繼承則會丟失類型信息。

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