Apache-Thrift-Thrift-Thrift

接着上篇文章说。

我们知道了Apache Thrift主要用于各个服务之间的RPC通信,并且支持跨语言;包括C++,Java,Python,PHP,Ruby,Go,Node.js等等,还有一些都没听说过的语言;而且从上篇文章的RPC例子中可以发现,Thrift是一个典型的CS(客户端/服务端)架构;加上跨语言的特性,我们可以推断一下:客户端和服务端是可以使用不同的语言开发的。

如果CS端可以使用不同的语言来开发,那么一定是有一种中间语言来关联客户端和服务端(相同语言也需要关联客户端和服务端)。其实这个答案都知道,那就是接口定义语言:IDL(Interface Description Language);下面我们从IDL进行开场表演,进行一次Thrift RPC的完整演出。

接口描述语言IDL

依旧照例,搬来wiki的解释:

接口描述语言(Interface Description Language,缩写IDL),是用来描述软件组件界面的一种计算机语言。IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流;比如,一个组件用C++写成,另一个组件用Java写成。

IDL通常用于远程调用软件。在这种情况下,一般是由远程客户终端调用不同操作系统上的对象组件,并且这些对象组件可能是由不同计算机语言编写的。IDL建立起了两个不同操作系统间通信的桥梁。

关于IDL数据类型和语法介绍,这里简单列举:

基本类型

  • byte:8位有符号整数(byte)
  • i16:16位有符号整数(short)
  • i32:32位有符号整数(int)
  • i64:64位有符号整数(long)
  • double:64位浮点数(double)
  • string:字符串(string)
  • bool:布尔变量(boolean)

Thrift不支持无符号整数,因为不是所有的语言都支持无符号整数。

容器类型

  • list:有序列表,元素可以重复
  • set:无序集合,元素不可重复
  • map<k,v>:字典结构,键值对

结构体

类似C语言的结构体。

1
2
3
4
5
// required和optional,必选和可选
struct Example {
1: required string name;
3: optional i32 age;
}

枚举类型

可以像C/C++一样定义枚举类型。

1
2
3
4
5
6
7
8
9
enum EnumType {
NUMBER = 2
}

struct exampleStruct {
required i32 id;
required string userName;
optional EnumType enumType = EnumType.NUMBER;
}

异常

可以自定义异常,所定义的异常会继承对应语言的异常类,比如Java,就会继承java.lang.Exception

1
2
3
4
exception RequestException {
1: required i32 code;
2: optional string reason;
}

服务

相当于Java中创建接口(Interface)一样,创建的service经过Thrift代码生成客户端和服务端的框架。

1
2
3
service exampleService{
string hello(1:string username)
}

命名空间

关键字namespace,相当于Java中的package,必须需要。

1
2
// 代码路径:service.study.thrift
namespace java service.study.thrift

常量

定义常量,复杂的类型和结构体可以使用JSON来表示。

1
2
const i32 INT_CONST = 20;
const map<i32,string> MAP_CONST = {1:"hello",2:"world"};

注释

支持#//单行注释,和/***/多行注释。

我们用以上的的数据类型和语法规则,自定义生成一个Thrift文件,取名叫hello.thrift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace java org.pross.thrift

enum RequestType {
SAY_HELLO, //问好
QUERY_TIME, //询问时间
}

struct Request {
1: required RequestType type; // 请求的类型,必选
2: required string name; // 发起请求的人的名字,必选
3: optional i32 age; // 发起请求的人的年龄,可选
}
// 异常
exception RequestException {
1: required i32 code;
2: optional string reason;
}
// 服务名,接口:addition()和sayHello(),将会使用到
service HelloMethod{
i32 division(1:i32 param1,2:i32 param2) throws (1:RequestException re) //可能抛出异常
string sayHello(1:string username)
}

安装好thrift,在终端运行:thrift --gen java hello.thrift,则在当前目录下会生成一个gen-java目录,在该目录下会按照namespace定义路径名,生成文件夹;最终我们可以看到生成了4个Java类:HelloMethodRuquestRequestExceptionRequestTypehello.thrift文件中定义的enum,struct,exception,service都相应地生成了一个Java类,这就是能支持Java语言通信的基本框架代码。

thrift --gen java hello.thrift,指定Java语言生成框架代码。

如果客户端和服务端使用不同的语言来编写,只需要对选择不同语言的生成框架代码即可。

我们再来看看生成的HelloMethod类的具体结构,简单介绍一下:

