本文由字节跳动技术团队杨晨曦分享,本文有修订和改动。

1、引言

本文将带你一起初步认识Thrift的序列化协议,包括Binary协议、Compact协议(类似于Protobuf)、JSON协议,希望能为你的通信协议格式选型带来参考。

 
 
技术交流:

(本文已同步发布于:http://www.52im.net/thread-4576-1-1.html

2、系列文章

本文是系列文章中的第 10 篇,本系列总目录如下:

IM通讯协议专题学习(一):Protobuf从入门到精通,一篇就够!

IM通讯协议专题学习(二):快速理解Protobuf的背景、原理、使用、优缺点

IM通讯协议专题学习(三):由浅入深,从根上理解Protobuf的编解码原理

IM通讯协议专题学习(四):从Base64到Protobuf,详解Protobuf的数据编码原理

IM通讯协议专题学习(五):Protobuf到底比JSON快几倍?全方位实测!

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf

IM通讯协议专题学习(七):手把手教你如何在NodeJS中从零使用Protobuf

IM通讯协议专题学习(八):金蝶随手记团队的Protobuf应用实践(原理篇)

IM通讯协议专题学习(九):手把手教你如何在iOS上从零使用Protobuf

IM通讯协议专题学习(十):初识 Thrift 序列化协议》(* 本文)

另外:如果您还打算系统地学习IM开发,建议阅读《新手入门一篇就够:从零开发移动端IM》。

3、 概述

Thrift 是 Facebook 开源的一个高性能,轻量级 RPC 服务框架,是一套全栈式的 RPC 解决方案,包含序列化与服务通信能力,并支持跨平台/跨语言。

Thrift整体架构如图所示:

Thrift 软件栈定义清晰,各层的组件松耦合、可插拔,能够根据业务场景灵活组合。

如图所示:

Thrift 本身是一个比较大的话题,本篇文章不会涉及到Thrift的全部内容,只会涉及到其中的序列化协议。

4、 Binary协议

4.1消息格式

这里通过一个示例对 Binary 消息格式进行直观的展示。

IDL 定义如下:

//接口

service SupService {

SearchDepartmentByKeywordResponse SearchDepartmentByKeyword(

1: SearchDepartmentByKeywordRequest request)

}

//请求

struct SearchDepartmentByKeywordRequest {

1: optional string Keyword

2: optional i32 Limit

3: optional i32 Offset

}

//假设request的payload如下:

{

Keyword: "lark",

Limit: 50,

Offset: nil,

}

4.2编码简图

4.3编码具体内容

抓包拿到编码后的字节流(转成了十进制,方便大家看)。

/* 接口名长度 */         0   0   0    25

/* 接口名 */            83  101  97  114  99  104  68  101  112  97  114  116

109  101  110  116  66  121  75  101  121  119  111

114  100

/* 消息类型 */           1

/* 消息序号 */           0   0   0   1

/* keyword 字段类型 */   11

/* keyword 字段ID*/     0   1

/* keyword len */      0   0   0   4

/* keyword value */    108   97   114   107

/* limit 字段类型 */     8

/* limit 字段ID*/       0   2

/* limit value */      0   0   0   50

/* 字段终止符 */         0

4.4编码含义

1)消息头:

msg_type(消息类型),包含四种类型:

  • 1)Call:客户端消息。调用远程方法,并且期待对方发送响应;
  • 2)OneWay:客户端消息。调用远程方法,不期待响应;
  • 3)Reply:服务端消息。正常响应;
  • 4)Exception:服务端消息。异常响应。

msg_seq_id消息序号):

  • 1)客户端使用消息序号来处理响应的失序到达,实现请求和响应的匹配;
  • 2)服务端不需要检查该序列号,也不能对序列号有任何的逻辑依赖,只需要响应的时候将其原样返回即可。

2)消息体:

消息体分为两种编码模式:

  • 1)定长类型 -> T-V 模式,即:字段类型 + 字段序号 + 字段值;
  • 2)变长类型 -> T-L-V 模式,即:字段类型 + 字段序号 + 字段长度 + 字段值。

具体是:

  • 1)field_type:字段类型,包括 String、I64、Struct、Stop 等;
  • 2)fied_id:字段序号,解码时通过序号确定字段;
  • 3)len:字段长度,用于变长类型,如 String;
  • 4)value:字段值。

字段类型有两个作用:

  • 1)Stop 类型用于停止嵌套解析;
  • 2)非 Stop 类型用于 Skip(Skip 操作是跳过当前字段,会在「常见问题 - 兼容性」进行讲解)。

