本文为学习记录,如有侵权,请联系删除
为何需要使用Thrift协议?
首先,Thrift是基于socket的,通过tcp协议来实现,即应用层协议
其次,现在大多使用分布式来进行架构,而实现分布式架构的方式主要是远程过程调用(Remote Procedure Call,RPC)。
简化版分布式架构:
rpc主要需要解决的是:
传输什么样的数据,用哪种协议
哪种方式数据交换的效率好
服务端如何处理请求
一个rpc协议一般可以分为 传输协议 和 序列化协议。 传输协议一般是指 元信息 (我这里理解为类似与报文头),而序列化协议是指将 参数序列化(我这里理解为将报文体序列化,方便传输)
thrift协议被设计成既包含传输协议也包含序列化协议,可以很好的解决上述问题。
Thrift使用原理
结构图:
thrift 其实是包括了框架部分和协议部分。整体架构图如下:
从上图我们可以看到,在一个层级中可以选择自己想要的,最后组合出来一个按照我们意愿的产物,也即 支持面很广。
thrift核心组件:
TProtocol 协议和编解码组件(主要是对报文进行编码、序列化)
TTransport 传输组件
TProcessor 服务调用组件
TServer,Client服务器和客户端组件
使用流程:
步骤如上:
创建对应的IDL文件,即(xxx.thrift)
使用IDL编译工具,将IDL文件生成对应的代码
创建对应的服务端和客户端代码(也可以在步骤2中一起生成)
Thrift 特性:
1. 基于二进制的高性能的编解码框架
二进制:二进制协议比文本形式的协议,发送的数据量肯定是更小,传输效率更高的。
编解码框架:作为rpc框架,一般都是封装了方法名、方法参数这些方法调用的方式。客户端发送消息。服务端接收到客户端的消息之后,解码消息,然后通过方法调用模型来完成实际服务器端业务方法的调用。
横向对比 :对比HTTP性能,
从文章的测试数据可以看到,thrift的性能要远高于http。但是thrift协议的设计是比较复杂的,也不是不加判断的就使用thrift协议。基于简单程度可以选择http,基于性能可以选择thrift协议。
产生这种现象的原因,一部分原因是因为非阻塞IO的机制,另一部分是http是无状态无连接的协议,而thrift协议使用前需要先建立连接,在多次传输中占有优势。
纵向对比:同比gRPC
gRPC框架的传输层是使用的http2,对比thrift主要是有更加多、更健全的文档,更优秀的鉴权功能等。但是支持广度、性能还是thrift要更胜一筹。
2. 基于NIO的底层通信
NIO,全称是NoneBlocking IO,非阻塞IO,和BIO(Blocking IO,阻塞IO)相对应。具体的解释和实现机制,不是这篇文章的重点,有兴趣可以自行查阅一下。可以看看篇文章。
3. 相对简单的服务调用模型
4. 使用IDL支持跨平台调用
小结:
总结来说就是高性能、传输速度快、支持面广,可以跨平台使用
协议结构
Thrift请求相应模型
可以将这里的message和struct理解为报文头和报文体的关系。注意图中的1、2不是发送了两次请求,应该理解为一组字节流,2紧跟在1后面,4紧跟在3后面。
Message
Message中主要包含Name,Message Type,Sequence ID等数据。
Name:为调用的方法名
Message Type:有Call, OneWay, Reply, Exception四种,在实际传递的时候,传递的是Type ID,这四种Type对应的Type ID如下
Call ---> 1
OneWay ---> 2
Reply ---> 3
Exception ---> 4
其中Call、OneWay用于Request, Reply、 Exception用于Response中。 四者的含义如下:
Call: 调用远程方法,并且期待对方发送响应。
OneWay: 调用远程方法,不期待响应。即没有步骤3,4。
Reply: 表明处理完成,响应正常返回。
Exception:表明出理出错。
- Sequence ID : 序列号, 有符号的四字节整数。在一个传输层的连接上所有未完成的请求必须有唯一的序列号,客户端使用序列号来处理响应的失序到达,实现请求和响应的匹配。服务端不需要检查该序列号,也不能对序列号有任何的逻辑依赖,只需要响应的时候将其原样返回即可。可以类比于tcp滑动窗口中的序列号来理解。
Struct
在上面的Thrift请求响应模型中,有两种Struct:
Request Struct
Response Struct
这两种Struct的结构是一样的,都是由多个Field组成
具体可参考这里
序列化协议
序列化本质是把 rpc的参数 转换成 一段连续的二进制流,进一步还可以对参数进行压缩(可选),使传输的流量更少。
Thrift支持多种序列化协议,常用的有: Binary、Compact、JSON。常用的是Binary和Compact。前者基本就是将参数顺序的写入连续的二进制流,后者对参数进行了一定的压缩。本文主要分析Binary,Compact有兴趣可自行查阅。
Binary序列化
binary序列化是一种二进制的序列化方式,不可读。
Message的序列化
Message的序列化分为两种,strict encoding和old encoding。 在有些实现中,会通过 检查Thrift消息的第一个bit来判断使用了那种encoding:
1 —-> strict encoding (会带上Version信息,默认模式)
0 —-> old encoding
官网上面是这样写的:
Thrift Protocols are stream oriented by design. There is no need for any explicit framing. For instance, it is not necessary to know the length of a string or the number of items in a list before we start serializing them. Some of the protocols available for majority of the Thrift-supported languages are:
- binary: Fairly simple binary encoding – the length and type of a field are encoded as bytes followed by the actual value of the field.
- compact: Described in [THRIFT-110](https://issues.apache.org/jira/browse/THRIFT-110)
- json
猜想使用三个bit的目的是为了方便后续扩展。
Struct的序列化
Struct装的是Thrift通信的实际参数,一个Struct由很多基本类型组合而成,要了解Struct怎么序列化的必须知道这些基本类型的序列化。
Binary序列化中各种字段类型的类型标记和占用大小:
数据类型 | 类型标志(一个字节) | 值 |
---|---|---|
bool | 2 | 一个字节 |
byte | 3 | 一个字节 |
double | 4 | 八个字节 |
i16 | 6 | 两个字节值 |
i32 | 8 | 四个字节值 |
i64 | 10 | 八个字节值 |
string | 11 | 四个字节数据长度+数据的值 |
binary | 11 | 四个字节数据长度+数据的值 |
struct | 12 | 多个连续的field数据+一个字节停止符(0) |
map | 13 | 一个字节的key类型标志+一个字节的val类型标志+四个字节的数据长度+数据的值(key+val) |
set | 14 | 一个字节的val类型标志+四个字节的数据长度+数据的值 |
list | 15 | 一个字节的val类型标志+四个字节的数据长度+数据的值 |
序列化后的二进制流格式
[id(2字节) + 类型标志(1字节) + 值] + [id(2字节) + 类型标志(1字节) + 值] + .....
由于二进制流中存在 id序号 和 类型flag, 当server收到协议后,会根据服务端注册的idl, 根据 id序号到二进制流中找对应的buffer, 并会对类型做进一步比对。
Compact
主要是对Binary进行了一点压缩