对于开发人员而言,需要关注几个核心内部接口/方法:

  • Iface:服务端通过实现HelloMethod.Iface接口,向客户端提供同步调用业务逻辑的接口。

  • AsyncIface:服务端通过实现HelloMethod.Iface接口,向客户端提供异步调用,异步调用接口多了一个回调参数AsyncMethodCallback<String>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 同步调用接口  
    public interface Iface {

    public int division(int param1, int param2) throws RequestException, org.apache.thrift.TException;

    public String sayHello(String username) throws org.apache.thrift.TException;

    }

    //异步调用接口
    public interface AsyncIface {

    public void division(int param1, int param2, org.apache.thrift.async.AsyncMethodCallback<Integer> resultHandler) throws org.apache.thrift.TException;

    public void sayHello(String username, org.apache.thrift.async.AsyncMethodCallback<String> resultHandler) throws org.apache.thrift.TException;

    }
  • Client/AsyncClient:客户端实例化HelloMethod.Client对象,以同步/异步的方式访问服务端提供的服务方法。

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
        // 提供工厂方法创建client对象
    public static class Factory implements org.apache.thrift.TServiceClientFactory<Client> {
    public Factory() {}
    public Client getClient(org.apache.thrift.protocol.TProtocol prot) {
    return new Client(prot);
    }
    public Client getClient(org.apache.thrift.protocol.TProtocol iprot, org.apache.thrift.protocol.TProtocol oprot) {
    return new Client(iprot, oprot);
    }
    }

    // 接口方法的客户端代理
    public int division(int param1, int param2) throws RequestException, org.apache.thrift.TException
    {
    // 发送方法调用请求
    send_division(param1, param2);
    // 接收返回值
    return recv_division();
    }

    public void send_division(int param1, int param2) throws org.apache.thrift.TException
    {
    // 创建方法参数对象,封装方法参数
    division_args args = new division_args();
    args.setParam1(param1);
    args.setParam2(param2);
    // 调用父类方法发送消息
    sendBase("division", args);
    }

    ...
    // org.apache.thrift.TServiceClient.sendBase
    private void sendBase(String methodName, TBase<?, ?> args, byte type) throws TException {
    // 发送消息头(TProtocol)
    this.oprot_.writeMessageBegin(new TMessage(methodName, type, ++this.seqid_));
    // 发送消息体,由方法参数对象自己处理编码, write(TProtocol)
    args.write(this.oprot_);
    this.oprot_.writeMessageEnd();
    this.oprot_.getTransport().flush();
    }
  • Processor:用来支持方法调用,每个服务实现类需要使用Processor来注册,这样服务器调用接口实现时能定位到具体的实现类。

  • 方法参数的封装类:以方法名_args命名

  • 方法返回值的封装类:以方法名_result命名

以上就是通过IDL语法规则,生成的中间语言,用于客户端和服务端之间的通信交流。

Thrift协议栈整体的架构

OK,我们已经搭建好了ClientServer端相互交流的桥梁了。接下来看看Thrift,了解下整体架构,在编写代码实现逻辑交互时,才能知其所以然。架构如下图:

thrift架构

在Client和Server的最顶层都是用户自定义的处理逻辑。

Processor(TProcessor的子类)是服务器端从Thrift框架转入用户逻辑的关键流程,负责对Client的请求做出响应,包括RPC请求转发、调用参数解析、用户逻辑调用,返回值写回等;也对TServer中请求的InputProtocol和OutputTProtocol进行操作,从InputProtocol中读出Client的请求数据,向OutputProtcol中写入用户逻辑的返回值。

TServer主要任务就是高效的接受Client的请求,并将请求转发到Processor进行处理,主要有以下实现:

  • TSimpleServer:简单的单线程网络阻塞模型,同时只能服务一个client连接,其他所有客户端在被服务器端接受之前都只能等待,主要用于测试。
  • TNonblockingServer:多线程服务模型,使用了NIO中的Selector选择器,通过调用select(),使得客户端的请求阻塞在多个连接上,而不是在单一的连接;可以服务多个客户端。
  • THsHaServer:使用单独的线程来处理网络I/O,一个独立的worker线程池来处理消息,只要有空闲的worker线程就会被立即处理。
  • TThreadedSelectorServer:维护了两个线程池,一个用来处理网络I/O,另一个用来进行请求的处理。当网络I/O是瓶颈的时候,TThreadedSelectorServer比THsHaServer的表现要好。
  • TThreadPoolServer:线程池网络模型,是将每一个请求都加入到ThreadPoolExecutor线程池中,并绑定其中一个worker线程去处理,直到关闭。

