0%

Thrift 协议讲解

本文主要是介绍 Thrift协议,Thrift 协议发展历史和发展背后的故事!再其次就是介绍如何编解码Thrift协议!如果你也想了解PB,可以看我写的这篇文章: PB协议讲解.

整体介绍

  1. Thrift 协议整体设计比较复杂,但是呢语法支持度也比较高,能满足大部分业务需求,完全可以通过编写IDL把所有接口定义能做的事情都做了!这个是区别于PB的,但是PB规范性要好于Thrift 以及整体的生态支持、社区上都是优与Thrift!

  2. Thrift 包含了RPC的协议层、序列化层!PB只提供了序列化/编码层,需要GRPC/其他RPC框架协议进行传输!

  3. 架构图就不聊了,网上一堆,就一个rpc通信框架其实架构图都一样!下文主要介绍: 消息协议(协议层) + 传输协议(传输层),有兴趣可以看下 KitexGRPCDubbo .

  4. Thrift 是 facebook 开源的一个rpc协议,诞生时间比较早,应该是10年之前了,所以国内互联网公司基本上都用的thrift协议,原因就是出来的比较早的成熟的RPC框架!但是也存在一个问题就是,古老的协议往往不满足现在的服务架构,所以后期也做了进一步的升级,但是老的业务还在跑,升级比较麻烦,也就导致很多公司Thrift并没有用到新特性!

  5. 大家可以看下GO的实现https://github.com/apache/thrift/tree/master/lib/go

  6. Thrift 语法丰富,详细可以看文档:https://thrift.apache.org/docs/ !

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
typedef string Birthday
const Birthday NationalDay='1949-10-01'

// 其他类型 i64 i32 i16 byte double bool binary
struct TestRequest {
1: string Field_name = 'default value' (api.tag='xxxx');
2: required string F_string_required;
3: optional string F_string_optional;
4: list<string> F_list_default;
5: map<string,string> F_map_default;
6: set<string> F_set_default;
7: optional Numberz F_enum = Numberz.Unknown,
}

enum Numberz{
Unknown = 0
ONE = 1
TWO
}

struct TestResponse {
}

service ThriftTest
{
TestResponse Test (1: TestRequest req),
}

消息(编码)协议

主要是详细介绍了消息传输的时候协议的主要编解码方式,注意thrift采用的是大端编码!

1. TBinaryProtocol

消息协议介绍,有两种协议格式!主要是由于历史原因,导致有两种协议并存!下面介绍基参考自官方文档: thrift-binary-protocol

1. 消息协议一(新编码,严格模式)

1
2
3
4
Binary protocol Message, strict encoding, 12+ bytes:
+--------+--------+--------+--------+--------+--------+--------+--------+--------+...+--------+--------+--------+--------+--------+
|1vvvvvvv|vvvvvvvv|unused |00000mmm| name length | name | seq id |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+...+--------+--------+--------+--------+--------+
  • 前两个字节表示版本号:**高位第一个bit固定为1(因为为了区分下面协议二)**,其他17个bit(vvvvvvvvvvvvvvv)表示版本。
  • 第三个字节为无用字节:unused是一个被忽略的字节。
  • 第四个字节为消息类型: mmm是消息类型,一个无符号的 3 位整数,同时高位5bit必须为0(有些SDK会校验)。
1
2
3
4
5
6
7
8
9
// 取头部四个字节: size = readInt32()
// 取消息类型: size & (1<<8)-1
// 取版本号: siez & 0xffff0000, 版本基本就是1,直接硬编码比较就行了

// 消息类型占用3bit
Call: 1
Reply: 2
Exception: 3
Oneway: 4
  • name length:消息名长度,占用4字节!大端!(注意大小端只是针对于number类型,而且大小端转换基本无开销)
  • name: 消息名,utf8编码
  • seq id: 占用四字节,大端!(一般rpc多路复用都有,因为要并发发送请求么,不能同一个连接 发送接收完 A 再发送接收 B请求, 像Http1.1就是PingPong协议,HTTP2也是有一个seq id ,这东西做的简单点就全局自增一个id即可!)

这里补充下位运算,位运算中 &一般作用就是取值, |一般作用就是Set值!

2. 消息协议二 (旧编码,非严格模式)

这个假如客户端/server端开启了严格模式,则不能兼容次协议

1
2
3
4
Binary protocol Message, old encoding, 9+ bytes:
+--------+--------+--------+--------+--------+...+--------+--------+--------+--------+--------+--------+
| name length | name |00000mmm| seq id |
+--------+--------+--------+--------+--------+...+--------+--------+--------+--------+--------+--------+
  • name length(四字节): 这里为了兼容上面协议一,所以高位第一个bit必须为0!也就是name length必须要有符号的正数!
  • name:消息名称
  • mmm: 消息类型,同上
  • seq id: 消息ID