4.5数据格式

定长数据类型:

变长数据类型:

5、Compact 协议

5.1概述

Compact 协议是二进制压缩协议,在大部分字段的编码方式上与 Binary 协议保持一致。

区别在于整数类型(包括变长类型的长度)采用了先 zigzag 编码 ,再 varint 压缩编码实现,最大化节省空间开销。

那么问题来了,varint 和 zigzag 是什么?

5.2varint 编码

解决的问题:定长存储的整数类型绝对值较小时空间浪费大。

据统计,RPC 通信时大部分时候传递的整数值都很小,如果使用定长存储会很浪费。

举个 🌰,对 i32 类型的 7 进行编码,可以说前面 3 个字节都浪费了:

00000000 00000000 00000000 00000111

解决思路:将整数类型由定长存储转为变长存储(能用 1 个字节存下就坚决不用 2 个字节)

原理并不复杂,就是将整数按 7bit 分段,每个字节的最高位作为标识位,标识后一个字节是否属于该数据。1 代表后面的字节还是属于当前数据,0 代表这是当前数据的最后一个字节。

以 i32 类型,数值 955 为例,可以看出,由原来的 4 字节压缩到了 2 字节:

binary编码:       00000000  00000000  00000011  10111011

切分:        0000  0000000   0000000   0000111   0111011

compact编码:                          00000111  10111011

当然,varint 编码同样存在缺陷,那就是存储大数的时候,反而会比 binary 的空间开销更大:本来 4 个字节存下的数可能需要 5 个字节,8 个字节存下的数可能需要 10 个字节。

5.3zigzag 编码

解决的问题:绝对值较小的负数经过 varint 编码后空间开销较大 举个 🌰,i32 类型的负数(-11)

原码:         10000000  00000000  00000000  00001011

反码:         11111111  11111111  11111111  11110100

补码:         11111111  11111111  11111111  11110101

varint编码:   00001111  11111111  11111111  11111111  11110101

显然,对于绝对值较小的负数,用 varint 编码以后前导 1 过多,难以压缩,空间开销比 binary 编码还大。

解决思路:负数转正数,从而把前导 1 转成前导 0,便于 varint 压缩

算法公式 & 步骤 & 示范:

//算法公式

32位: (n << 1) ^ (n >> 31)

64位: (n << 1) ^ (n >> 63)

/*

* 算法步骤:

* 1. 不分正负:符号位后置,数值位前移

* 2. 对于负数:符号位不变,数值位取反

*/

//示例

负数(-11)

补码:                     11111111  11111111  11111111  11110101

符号位后置,数值位前移:      11111111  11111111  11111111  11101011

符号位不变,数值位取反(21):  00000000  00000000  00000000  00010101

正数(11)

补码:                     00000000  00000000  00000000  00010101

符号位后置,数值位前移(22):  00000000  00000000  00000000  00101010

奇怪的知识:为什么取名叫 zigzag?

因为这个算法将负数编码成正奇数,正数编码成偶数。最后效果是正负数穿插向前。

就像这样:

编码前       编码后

0           0

-1          1

1           2

-2          3

2           4

6、Json 协议

Thrift 不仅支持二进制序列化协议,也支持 Json 这种文本协议。

数据格式:

/* bool、i8、i16、i32、i64、double、string */

"编号": {

"类型": "值"

}

//示例

"1": {

"str": "keyword"

}

/* struct */

"编号": {

"rec": {

"成员编号": {

"成员类型": "成员值"

},

...

}

}

//示例

"1": {

"rec": {

"1": {

"i32": 50

}

}

}

/* map */

"编号": {

"map": [

"键类型",

"值类型",

元素个数,

"键1",

"值1",

...

"键n",

"值n"

]

}

//示例

"6": {

"map": [

"i64",

"str",

1,

666,

"mapValue"

]

}

/* List */

"编号": {

"set/lst": [

"值类型",

元素个数,

"ele1",

"ele2",

"elen"

]

}

//示例

"2": {

"lst": [

"str",

2,

"lark","keyword"]

}

7、修改字段类型导致协议解析不一致的通信问题

现象:A 服务访问 B 服务,业务逻辑短时间处理完,但整个请求 15s 超时,必现。

直接原因:IDL 类型被修改;并且只升级了服务端(B 服务),没升级客户端(A 服务)。

本质原因:string 是变长编码,i64 是定长编码。由于客户端没有升级,所以反序列化的时候,会把 signTime 当做 string 类型来解析。而变长编码是 T-L-V 模式,所以解析的时候会把 signTime 的低位 4 字节翻译成 string 的 length。

