背景

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位中的最后三位指定了消息类型。

消息类型如下表:

类型二进制十进制
Call0011
Reply0102
Exception0113
Oneway1004

示例中的这三位是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     |

总结

  1. Thrift使用的二进制协议是自定义协议。数据发送方需要根据接口定义文件生成二进制数据流,数据接收方也需要根据接口定义文件将二进制流解析成对象。所以Thrift的二进制协议不是自解释型的协议。在序列化和反序列化时,都需要接口定义文件协助。(JSON是自解释型的,只要满足JSON数据格式,就能被程序正确地反序列化,这点Thrift的二进制协议做不到)。

  2. Thrift底层基于TCP,支持TCP长连接调用,性能比HTTP协议会高很多。

  3. 看起来RPC调用没有做同步处理,因此调用过程可能是非线程安全的。这点在实际使用中需要多加注意。

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