3. 类型编码协议

这个协议比较简单,就是个基本的编码协议!!!其实就是一个最简单的 tlv 编码!

1. struct
1
2
3
4
5
6
7
8
9
Binary protocol field header and field value:
+--------+--------+--------+--------+...+--------+
|tttttttt| field id | field value |
+--------+--------+--------+--------+...+--------+

Binary protocol stop field:
+--------+
|00000000|
+--------+
  • tttttttt字段类型,带符号的 8 位整数。一个字节!
  • field idfield-id,一个大端序的有符号 16 位整数。两个字节!
  • field-value编码的字段值!
1
2
3
4
5
6
7
8
9
10
11
12
# 字段类型
BOOL, encoded as 2
I8, encoded as 3
DOUBLE, encoded as 4
I16, encoded as 6
I32, encoded as 8
I64, encoded as 10
BINARY, used for binary and string fields, encoded as 11
STRUCT, used for structs and union fields, encoded as 12
MAP, encoded as 13
SET, encoded as 14
LIST, encoded as 15
2. list / set
1
2
3
4
Binary protocol list (5+ bytes) and elements:
+--------+--------+--------+--------+--------+--------+...+--------+
|tttttttt| size | elements |
+--------+--------+--------+--------+--------+--------+...+--------+
  • tttttttt表示元素类型,编码为 int8
  • size 表示全部元素个数,编码为 int32,仅正值
  • elements 全部元素,顺序排列
3. map
1
2
3
4
Binary protocol map (6+ bytes) and key value pairs:
+--------+--------+--------+--------+--------+--------+--------+...+--------+
|kkkkkkkk|vvvvvvvv| size | key value pairs |
+--------+--------+--------+--------+--------+--------+--------+...+--------+
  • kkkkkkkk是关键元素类型,编码为 int8
  • vvvvvvvv是值元素类型,编码为 int8
  • size是 map的size,编码为 int32,仅正值
  • key value pairs是编码的键和值,意思就是先读key,再读value,再读key,再读value
4. string/binary
1
2
3
4
Binary protocol, binary data, 4+ bytes:
+--------+--------+--------+--------+--------+...+--------+
| byte length | bytes |
+--------+--------+--------+--------+--------+...+--------+
  • byte length是字节数组的长度
  • bytes是字节数组的字节

string 类型就是utf-8 编码为字节流,然后传输的时候用 binary 类型即可!

5. 其他类型

基本类型就是占用固定字节!比如boolean一个字节,i64是8个字节之类的!! 枚举等同于i32!

2. TCompactProtocol (改进协议,和PB基本类似!)

名字显而易见,就是用来压缩的,压缩算法和pb很像,就是 zigzag(整数压缩算法)+ varint 算法, 这俩东西可以看我讲的PB协议的文章 ,不过也做了很多取巧的地方,下面内容基本来自官方文档: thrift-compact-protocol

1. 消息协议

1
2
3
4
Compact protocol Message (4+ bytes):
+--------+--------+--------+...+--------+--------+...+--------+--------+...+--------+
|pppppppp|mmmvvvvv| seq id | name length | name |
+--------+--------+--------+...+--------+--------+...+--------+--------+...+--------+

这个协议也很简单,就是更紧凑

  • 第一个字节: 协议ID,TCompactProtocol ID为 130,二进制编码为 10000010
  • 第二个字节: version + type, 其中version是低5bit,type是高3bit , 其中 COMPACT_VERSION = 1
  • seq id 4字节,var int 编码
  • name len 为4字节,也是var int 编码
  • name: 消息名称
1
2
3
4
5
// 消息类型
Call: 1
Reply: 2
Exception: 3
Oneway: 4

2. 类型编码协议

1. struct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Compact protocol field header (short form) and field value:
+--------+--------+...+--------+
|ddddtttt| field value |
+--------+--------+...+--------+

Compact protocol field header (1 to 3 bytes, long form) and field value:
+--------+--------+...+--------+--------+...+--------+
|0000tttt| field id | field value |
+--------+--------+...+--------+--------+...+--------+

Compact protocol stop field:
+--------+
|00000000|
+--------+
  • dddd是字段 delta,一个无符号的 4 位整数,严格正数。
  • tttt是字段类型 id,一个无符号的 4 位整数。
  • field id字段 id,一个有符号的 16 位整数,编码为 zigzag int!
  • field-value编码的字段值。
