背景
Thrift是一种接口描述语言和二进制通讯协议。原由Facebook于2007年开发,2008年正式提交Apache基金会托管,成为Apache下的开源项目。
Thrift是一个RPC通讯框架,采用自定义的二进制通讯协议设计。相比于传统的HTTP协议,效率更高,传输占用带宽更小。另外,Thrift是跨语言的。Thrift的接口描述文件,通过其编译器可以生成不同开发语言的通讯框架。
安装
在Mac OS X系统下,可以直接使用homebrew安装thrift,如下:
future@FuturedeMacBook-Pro ~ % brew install thrift
Updating Homebrew...
==> Downloading https://mirrors.aliyun.com/homebrew/homebrew-bottles/bottles/thr
######################################################################## 100.0%
==> Pouring thrift-0.13.0.catalina.bottle.1.tar.gz
🍺 /usr/local/Cellar/thrift/0.13.0: 95 files, 6.8MB
安装完成后,输入thrift -version
查看,如下所示表示安装成功:
future@FuturedeMacBook-Pro ~ % thrift -version
Thrift version 0.13.0
编写代码
创建接口描述文件,Thrift的语言规范参考Thrift IDL。
service Demo {
string greeting(1: required string name)
}
保存文件为demo.thrift,通过Thrift命令thrift --gen java demo.thrift
编译java语言的RPC框架。命令执行成功后,会在目录下生成如下文件:
java中实际使用Thrift时,需要引入相关jar包。为了方便起见,使用IDE创建maven工程,并引入Thrift基础包。
修改pom.xml文件,增加部分<properties/>
和<dependencies/>
配置。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>ThriftDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.thrift/libthrift -->
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.13.0</version>
</dependency>
</dependencies>
</project>
将生成的Demo.java文件拷贝到src/main/java目录下面。简单起见,我们不创建package,直接使用root package。
创建DemoImpl.java、Server.java、Client.java文件备用。调整后的文件结构如下:
在DemoImpl.java中编写服务逻辑,示例代码如下:
import org.apache.thrift.TException;
public class DemoImpl implements Demo.Iface {
@Override
public String greeting(String name) throws TException {
return "Hello " + name;
}
}
在Server.java中编写服务端程序:
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TTransportException;
public class Server {
public static void main(String[] args) throws TTransportException {
TProcessor tProcessor = new Demo.Processor<>(new DemoImpl());
TServerSocket serverSocket = new TServerSocket(8080);
TServer.Args tArgs = new TServer.Args(serverSocket);
tArgs.processor(tProcessor);
tArgs.protocolFactory(new TBinaryProtocol.Factory());
TServer server = new TSimpleServer(tArgs);
server.serve();
}
}
在Client.java中编写客户端程序:
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
public class Client {
public static void main(String[] args) throws TException {
TTransport transport = new TSocket("localhost", 8080, 0);
TProtocol protocol = new TBinaryProtocol(transport);
Demo.Client client = new Demo.Client(protocol);
transport.open();
String result = client.greeting("Thrift");
System.out.println(result);
}
}
测试验证
为了方便测试Thrift协议,我们在验证之前开启抓包软件,如下:
表达式tcp && (tcp.srcport == 8080 || tcp.dstport == 8080)
是为了过滤出通过tcp协议传入8080端口或从8080端口发送的数据。因为Thrift的二进制协议底层实现是tcp,另外我们的服务指定了监听端口8080。
启动服务端后,再通过debug运行客户端。
程序执行完transport.open()
这句后,网络上会出现典型的TCP建立链接的三次握手信令交互:
执行String result = client.greeting("Thrift")
这句时,网络上显示出实际RPC调用时的网络传输数据:
在IDE的控制台上,会打印RPC调用的返回结果:
二进制协议
关于Thrift TBinaryProtocol的解析可以参考这篇文章Thrift序列化协议浅析
在抓包工具中,看一下请求时,通过网络发送给服务端的数据内容:
二进制数据内容如下:
80 01 00 01 00 00 00 08
67 72 65 65 74 69 6e 67 00 00 00 01 0b 00 01 00
00 00 06 54 68 72 69 66 74 00
前十六位为80 01
,翻译成二进制是1000 0000 0000 0001
,对应协议版本号。
后面十六位为00 01
,其中的前8位按照协议规定未被使用,后8位中的最后三位指定了消息类型。
消息类型如下表:
类型 | 二进制 | 十进制 |
---|---|---|
Call | 001 | 1 |
Reply | 010 | 2 |
Exception | 011 | 3 |
Oneway | 100 | 4 |
示例中的这三位是001
,对应消息类型是Call类型。
消息类型段后面的32位数据是00 00 00 08
。根据协议规定,这32个bit定义了服务名称的长度。因此可知,调用的服务名称是8个字节的长度。
后面的8个字节的数据为67 72 65 65 74 69 6e 67
,采用的是UTF-8编码格式。通过UTF-8编码转换工具可查看实际传入内容是greeting。
在名称后面的32个bit定义了消息的seq id,是一个从1自增的整形数。在示例中这32位是00 00 00 01
,消息的序列号是1。
在这之后传输的就是具体的数据了。在请求类型的消息中,其实就是函数调用的入参。格式可以参见Thrift的TBinaryProtocol二进制协议分析
数据解析时,默认先读取一个字节,确定数据类型。数据类型在源码中定义有如下几类:
public final class TType {
public static final byte STOP = 0;
public static final byte VOID = 1;
public static final byte BOOL = 2;
public static final byte BYTE = 3;
public static final byte DOUBLE = 4;
public static final byte I16 = 6;
public static final byte I32 = 8;
public static final byte I64 = 10;
public static final byte STRING = 11;
public static final byte STRUCT = 12;
public static final byte MAP = 13;
public static final byte SET = 14;
public static final byte LIST = 15;
public static final byte ENUM = 16;
}
示例中这个字节是0b
,也就是00001001
,对应十进制是11。因此,对应的数据类型是string类型。
数据类型的8个bit之后,是2个字节的编号,指明这段数据在结构体中的位置。
示例中是00 01
,也就是十进制的1。这和我们在Demo.thrift中string greeting(1: required string name)
这句里面的1,是一致的。
如果是string类型的话,在2个字节的编号后,会有4个字节给出string的长度。
示例中这段是00 00 00 06
,说明这个string的长度是6。
其后的6个字节为54 68 72 69 66 74
,对应的是Thrift
。
最后的00
,表示的是STOP
,也就是结束位。
汇总起来,整个数据内容如下:
|- 版本 -|- 消息类型 -|
| 80 01 | 00 01 |
|- 服务名长度 -|- greeting -|
| 00 00 00 08| 67 72 65 65 74 69 6e 67|
|- 序列号:1 -|
| 00 00 00 01 |
|- 类型:STRING -| 编号:1 | 长度:6 |- Thrift -|
| 0b | 00 01 |00 00 00 06 54 68 72 69 66 74 |
|- 类型:STOP -|
| 00 |
总结
-
Thrift使用的二进制协议是自定义协议。数据发送方需要根据接口定义文件生成二进制数据流,数据接收方也需要根据接口定义文件将二进制流解析成对象。所以Thrift的二进制协议不是自解释型的协议。在序列化和反序列化时,都需要接口定义文件协助。(JSON是自解释型的,只要满足JSON数据格式,就能被程序正确地反序列化,这点Thrift的二进制协议做不到)。
-
Thrift底层基于TCP,支持TCP长连接调用,性能比HTTP协议会高很多。
-
看起来RPC调用没有做同步处理,因此调用过程可能是非线程安全的。这点在实际使用中需要多加注意。