十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Apache Thrift Compact Protocol 线缆编码规范:从 ZigZag 变长整数到字段压缩的完整实战指南

Apache Thrift Compact Protocol 线缆编码规范:从 ZigZag 变长整数到字段压缩的完整实战指南 后端微服务API设计【免费下载链接】thriftApache Thrift项目地址https://gitcode.com/gh_mirrors/thrift2/thrift点击查看免费下载Thrift 的compact protocol是一种旨在最小化 RPC 线上字节数的二进制编码协议。与 thrift-binary-protocol 相比它通过 ZigZag 变长整数varint、字段号增量压缩、布尔值内联到字段头等手段大幅缩减了传输开销。本文以仓库中的官方规范文档 thrift-compact-protocol.md 为主体骨架逐一解析基础类型、Message、Struct、List/Set、Map 的逐字节编码格式并结合 Apache Thrift 官方库的 Java、C、Python 参考实现TCompactProtocol.java、TCompactProtocol.h、TCompactProtocol.py进行源码级印证。读完本文你将能够独立完成跨语言 Compact 协议报文的手工编码、解码、抓包分析与兼容性排查。1. 协议背景与设计动机Compact protocol 的线上编码约定主要源自 THRIFT-110 A more compact format 提案并以后续 Apache Thrift 库中 Java 实现约 0.9.1 版本为事实基准其余语言的官方实现C、Python、Dart、Swift、Netstd 等在行为上保持一致。其压缩思路可以归纳为两点见 TCompactProtocol.java 的类注释处处使用变长整数variable-length integers最大化利用未使用的位make use of unused bits wherever possible。收益与结构构成直接相关字段越多、嵌套结构越多、短字符串与短集合越多、i32/i64 取值越小压缩收益越明显。反之若全是长字符串和随机大数收益有限。在 Thrift 分层模型中Protocol 层位于 Transport 层之上Protocol 负责把结构化的数据对象序列化为字节流Transport 负责字节流的物理传输。Compact protocol 正是 Protocol 层的一种具体实现同一份编码在任意语言的实现间可直接互操作。2. 基础类型编码2.1 整数编码ZigZag ULEB128 varintCompact protocol 的整数编码采用ZigZag变换后的ULEB128 变长编码即每次按 7 位切分、最高位作续延标志的 varint。int8直接编码为 1 个字节其余整数类型int16/int32/int64先统一转换为int64做 ZigZag 变换后再编码为 varint。ZigZag 变换的核心思想把符号位映射到最低有效位LSB使绝对值小的负数也能获得紧凑编码。例如 0→0-1→11→2-2→3…… 变换公式Scala 表述如下def ToZigzag(n: Long): Long (n 1) ^ (n 63) def FromZigzag(n: Long): Long (n 1) ^ - (n 1)在 Java 参考实现中对应的方法位于 TCompactProtocol.javaprivate long longToZigzag(long l) { return (l 1) ^ (l 63); } private int intToZigZag(int n) { return (n 1) ^ (n 31); }Python 实现的等价代码见 TCompactProtocol.pymakeZigZag/fromZigZag。ULEB128 编码规则从最低有效位开始每次取 7 位编码为一个字节若该字节之后还有后续字节则最高位bit 7置 1否则置 0。规范文档给出的 50399 编码示例50399 11000100 11011111 (LSB) 0000011 0001001 1011111 (7-bit groups) 00000011 10001001 11011111 (add continuation bits) 0x03 0x89 0xDF (hex) → 0xDF 0x89 0x03 (write to ram LSB first)即 50399 在线上实际写出0xDF 0x89 0x03三个字节。Java 实现中writeVarint32/writeVarint64的逻辑位于 TCompactProtocol.java每轮把n 0x7F取出作为当前字节若n ~0x7F ! 0则最高位置 1 并n 7继续。i32 的 varint 占 1–5 字节i64 的 varint 占 1–10 字节。注意varint 在 Compact protocol 中也会直接被用来表示非负数值如容器 size、消息 seq id、字符串长度此时不做 ZigZag 变换。2.2 Enum 编码枚举在生成的代码中取序数值ordinal value然后按int32编码即先 ZigZag 再 varint。2.3 Binary 编码Binary字节数组在线上由「长度前缀 数据」构成Binary protocol, binary data, 1 bytes: --------...----------------...-------- | byte length | bytes | --------...----------------...--------byte length字节数组长度使用varint编码必须 ≥ 0bytes字节数组本体。2.4 String 编码字符串先按UTF-8编码再按 Binary 的方式发送不带 NUL 终止符。Java 实现见 TCompactProtocol.javastr.getBytes(StandardCharsets.UTF_8)后先写 varint 长度再写字节。2.5 Double 编码double先按 IEEE 754 双精度位布局转换为int64绝大多数运行时都提供现成的转换 API然后二进制协议binary protocol按大端序写 8 字节Compact protocol 按小端序写 8 字节。文档特别说明小端序是早期实现的一个 bug但由于兼容性原因最终成为了事实标准de-facto standard。Java 实现的writeDouble通过fixedLongToBytes以小端序写出见 TCompactProtocol.java 与 fixedLongToBytes。2.6 Boolean 编码布尔值的编码取决于出现位置作为结构体字段值时直接编码进字段头见下文 Struct 章节不占额外字节作为 set/list/map 的元素值时按int8编码true为1false为0。2.7 UUID 编码uuid类型按16 字节二进制、大端序期望。在某些平台上可能需要进行字节序转换——例如 Windows 以内存布局复杂的记录结构保存 GUID其布局与标准不同。由于长度固定不需要长度前缀该字段总是恰好 16 字节。Java 实现中writeUuid/readUuid固定读写 16 字节见 TCompactProtocol.java 与 readUuid。3. Message 编码一条 RPC 消息Message在线上以如下格式开头Compact protocol Message (4 bytes): ------------------------...----------------...----------------...-------- |pppppppp|mmmvvvvv| seq id | name length | name | ------------------------...----------------...----------------...--------字段说明pppppppp协议标识protocol id固定为1000 0010即0x82mmm消息类型message type无符号 3 位整数vvvvv版本号version无符号 5 位整数固定为00001即 1seq id序列号有符号 32 位整数varint 编码name length方法名字节长度有符号 32 位整数varint 编码必须 ≥ 0name要调用的方法名UTF-8 编码字符串消息类型取值Call1Reply2Exception3Oneway4即第二个字节的高 3 位放消息类型、低 5 位放版本随后是 varint 化的 seq id 与字符串化的方法名。Java 实现writeMessageBegin见 TCompactProtocol.java其中常量PROTOCOL_ID 0x82、VERSION 1、VERSION_MASK 0x1f、TYPE_MASK 0xE0、TYPE_SHIFT_AMOUNT 5L88-L93。读端readMessageBeginL486-L506会校验协议 id 与版本号若与0x82/1不符抛出TProtocolException。这也解释了为什么 Compact 消息头能携带协议标识它可以与二进制协议在同一端口上共存并按首字节区分。C 实现的writeMessageBegin逻辑完全一致见 TCompactProtocol.tccC 头部常量定义见 TCompactProtocol.h。Python 实现位于 TCompactProtocol.py其注释特别提醒seq id 按「varint」写出varint 只表示非负数写入负数会导致死循环因此实现会对负数 seq id 做偏移转换。4. Struct 编码4.1 结构体总体结构一个Struct是零个或多个字段的序列以stop 字段结束。每个字段由「字段头 编码后的字段值」组成可用如下 BNF 概括struct :: ( field-header field-value )* stop-field field-header :: field-type field-id由于每个字段头都携带字段号field-id由 Thrift IDL 定义字段可以按任意顺序编码。Thrift 类型系统不可扩展只能编码原始类型与结构体因此解码端遇到未知字段时可以安全地直接跳过——字段类型可用于决定如何解析字段值。值得注意的是字段名不会出现在线上因此 IDL 中的字段重命名不会破坏前向/后向兼容。兼容性提醒默认 Java 实现Apache Thrift 0.9.1在解码到「与预期字段类型不符」的字段时行为未定义。理论上可以通过额外检查检测出来其他实现可能执行该检查后选择忽略该字段或返回协议异常TProtocolException。Union与Exception的编码方式与 Struct 完全相同唯一的附加约束是 Union最多只能编码 1 个字段。4.2 字段头短格式与长格式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字段号增量field id delta无符号 4 位整数严格为正tttt字段类型 idfield-type id无符号 4 位整数field id字段号varintint16编码最大字段号为 32767field-value编码后的字段值字段号增量按下式计算current-field-id - previous-field-id若是结构体的第一个字段则就是current-field-id本身。当字段号增量在 1–15含范围内时应使用短格式一个字节同时放下增量与类型否则使用长格式类型字节 varint 字段号。Java 实现的writeFieldBeginInternal精确复刻了这一逻辑见 TCompactProtocol.javaif (field.id lastFieldId_ field.id - lastFieldId_ 15) { // 短格式增量与类型合一字节 writeByteDirect((field.id - lastFieldId_) 4 | typeToWrite); } else { // 长格式类型字节 varint 字段号 writeByteDirect(typeToWrite); writeI16(field.id); }解码端readFieldBeginL529-L562取出类型字节后检查高 4 位是否为 0非 0 即为增量lastFieldId_ modifier为 0 则继续读取 varint 字段号。为了支撑增量计算编解码器使用一个「字段栈」ShortStack记录外层结构体的最近字段号——这正是writeStructBegin/writeStructEnd在线上不产生任何字节、只在栈上做 push/pop 的原因L210-L223。Python 实现的__writeFieldHeader同样实现该增量规则见 TCompactProtocol.py。4.3 字段类型表以下字段类型可以被编码字段头中的tttt取值字段类型编码值BOOLEAN_TRUE1BOOLEAN_FALSE2I83I164I325I646DOUBLE7BINARYbinary 与 string 字段8LIST9SET10MAP11STRUCTstruct 与 union 字段12UUID13由于布尔值拥有两个专用字段类型TRUE/FALSE布尔字段的值被直接编码进字段头其值部分长度为 0 字节。Java 实现中writeFieldBegin遇到TType.BOOL时会把字段信息暂存booleanField_待writeBool时再决定写出BOOLEAN_TRUE还是BOOLEAN_FALSE作为类型字节L231-L238、L301-L311读端readFieldBegin若发现布尔类型则把值预存到boolValue_供readBool直接消费。各语言的类型码常量保持一致Java 见Types内部类L96-L110Python 见CompactType类TCompactProtocol.pyC 见 TCompactProtocol.h。5. List 和 Set 编码List 与 Set 编码方式相同一个「大小 元素类型」的头部随后是各元素的编码。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–14tttt元素类型4 位无符号整数size大小varintint32编码值为15或更大elements编码后的元素当长度在 0–14含范围内时应使用短格式否则长格式第一字节高 4 位固定为1111即0xF0随后跟 varint 的真实大小。元素类型取值如下注意与字段类型的关系见下方说明元素类型编码值BOOL2I83I164I325I646DOUBLE7BINARYbinary 与 string 字段8LIST9SET10MAP11STRUCTstruct 与 union 字段12UUID13注意虽然当前字段类型表和元素类型表看起来几乎相同但不保证未来新增类型后两者仍保持一致——两个编号空间是独立维护的。List/Set 最大大小可配置。默认无上限即上限为 int32 最大值 2147483647。Java 实现writeCollectionBegin见 TCompactProtocol.javasize 14时单字节写出size 4 | type否则写出0xf0 | type后再写 varint 大小。Python 实现见 TCompactProtocol.py。C 实现见 TCompactProtocol.tcc。6. Map 编码Map 的头部包含大小、键类型、值类型随后是编码后的键值对。BNF 如下map :: empty-map | non-empty-map empty-map :: 0 non-empty-map :: size key-element-type value-element-type (key value)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大小varintint32编码严格为正kkkk键元素类型4 位无符号整数vvvv值元素类型4 位无符号整数key value pairs编码后的键与值空 map仅一个字节0x00不写键值类型解码端因此无法得知空 map 的键值类型只能将其当作占位。非空 mapvarint 大小 一个「键类型 4 | 值类型」字节 键值对。元素类型与 List 章节完全一致。Map 最大大小同样可配置默认无上限即 int32 最大值 2147483647。Java 实现writeMapBegin见 TCompactProtocol.javaif (map.size 0) { writeByteDirect(0); } else { writeVarint32(map.size); writeByteDirect(getCompactType(map.keyType) 4 | getCompactType(map.valueType)); }Python 实现见 TCompactProtocol.py。7. 本文使用的 BNF 记号规范文档使用以下 BNF 记号便于精确定义各类容器的文法项后附加表示重复该项重复 1 次或多次项后附加*表示可选重复该项重复 0 次或多次项之间用|表示选择选择第一个匹配的项圆括号(与)用于分组。8. 源码级佐证跨语言实现的一致性Compact protocol 的编码规则在所有官方语言实现中保持一致核心常量与算法可以相互印证语言实现文件关键证据JavaTCompactProtocol.javaPROTOCOL_ID0x82、VERSION1、类型码表Types、delta 字段头、writeCollectionBegin/writeMapBeginCTCompactProtocol.h、TCompactProtocol.tcc相同常量与writeMessageBegin/writeFieldBegin/writeCollectionBegin逻辑PythonTCompactProtocol.pyCompactType类型码表、__writeFieldHeader增量逻辑、writeCollectionBegin/writeMapBeginDartt_compact_protocol.dart同一协议的另一语言实现SwiftTCompactProtocol.swift同上NetstdTCompactProtocol.cs同上从源码结构可以推断协议设计上刻意将「字段类型编号」与「元素类型编号」作为两套平行但当前取值相同的编号空间维护这与规范文档中「不保证未来仍相同」的警告互为印证。9. 安全考量长度限制与 DoS 防护规范文档明确指出 List/Set 与 Map 的最大大小可配置默认无上限int32 最大值。但在不可信网络环境中攻击者可能声明一个巨大的容器大小诱导解码端分配海量内存构成经典的 OOM 拒绝服务Denial of Service攻击面。官方 Java 实现通过TCompactProtocol.Factory提供两个限制参数见 TCompactProtocol.javastringLengthLimit变长字段字符串/binary最多读取的字节数containerLengthLimit容器map/set/list最多读取的元素数。默认均为NO_LENGTH_LIMIT -1即无限制。解码端在readMapBegin/readListBegin/readString等处调用checkContainerReadLength/checkStringReadLengthL721-L742进行校验超限抛出TProtocolException(SIZE_LIMIT)负长度抛出NEGATIVE_SIZE。该防护能力在测试 TestTCompactProtocol.java 中以testOOMDenialOfService用例被专门验证——这从测试层面证实了「容器长度限制」是针对 OOM DoS 的官方安全机制。在自建服务端时建议显式设置这两个限制而不要依赖默认的「无限制」行为。10. 与其他协议的关系Compact protocol 是 Thrift 多种线缆编码之一与 thrift-binary-protocol定长编码、字段名不编码但字段类型与 id 逐字段显式写出和 thrift-json 等协议 并存。它在「节省带宽」与「编解码复杂度」之间做了取舍对小值整数、短字符串、小集合、布尔字段多的负载压缩收益最大对大随机数、长字符串、结构字段号稀疏且增量大的负载收益有限甚至可能略增开销。选择哪种协议应基于实际负载特征与跨语言兼容性要求综合决定而无论选择哪种其线缆格式都遵循本文第 7 节所述的同一套 BNF 文法体系进行描述与实现。赞分享后端微服务API设计【免费下载链接】thriftApache Thrift项目地址https://gitcode.com/gh_mirrors/thrift2/thrift点击查看免费下载相关推荐Apache Thrift Compact 协议编码规范全解析ZigZag、Varint 与结构体字段编码实战Apache Thrift Compact 协议编码规范全解析ZigZag、Varint 与结构体字段编码实战 本文以 Apache Thrift 官方规范文后端RPC框架序列化代码生成Apache Thrift 二进制协议Binary Protocol线缆编码完全指南Apache Thrift 二进制协议Binary Protocol线缆编码完全指南 本文以 Apache Thrift 官方协议规范 doc/specs/后端RPC框架序列化代码生成Zstandard 压缩格式规范深度解读从帧结构到熵编码的完整实现指南Zstandard 压缩格式规范深度解读从帧结构到熵编码的完整实现指南 导读 本文以 Zstandard 官方格式规范 doc/zstd_compressio数据工程上一篇思源宋体TTF深度解析开源字体工程的架构革命与跨平台实战应用下一篇3分钟解锁iOS应用安装自由TrollInstallerX终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表