1
2
3
4
5
6
7
8
9
10
11
12
13
# 字段的类型,其中下面的 map/list 类型,也很简单就是可以把BOOLEAN_FALSE当作BOOL类型即可,就不重复写了!
BOOLEAN_TRUE, encoded as 1
BOOLEAN_FALSE, encoded as 2
I8, encoded as 3
I16, encoded as 4
I32, encoded as 5
I64, encoded as 6
DOUBLE, encoded as 7
BINARY, used for binary and string fields, encoded as 8
LIST, encoded as 9
SET, encoded as 10
MAP, encoded as 11
STRUCT, used for both structs and union fields, encoded as 12

这里比较特殊的就是:bool类型是不占用field value字节的,其次就是有两种编码协议,它的原理确实比pb 还要取巧,哈哈哈!

首先我们在编码的时候,field_id 一般都是顺序自增的!也就是基本上是1,2,3…n , 这时候由于type占用4bit,此时可以用剩余的4bit存储field_id的增量即可!这里增量处理说实话比PB编码还要巧妙!

1
2
3
4
5
6
7
8
9
10
// start 
last_field_id=0

// write field
if field_id > last_field_id & field_id-last_field_id<=15 {
// use delta
}else{
// use normal
}
last_field_id=field_id

例如下面这个例子一个结构体定义的field_id为1,2,3,4,5,30,31,32

1
2
3
4
5
6
7
8
9
10
11
+--------+--------+--------+--------+
|0000tttt| field id| field value |
+--------+--------+--------+--------+
0000tttt, 1 # 1 (懒得用二进制编码表示了)
0001tttt, # 2 (这里用0001是因为字段ID增量是1)
0001tttt, # 3
0001tttt, # 4
0001tttt, # 5
0000tttt, 30 # 30(这里重制是因为30-5>15了,所以重置了)
0001tttt, # 31
0001tttt, # 32

这里还有个细节点就是 bool 类型!bool 类型是不占用 field_value 的!

2. list/set
1
2
3
4
5
6
7
8
9
Compact protocol list header (1 byte, short form) and elements:
+--------+--------+...+--------+
|sssstttt| elements |
+--------+--------+...+--------+

Compact protocol list header (2+ bytes, long form) and elements:
+--------+--------+...+--------+--------+...+--------+
|1111tttt| size | elements |
+--------+--------+...+--------+--------+...+--------+
  • ssss是大小,4 位无符号整数,值0-14
  • tttt是元素类型,一个 4 位无符号整数
  • size是大小,var int (int32),正值15或更高
  • elements是编码元素

这个其实我就不用说了,假如size<=15的话那么可以使用第一种了!

3. map
1
2
3
4
5
6
7
8
9
Compact protocol map header (1 byte, empty map):
+--------+
|00000000|
+--------+

Compact protocol map header (2+ bytes, non empty map) and key value pairs:
+--------+...+--------+--------+--------+...+--------+
| size |kkkkvvvv| key value pairs |
+--------+...+--------+--------+--------+...+--------+
  • size是大小,一个 var int (int32),严格的正值
  • kkkk是关键元素类型,一个 4 位无符号整数
  • vvvv是值元素类型,一个 4 位无符号整数
  • key value pairs是编码的键和值

其实这个更不用说了,就是当size等于0时,就写入第一种协议!

4. binary、string
1
2
3
4
Binary protocol, binary data, 1+ bytes:
+--------+...+--------+--------+...+--------+
| byte length | bytes |
+--------+...+--------+--------+...+--------+
  • byte length 采用varint, 四字节编码
  • bytes body

string类型传输的时候是utf8编码的binary类型!

5. 其他类型

占用固定字节,采用varint编码

消息(传输)协议

其实上面部分讲到的消息协议已经是一个完整的协议,你直接基于tcp流发送请求响应协议即可!但是为啥这块还跑出个传输协议!对的上面协议其实是有局限性的!因此下列列出了几个传输协议!下面这块内容基本来自于官方文档: rpc 协议介绍

Framed 协议 & Bufferd(Unframed) 协议 (传统RPC协议)

Bufferd协议就是我们需要不断的读取socket,然后进行协议拆解!

现在问题就是异步比较流行,所以需要再发送数据的时候采用Framed编码,先写size,再写payload!当server端处理的时候可以根据frame_size来获取payload,然后交给 processs处理!官方的解释是为了支持 async 编程!其实我觉得就是为了更加高效罢了,不需要把整个包解析出来,也就是传输层可以节省很多性能开销!

image-20220319214241315

其实这个协议是有问题的,就是假如消息体过大的话!那么内存开销就比较大,因为需要先写到内存里,然后再头部加四个字节!

THeader 协议

