RPC服务-Thrift

Thrift概述

Thrift作为Facebook开源的RPC框架, 通过IDL(Interface Definition Language)中间语言, 并借助代码生成引擎生成各种主流语言的rpc框架服务端/客户端代码

语义语法

注释

  • Thrift支持C多行风格和Java/C++单行风格.

命名空间

  • Thrift中的命名空间类似于C++中的namespace和java中的package,它们提供了一种组织(隔离)代码的简便方式; 名字空间也可以用于解决类型定义中的名字冲突.

基本类型

  • bool: 布尔值 对应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

  • binary: Blob 类型 对应Java中的byte[]

集合类型

  • list<t>: 元素类型为t的有序表,容许元素重复.

  • set<t>: 元素类型为t的无序表,不容许元素重复.

  • map<t, t>: 值类型为t的键值对,键不容许重复.

枚举类型

  • Thrift不支持枚举类嵌套,枚举常量必须是32位的正整数, 末尾属性没有分号

常量

  • 在变量前面加上const, 改变量被声明为一个常量.

结构体

  • 类似与Java中的对象, struct不能继承,但是可以嵌套(不能嵌套自己), 其成员都是有明确类型. 不支持泛型. 成员分割符可以是,或是; 定义结构体成员时, 成员必须是被正整数编号过的,其中的编号使不能重复的,这个是为了在传输过程中编码使用.

  • 字段会有optional和required之分, 如果不指定则为无类型–可以不填充该值,但是在序列化传输的时候也会序列化进去,optional是不填充则部序列化,required是必须填充也必须序列化.

  • 同一文件可以定义多个struct,也可以定义在不同的文件,进行include引入.

异常

  • 异常在语法和功能上类似于结构体,差别是异常使用关键字exception,而且异常是继承每种语言的基础异常类

文件包含

  • 为了便于管理、重用和提高模块性/组织性,我们常常分割Thrift定义在不同的文件中; 通过include关键字进行分文件的引入包含

应用示例

基于IDL示例

示例结构

1
2
3
4
5
6
7
8
[root@localhost etc]$ tree sample
sample
├── generate.sh
├── idl
│   ├── Request.thrift
│   ├── Response.thrift
│   └── ServiceIface.thrift
└── src

定义结构体

定义入参

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost sample]$ cat idl/Request.thrift
// 定义包名
namespace java com.sample.thrift.model

include "Response.thrift"

/**
* 查询入参
*/
struct QueryEntry {
required string keyword; // 检索词组(required 必填)
optional Response.AuditEnum audit; // 审核状态(optional 选填)
}

定义出参

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
[root@localhost sample]$ cat idl/Response.thrift
// 定义包名
namespace java com.sample.thrift.model

/**
* 审批状态枚举定义
*/
enum AuditEnum {
SUBMIT = 1, // 提交待审
APPROVED = 2, // 审核通过
REJECT = 3, // 提交驳回
CANCEL = 4 // 提交取消
}

/**
* 数据结果集
*/
struct SampleView {
optional i64 id; // 主键
optional string title; // 标题
optional list<string> tags; // 标签
optional bool online; // 是否上线
optional AuditEnum audit; // 审核状态
}

/**
* 数据状态模型
*/
struct ApiResult {
optional i32 status; // 状态
optional string message; // 描述
optional map<i64,SampleView> data; // 数据
}

定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@localhost sample]$ cat idl/ServiceIface.thrift
// 定义包名
namespace java com.sample.thrift.iface

include "Request.thrift"
include "Response.thrift"

/**
* 定义服务接口
*/
service SampleService {

/**
* 定义RPC服务接口
* param query 查询条件
* param page 页码
* param size 页量
*/
Response.ApiResult query(1:Request.QueryEntry query, 2: i32 page, 3: i32 size);
}

代码生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@localhost sample]$ thrift -version
Thrift version 0.11.0
[root@localhost sample]$ cat generate.sh
#!/bin/bash

for file in `ls idl`
do
thrift -r -gen java -out ./src idl/$file
done
[root@localhost sample]$ sh generate.sh
[root@localhost sample]$ tree src
src
└── com
└── sample
└── thrift
├── iface
│   └── SampleService.java
└── model
├── ApiResult.java
├── AuditEnum.java
├── QueryEntry.java
└── SampleView.java

5 directories, 5 files

基于注解示例

上面的IDL生成的方式我们可以使用注解方式进行定义

定义结构体

QueryEntry.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@ThriftStruct
@Setter
@NoArgsConstructor
public class QueryEntry {

/**
* 关键词
*/
private String keyword;

// 如果有非无参构造函数,需要在构造函数上加上@ThriftConstructor注解,
// 一个类只能有一个带有@ThriftConstructor注解的构造函数
@ThriftConstructor
public QueryEntry(String keyword) {
this.keyword = keyword;
}

@ThriftField(1)
public String getKeyword() {
return keyword;
}
}

AuditEnum.java

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义枚举
@ThriftEnum
@Getter
@AllArgsConstructor
public enum AuditEnum {

SUBMIT(1), // 提交待审
APPROVED(2), // 审核通过
REJECT(3), // 提交驳回
CANCEL(4); // 提交取消

private int audit;
}

