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文件外继承则会丢失类型信息。

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