signTime 是时间戳,大整数,比如:1624206147902,转成二进制为:

100000000 00000000 00000001 01111010 00101010 00111011 00000001 00111110

低位 4 字节转成十进制为:378 。

也就是要再读 378 个字节作为 SignTime 的值,这已经超过了整个 payload 的大小,最终导致 Socket 读超时。

注:修改类型不一定就会导致超时,如果 value 的值比较小,解析到的 length 也比较小,能够保证读完。

但是错误的解析可能会导致各种预期之外的情况,包括:

  • 1)乱码;
  • 2)空值;
  • 3)报错:unknown data type xxx (skip 异常)。

8、通信协议带来的常见问题

8.1兼容性

1)增加字段:

通过 skip 来跳过增加的字段,从而保证兼容性。

2)删除字段:

编译生成的解析代码是基于 field_id 的 switch-case 结构,语法结构上直接具备兼容性。

3)修改字段名:

不破坏兼容性,因为 binary 协议不会对 name 进行编码。

8.2Exception

Thrift 有两种 Exception:

  • 1)一种是框架内置的异常;
  • 2)一种是 IDL 自定义的异常。

框架内置的异常包括:

  • 1)方法名错误;
  • 2)消息序列号错误;
  • 3)协议错误。

这些异常由框架捕获并封装成 Exception 消息,反序列化时会转成 error 并抛给上层。

逻辑如下:

另一种异常是由用户在 IDL 中自定义的,关键字是 exception,用法上跟 struct 没有太大区别。

8.3optional、require 实现原理

optional 表示字段可填,require 表示必填。

字段被标识为 optional 之后:

  • 1)基本类型会被编译为指针类型;
  • 2)序列化代码会做空值判断,如果字段为空,则不会被编码。

字段被标识为 require 之后:

  • 1)基本类型会被编译为非指针类型(复合类型 optional 和 require 没区别);
  • 2)序列化不会做空值判断,字段一定会被编码。如果没有显式赋值,就编码默认值(默认空值,或者 IDL 显式指定的默认值)。

9、参考资料

[1] Protobuf从入门到精通,一篇就够!

[2] 如何选择即时通讯应用的数据传输格式

[3] 强列建议将Protobuf作为你的即时通讯应用数据传输格式

[4] APP与后台通信数据格式的演进:从文本协议到二进制协议

[5] 面试必考,史上最通俗大小端字节序详解

[6] 移动端IM开发需要面对的技术问题(含通信协议选择)

[7] 简述移动端IM开发的那些坑:架构设计、通信协议和客户端

[8] 理论联系实际:一套典型的IM通信协议设计详解

[9] 58到家实时消息系统的协议设计等技术实践分享

[10] 金蝶随手记团队的Protobuf应用实践(原理篇)

[11] 新手入门一篇就够:从零开发移动端IM