随着技术/需求/业务不断发展,传统的架构不能满足业务快速发展,普通的传输协议不满足于现状,因此当出现service mesh的新一代微服务架构的时候,需要通过Mesh去做流量治理(而不是框架),我们需要在协议里注入一些服务信息进行流量治理等等,因此后来Facebook推出了THeaderProtocol。其实原来的老协议也能做,那就是可以在 message中定义一些 公共字段来注入流量信息,但是存在问题就是需要把整个包解出来,浪费性能,而且比较麻烦!注意这里讲到的THeaderProtocol是有区别于字节内部的TTHeader 协议的以及Mesh协议等!

协议如下:

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
  0 1 2 3 4 5 6 7 8 9 a b c d e f 0 1 2 3 4 5 6 7 8 9 a b c d e f
+----------------------------------------------------------------+
| 0| LENGTH |
+----------------------------------------------------------------+
| 0| HEADER MAGIC | FLAGS |
+----------------------------------------------------------------+
| SEQUENCE NUMBER |
+----------------------------------------------------------------+
| 0| Header Size(/32) | ...
+---------------------------------

Header is of variable size:
(and starts at offset 14)

+----------------------------------------------------------------+
| PROTOCOL ID (varint) | NUM TRANSFORMS (varint) |
+----------------------------------------------------------------+
| TRANSFORM 0 ID (varint) | TRANSFORM 0 DATA ...
+----------------------------------------------------------------+
| ... ... |
+----------------------------------------------------------------+
| INFO 0 ID (varint) | INFO 0 DATA ...
+----------------------------------------------------------------+
| ... ... |
+----------------------------------------------------------------+
| |
| PAYLOAD |
| |
+----------------------------------------------------------------+

具体细节我就不讲了,其实就是可以携带一些header信息,在传输的时候可以携带上,然后头部有头部编码的协议!

header信息可以做些什么呢,它包含一些 trace、acl、env、服务优先级之类的!

其次就是mesh其实只需要关注于这些信息,payload部分它不关心,所以他不需要解pyload 部分!

如何解码

上面介绍完了,我们发现Thrift 编码协议+传输协议有很多,那么问题是我一个client/server需要全部支持,怎么解码呢,一个消息来了如何解码是麻烦事? 这里我简单介绍一下我的大体实现逻辑!仅供参考:源码

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// buffered reader
type reader interface {
io.Reader
Peek(int) ([]byte, error)
}

// Unframed: Buffered协议
// Framed: Framed协议
// Header: 开源的THeader协议
// TTHeader: 字节的TTHeader协议
// MeshHeader: 字节的MeshHeader协议
func GetProtocol(ctx context.Context, reader reader) (Protocol, error) {
if IsUnframedHeader(reader, 0) {
return UnframedHeader, nil
}
if IsUnframedHeader(reader, FrameHeaderSize) {
return FramedHeader, nil
}
if IsUnframedBinary(reader, 0) {
return UnframedBinary, nil
}
if IsUnframedBinary(reader, FrameHeaderSize) {
return FramedBinary, nil
}
if IsUnframedUnStrictBinary(reader, 0) {
return UnframedUnStrictBinary, nil
}
if IsUnframedUnStrictBinary(reader, FrameHeaderSize) {
return FramedUnStrictBinary, nil
}
if IsUnframedCompact(reader, 0) {
return UnframedCompact, nil
}
if IsUnframedCompact(reader, FrameHeaderSize) {
return FramedCompact, nil
}
if kitex.IsTTHeader(reader) {
meatInfo := GetMateInfo(ctx)
size, err := kitex.ReadTTHeader(reader, meatInfo)
if err != nil {
return UnknownProto, err
}
if IsUnframedBinary(reader, size) {
_ = commons.SkipReader(reader, size)
return UnframedBinaryTTHeader, nil
}
if IsFramedBinary(reader, size) {
_ = commons.SkipReader(reader, size)
return FramedBinaryTTHeader, nil
}
}
if kitex.IsMeshHeader(reader) {
meatInfo := GetMateInfo(ctx)
size, err := kitex.ReadMeshHeader(reader, meatInfo)
if err != nil {
return UnknownProto, err
}
if IsUnframedBinary(reader, size) {
_ = commons.SkipReader(reader, size)
return UnframedBinaryMeshHeader, nil
}
if IsFramedBinary(reader, size) {
_ = commons.SkipReader(reader, size)
return FramedBinaryMeshHeader, nil
}
}
return UnknownProto, thrift.NewTProtocolExceptionWithType(
thrift.UNKNOWN_PROTOCOL_EXCEPTION,
errors.New("unknown protocol"),
)
}

参考

本人坚持原创技术分享,如果你觉得文章对您有用,请随意打赏! 如果有需要咨询的请发送到我的邮箱!