TProtocol 是传输协议层,主要负责结构化数据,并组装成Message,或者从Message结构中读出结构化数据。将一个有类型的数据转化为字节流以交给TTransport进行传输,或者从TTransport中读取一定长度的字节数据转化为特定类型的数据。如int32会被TBinaryProtocol Encode为一个四字节的字节数据,或者TBinaryProtocol从TTransport中取出四个字节的数据Decode为int32。

传输协议包括:TBinaryProtocol(基于二进制编码传输的协议),TCompactProtocol(紧凑,高效的二进制传输协议,使用Variable-Length Quantity (VLQ) 编码对数据编码压缩,主要是对整数采用可变长度),TJSONProtocol(使用JSON格式编码的传输协议),TDebugProtocol(文本格式,方便抓包Debug)。

TTransport 传输层负责以字节流方式发送和接收Message,是底层IO模块在Thrift框架中的实现,每一个底层IO模块都会有一个对应TTransport来负责Byte Stream(字节流)数据在该IO模块上的传输,有以下TTransport的实现:

  • TSocket:阻塞型 socket,是最常见的模式,用于客户端,采用系统函数 read 和 write 进行读写数据
  • TServerSocket:非阻塞型 socket,用于服务器端,accecpt 到的 socket 类型都是 TSocket
  • THttpTransport:采用HTTP传输协议进行传输,非阻塞式中使用
  • TFramedTransport:按块的大小进行传输,支持定长数据发送和接收
  • TFileTransport:用于写数据到文件,以文件形式进行传输
  • TMemoryBuffer:从一个缓冲区中读写数据,Java实现使用的是ByteArrayOutputStream
  • TZlibTransport:Java中采用java.util.zip包,来进行压缩和解压缩

底层IO模块,负责实际的数据传输,包括Socket,文件,或者压缩数据流等。

Show me your code

项目需要引入libthrift依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.12.0</version>
</dependency>

创建接口实现类实现HelloMethod.Iface接口,并实现相应方法的处理逻辑和返回。

1
2
3
4
5
6
7
8
9
10
11
12
public class HelloThriftImpl implements HelloMethod.Iface {
// 除法,整除
public int division(int param1, int param2) throws RequestException {
int value = param1 / param2;
return value;
}

// say + username
public String sayHello(String username) {
return "hello "+username;
}
}

创建 ThriftServer.java 实现服务端,关键代码如下:

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
26
27
28
public class ThriftServer{

public static void initServer(){
try {
// 设置多线程服务模型
TNonblockingServerSocket socket = new TNonblockingServerSocket(9090);
// 关联业务处理器TProcessor
TProcessor processor = new HelloMethod.Processor(new HelloThriftImpl());
// 设置二进制传输协议,TFramedTransport传输方式和关联业务处理
TNonblockingServer.Args arg = new TNonblockingServer.Args(socket);
arg.protocolFactory(new TBinaryProtocol.Factory());
arg.transportFactory(new TFramedTransport.Factory());
arg.processorFactory(new TProcessorFactory(processor));

// 开启服务
TServer server = new TNonblockingServer (arg);
System.out.println("start server port 9090 ...");
server.serve();

} catch (TTransportException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
initServer();
}
}

创建ThriftClient.java实现客户端:

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
26
27
28
29
30
31
32
33
34
35
public class ThriftClient {

private static void sendServerRequest() {
try {
// 创建客户端连接,url,port,timeout
TSocket socket = new TSocket("localhost", 9090, 3000);
// 设置TFramedTransport传输方式,二进制传输协议
TTransport transport = new TFramedTransport(socket);
TProtocol protocol = new TBinaryProtocol(transport);
// open client
socket.open();

// 实例客户端业务处理,调用方法,返回结果
HelloMethod.Client client = new HelloMethod.Client(protocol);
// hello + pross
String hello = client.sayHello("pross");
// 3/1 =3
int value = client.division(3, 1);
System.out.println(hello);
System.out.println("division value:"+value);

socket.close();
} catch (TTransportException e) {
e.printStackTrace();
} catch (RequestException e) {
e.printStackTrace();
} catch (TException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
sendServerRequest();
}
}

先运行服务端,出现提示:

1
start server port 9090 ...

然后运行客户端,控制台打印出:

1
2
hello pross
division value:3

看到了预料之中的结果,Thrift演出毕。

Ref

Thrift RPC详解

Thrift: The Missing Guide

Apache Thrift

完。