(本文已同步发布于:http://www.52im.net/thread-4576-1-1.html

IM通讯协议专题学习(十):初识 Thrift 序列化协议的更多相关文章

  1. JAVA RPC (三) 之thrift序列化协议入门杂谈

    首先抱歉让大家久等了,最近工作的原因,再加上自己维护koalas rpc利用的大部分时间,一直没腾出空来写这篇文章. 先放出来自研的企业级RPC框架源代码地址,上面有使用方式和部署环境说明,说环境部署 ...

  2. DotNetty网络通信框架学习之初识Netty

    p{ text-align:center; } blockquote > p > span{ text-align:center; font-size: 18px; color: #ff0 ...

  3. 第十六章 IIC协议详解+UART串口读写EEPROM

    十六.IIC协议详解+Uart串口读写EEPROM 本文由杭电网友曾凯峰根据小梅哥FPGA IIC协议基本概念公开课内容整理并最终编写Verilog代码实现使用串口读写EEPROM的功能. 以下为原文 ...

  4. Websocket协议的学习、调研和实现

    本文章同时发在 cpper.info. 1. websocket是什么 Websocket是html5提出的一个协议规范,参考rfc6455. websocket约定了一个通信的规范,通过一个握手的机 ...

  5. 初识TCP/IP协议

    初识TCP/IP协议 TCP/IP 全称是(Transmission Control Protocol / Internet Protocol),传输控制协议/网际协议.TCP/IP定义了电子设备(比 ...

  6. 关于wcf,webservice,webapi或者其他服务或者接口有什么区别 WCF、WebAPI、WebService之间的区别 【转载】HTTP和SOAP完全就是两个不同的协议 WebService学习总结(一)——WebService的相关概念

    wcf,webservice采用的是rpc协议,这个协议很复杂,所以每次要传递.要校验的内容也很复杂,别看我们用的很简单,但实际是frame帮我们做掉了rpc生成.解析的事情webapi遵循是rest ...

  7. TCP/IP协议组学习笔记

    TCP/IP协议族学习笔记: 一.基础概念: (1)TCP(Transmission Control Protocol) 传输控制协议. (2)IP(Internet Protocol)网际协议.IP ...

  8. 图灵学院JAVA互联网架构师专题学习笔记

    图灵学院JAVA互联网架构师专题学习笔记 下载链接:链接: https://pan.baidu.com/s/1xbxDzmnQudnYtMt5Ce1ONQ 密码: fbdj如果失效联系v:itit11 ...

  9. swift学习笔记4——扩展、协议

    之前学习swift时的个人笔记,根据github:the-swift-programming-language-in-chinese学习.总结,将重要的内容提取,加以理解后整理为学习笔记,方便以后查询 ...

  10. LFI、RFI、PHP封装协议安全问题学习

    本文希望分享一些本地文件包含.远程文件包含.PHP的封装协议(伪协议)中可能包含的漏洞 相关学习资料 http://www.ibm.com/developerworks/cn/java/j-lo-lo ...

随机推荐

  1. 12C RAC 故障分析 - ORA-17503: ksfdopn:2 Failed to open file +DATA/EIC1/PASSWORD/pwdeic1.256.957086685

    一.故障描述 Oracle 12C PDBS(PDB NAME = WRYPC) 在note1节点上是MOUNTED状态,在note2节点上是READ WRITE状态.note1节点上启动该PDB长时 ...

  2. DDCA —— 大缓存、虚拟内存:多核缓存、NUCA缓存、页表等

    1. 缓存中的多核问题 1.1 多核系统中的缓存 Intel Montecito缓存 两个 core,每个都有一个私有的12 MB的L3缓存和一个1 MB的L2缓存,图中深蓝色部分均为L3缓存. 在多 ...

  3. Luatools新手必看:从下载开始的保姆级教程!

    ​ 作为由合宙所提供的调试工具,Luatools支持最新固件获取.固件打包.trace打印.单机烧录等功能 此工具适用于合宙所有 4G 模组和 4G + GNSS 模组. 一.下载和安装 (一)运行环 ...

  4. 【原创】PREEMPT-RT 系统cpu使用率周期CPU飙高问题

    PREEMPT-RT 系统cpu使用率周期CPU飙高问题 目录 PREEMPT-RT 系统cpu使用率周期CPU飙高问题 背景 现象 复现条件 原因 解决措施 背景 在22年进行PREEMPT-RT系 ...

  5. 使用wxpython开发跨平台桌面应用,设计系统的登录界面

    一般的系统登统界面,设计好看一些,系统会增色不少,而常规的桌面程序,包括Web上的很多界面,都借助于背景图片的效果来增色添彩,本篇随笔介绍基于WxPython来做一个登录界面效果,并对系统登录界面在不 ...

  6. 做AI运动小程序有哪些解决方案,如何进行选型?

    引言:随着深度学习技术的发展进步,已经不再依赖强大的GPU算力,便可实现AI推理了,让AI技术渗透到了电脑.手机.智能设备等各类设备.体育.健身行业也不例外,阿里体育等IT大厂,推出的乐动力.天天跳绳 ...

  7. 优秀的 Java 程序员所应该知道的 Java 知识

    JDK 相关知识 JDK 的使用 JDK 源代码 JDK 相应技术背后的原理 JVM 相关知识 服务器端开发需要重点熟悉的 Java 技术 Java 并发 Java IO 开源框架 Java 之外的知 ...

  8. golang之http请求库go-resty

    github: https://github.com/go-resty/resty go-resty 特性# go-resty 有很多特性: 发起 GET, POST, PUT, DELETE, HE ...

  9. Docker之修改默认存储路径

    背景:Docker 默认安装的情况下,会使用 /var/lib/docker/ 目录作为存储目录,用以存放拉取的镜像和创建的容器等.不过由于此目录一般都位于系统盘,遇到系统盘比较小,而镜像和容器多了后 ...

  10. 使用 spring stream 发送消息

    为什么使用spring stream ? spring stream 是用来做消息队列发送消息使用的.他隔离了各种消息队列的区别,使用统一的编程模型来发送消息. 目前支持: rabbitmq kafk ...