SampleView.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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@ThriftStruct
@Getter
@NoArgsConstructor
public class SampleView {
/**
* 主键
*/
private Long id;
/**
* 标题
*/
private String title;
/**
* 标签
*/
private List<String> tags;
/**
* 是否上线
*/
private boolean online;
/**
* 审核状态
*/
private AuditEnum audit;

@ThriftField(1)
public Long getId() {
return id;
}

@ThriftField(2)
public String getTitle() {
return title;
}

@ThriftField(3)
public List<String> getTags() {
return tags;
}

@ThriftField(4)
public boolean isOnline() {
return online;
}

@ThriftField(5)
public AuditEnum getAudit() {
return audit;
}
}

ApiResult.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
@ThriftStruct
@Setter
@NoArgsConstructor
public class ApiResult {

// 状态
private int status;
// 描述
private String message;
// 数据
private Map<Long, SampleView> data;

@ThriftField(value = 1, requiredness = ThriftField.Requiredness.REQUIRED)
public int getStatus() {
return status;
}

@ThriftField(2)
public String getMessage() {
return message;
}

@ThriftField(3)
public Map<Long, SampleView> getData() {
return data;
}
}

定义接口服务

1
2
3
4
5
6
7
8
9
10
11
@ThriftService
public interface SampleService {

/**
* 服务接口
* @return
*/
@ThriftMethod(exception = {@ThriftException(type = TException.class, id = 1)})
public ApiResult query(QueryEntry query, int page, int size);

}

说明: 示例中@Getter@Setter@NoArgsConstructor是lombok注解(简化模板代码),非thrift注解.

Maven生成插件

thrift对应maven生成插件为maven-thrift-plugin(注意:将thrift文件放置在src/main/thrift/目录下)

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
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.thrift.tools</groupId>
<artifactId>maven-thrift-plugin</artifactId>
<version>0.1.11</version>
<configuration>
<!-- config thrift bin file path -->
<thriftExecutable>/usr/local/bin/thrift</thriftExecutable>
</configuration>
<executions>
<execution>
<id>thrift-sources</id>
<phase>generate-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>thrift-test-sources</id>
<phase>generate-test-sources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

运行mvn package后,会自动在target目录下生成java源码及编译后的class

数据传输协议

Thrift在传输协议(传输格式)上总体上划分为文本(text)和二进制(binary)传输协议;为节约带宽,提供传输效率, 一般情况下使用二进制类型的传输协议为多数.

  • TBinaryProtocol: 二进制编码格式进行数据传输。

  • TCompactProtocol: 压缩格式进行数据传输。

  • TTupleProtoco: 继承于TCompactProtocol,Struct的编解码时使用更省地方但IDL间版本不兼容的TupleScheme.

  • TJSONProtocol: JSON格式编码协议进行数据传输。

  • TSimpleJSONProtocol: 这种协议只提供JSON只写的协议,适用于通过脚本语言解析

  • TDebugProtocol: 在开发的过程中帮助开发人员调试用的,使用易懂的可读的文本格式.

说明: 客户端和服务端的协议要一致.

数据传输方式

protocol说明的是什么被传送(传送内容格式),transports说明的是如何传送(传送方式)

一个server只允许定义一个接口服务。这样的话多个接口需要多个server。这样会带来资源的浪费。通常可以通过定义一个组合服务来解决。

  • TSocket: 采用blocking socket I/O

  • TFileTransport: 以文件(日志)形式进行传输。

  • TFramedTransport: 以帧的形式发送,每帧前面是一个长度。要求服务器方式为non-blocking server

  • TZlibTransport: 使用zlib进行压缩传输,与其他传输方式联合使用.

  • TMemoryTransport: 使用内存I/O,java实现中在内部使用了ByteArrayOutputStream

  • TNonblockingTransport: 使用JDKNIO的Transport实现,读写的byte[]会每次被wrap成一个ByteBuffer.

序列化和反序列化

Thrift序列化时属性标识如下, 没有域的名称,因此与JSON/XML这种序列化工具相比,thrift序列化后生成的文件体积要小很多.

1
数字ID + 属性类型TYPE

Thrift的向后兼容性需要满足2个条件, 这样无论增加还是删除域,都可以实现向后兼容.

  1. 域的序号不能改变(数值)

  2. 域的类型不能改变(类型)

服务端类型

Thrift提供网络模型有单线程、多线程、事件驱动; 也可划分为:阻塞服务模型、非阻塞服务模型。

阻塞服务模型

  • TSimpleServer: 简单的单线程服务器,主要用于测试.

  • TThreadPoolServer: 使用标准阻塞式IO的多线程服务器

非阻塞服务模型

  • TNonblockingServer: 采用NIO的模式, 借助Channel/Selector机制, 采用IO事件模型来处理.唯一可惜的是这个单线程处理. 当遇到handler里有阻塞的操作时, 会导致整个服务被阻塞住.(需使用TFramedTransport数据传输方式)

  • THsHaServer: 同步半异步的服务模型,一个单独的线程用来处理网络I/O,一个worker线程池用来进行消息的处理.

  • TThreadedSelectorServer: 有一条线程专门负责accept,若干条Selector线程处理网络IO,一个Worker线程池处理消息(使用得最多)。

相关文档

参考文档