线缆编码完全指南)
Apache Thrift 二进制协议Binary Protocol线缆编码完全指南【免费下载链接】thriftApache Thrift项目地址: https://gitcode.com/GitHub_Trending/thr/thrift本文以 Apache Thrift 官方协议规范 doc/specs/thrift-binary-protocol.md 为核心系统讲解 Thrift 经典binary protocol在网络上传输时的字节级编码规则从基础类型、消息头、Struct 字段到 List/Set/Map 容器的布局与字节序并结合仓库内 C、Python 等语言的真实实现如 TBinaryProtocol.tcc、TBinaryProtocol.py逐一印证。读完本文你将能够手工构造/解析 Thrift 二进制报文、诊断跨语言互操作问题并理解 strict 模式、大小限制与大小端切换等关键配置的实际影响。概述什么是 Thrift Binary Protocolbinary protocol是 Thrift 最古老、使用最广泛的线缆编码协议。本文档描述的编码事实主要基于 Apache Thrift Java 实现0.9.1 与 0.9.3但所有符合规范的实现行为应当一致。核心设计原则是简单直接地按字节顺序写出数据不做压缩、不做字段名传输以最少的开销完成序列化。与压缩型协议参见 doc/specs/thrift-compact-protocol.md相比binary protocol 的每个字段、容器头都携带完整的类型标记和定长长度前缀编码和解码都极为直接CPU 开销低代价是线缆体积较大。其内容结构如下Base types基础类型编码Message消息头编码Struct结构体编码List and Set列表与集合编码Map映射编码BNF 记法说明基础类型Base Types编码整数编码大端网络字节序在 binary protocol 中整数一律最高有效字节在前big endian即网络字节序。int8占 1 字节int16占 2 字节int32占 4 字节int64占 8 字节。仓库中的 C 实现直接印证了这一点——TBinaryProtocol.tcc 中writeI16/writeI32/writeI64通过ByteOrder_::toWire16/32/64完成字节序转换而 Python 实现 TBinaryProtocol.py 使用struct.pack(!h/!i/!q)其中的!前缀即明确表示大端字节序。值得注意的例外C 实现允许选择小端序的 binary protocol。这在 TBinaryProtocol.h 中通过模板参数ByteOrder_实现——默认是TNetworkBigEndian另定义了别名TLEBinaryProtocol对应TNetworkLittleEndian。因为当代 CPU 在内存中存整数即为小端序小端编码能带来微小但可感知的性能提升。但必须警惕doc/thrift-threat-model.md 明确指出这是脱离规范off-spec的互操作模式当对端不是 C 时会产生静默数据损坏不应启用。Enum 编码生成的代码先把枚举取ordinal 值再按int32编码。也就是说枚举在线上占用 4 字节编码方式与普通 int32 完全一致。Binary 编码字节数组binary数据按长度前缀 原始字节发送长度前缀本身是网络字节序的有符号 32 位整数必须 0Binary protocol, binary data, 4 bytes: ----------------------------------------...-------- | byte length | bytes | ----------------------------------------...--------C 实现 TBinaryProtocol.tcc 中writeString先写writeI32(size)再写数据体Python 的 TBinaryProtocol.py 同样先writeI32(len(str))。String 编码string先编码为UTF-8然后按上述 binary 的规则发送长度前缀 UTF-8 字节。解码端如 readStringBody会先读长度再按长度读取字节C 实现还尝试通过borrow零拷贝读取。Double 编码double先按 IEEE 754 双精度浮点double format位布局转换为一个int64大多数运行时都提供该转换库binary 与 compact 协议随后都把该 int64 按8 字节大端序编码。C 实现 TBinaryProtocol.tcc 用bitwise_castuint64_t(dub)取位模式再经toWire64写 8 字节Python 用struct.pack(!d, dub)。Boolean 编码bool先转换为int8true编码为1false编码为0。C 的writeBoolTBinaryProtocol.tcc写tmp value ? 1 : 0共 1 字节Python 的 writeBool 同理。读取端则以非 0 即 true处理readBool。UUID 编码uuid类型以16 字节二进制、大端网络序编码。由于长度固定不需要长度前缀字段永远是 16 字节。在某些平台上可能需要字节序转换——例如 Windows 将 GUID 保存在内存布局复杂、与线缆序不同的记录式结构体中。C 的writeUUIDTBinaryProtocol.tcc直接把uuid.data()的 16 字节写入传输层并在注释中提示了端序交换问题TODO 指向 Delphi 实现。Message消息头编码RPC 消息Message有两种编码方式strict严格编码与旧式非严格编码。Strict 编码带版本号12 字节Binary protocol Message, strict encoding, 12 bytes: ------------------------------------------------------------------------...---------------------------------------- |1vvvvvvv|vvvvvvvv|unused |00000mmm| name length | name | seq id | ------------------------------------------------------------------------...----------------------------------------各字段含义vvvvvvvvvvvvvvv版本号无符号 15 位整数固定为 1二进制000 0000 0000 0001其最高位第 32 位为1。unused被忽略的 1 字节。mmm消息类型无符号 3 位整数。前 5 位必须为0——因为部分客户端0.9.1 的 Java 实现已验证会读取整个字节。name length方法名字节长度网络字节序的有符号 32 位整数必须 0。name方法名UTF-8 编码字符串。seq id序列号网络字节序的有符号 32 位整数。旧式非严格编码9 字节Binary protocol Message, old encoding, 9 bytes: ----------------------------------------...------------------------------------------------ | name length | name |00000mmm| seq id | ----------------------------------------...------------------------------------------------旧式编码没有版本号直接是name length name 消息类型(1字节) seq id。两种格式的兼容判定由于name length必须为正数因此其最高位恒为0接收端通过读取到的第一个 32 位整数的符号位即可判断使用的是 strict 格式负数最高位为 1还是旧格式正数。因此使用不同编码变体的服务端与客户端可以透明互通但当 strict 模式被强制开启时旧格式会被拒绝。C 实现中strict_read_与strict_write_分别控制读取与写入的严格性默认值为strict_read_ false、strict_write_ trueTBinaryProtocol.h。写入端 writeMessageBegin 在strict_write_时写VERSION_1 | messageType即0x80010000 | 类型加方法名加 seqid读取端 readMessageBegin 先读 int32若为负则检查sz VERSION_MASK VERSION_1VERSION_1 0x80010000不匹配抛BAD_VERSION若为正且strict_read_开启同样抛BAD_VERSION旧协议客户端在严格模式下。Python 实现 TBinaryProtocol.py 与 C 一致构造参数strictReadFalse, strictWriteTrue写消息时writeI32(VERSION_1 | type)读消息时按sz 0判断版本格式。消息类型取值消息类型编码值如下见 TEnum.h 与 Thrift.py消息类型值Call调用1Reply应答2Exception异常3Oneway单向4Struct结构体编码一个Struct是零个或多个字段的序列后跟一个 stop 字段。每个字段以字段头field header开始后接编码后的字段值。用 BNF 可概括为struct :: ( field-header field-value )* stop-field field-header :: field-type field-id字段顺序与兼容性因为每个字段头都包含 IDL 中定义的field-id字段可以按任意顺序编码。Thrift 的类型系统不可扩展只能编码原始类型与结构体因此解码时遇到未知字段可以安全忽略解码时依据字段类型field-type决定如何解析字段值。字段名不会被编码所以 IDL 中的字段重命名不会影响向前/向后兼容性——这也是二进制协议实现精简设计的关键点。兼容性警示默认的 Java 实现Apache Thrift 0.9.1在解码到与预期 field-type 不符的字段时行为未定义。理论上可以在付出额外检查开销的前提下检测到这种不匹配其他实现可能会执行检查然后选择忽略该字段或返回协议异常TProtocolException。跨版本、跨语言对接时应避免变更字段类型。Union 与 ExceptionUnion的编码与 struct 完全相同额外约束是最多只编码 1 个字段。Exception的编码与 struct 完全相同。字段头与 stop 字段的字节布局Binary protocol field header and field value: --------------------------------...-------- |tttttttt| field id | field value | --------------------------------...-------- Binary protocol stop field: -------- |00000000| --------tttttttt字段类型field-type有符号 8 位整数。field id字段编号大端序的有符号 16 位整数。field-value编码后的字段值。C 的 writeFieldBegin 写 1 字节类型 2 字节 fieldIdwriteFieldStop 写T_STOP。读取端 readFieldBegin 先读类型字节若为T_STOP则字段结束fieldId 置 0否则再读 2 字节 fieldId。字段类型取值表以下是 binary protocol 使用的全部 field-type 取值在 Thrift.py 中可逐一定位到对应常量类型名编码值说明BOOL2布尔I838 位有符号整数别名BYTE/I08DOUBLE4双精度浮点I16616 位有符号整数I32832 位有符号整数I641064 位有符号整数BINARY11用于 binary 与 string 字段别名STRING/UTF7STRUCT12用于 struct 与 union 字段MAP13映射SET14集合LIST15列表UUID16通用唯一标识符另注意STOP 0、VOID 1 保留用于控制流不作为字段值类型出现。List 和 Set 编码List 与 Set 的编码方式完全相同一个声明元素个数与元素类型的头后跟编码后的各个元素。Binary protocol list (5 bytes) and elements: ------------------------------------------------...-------- |tttttttt| size | elements | ------------------------------------------------...--------tttttttt元素类型编码为 int8。size元素个数编码为 int32仅允许正值。elements各元素值。元素类型的取值与 field-type 完全相同见上文 Struct 一节完整列表。C 的 readListBegin /readSetBegin在读入 size 后会做两项检查sizei 0抛NEGATIVE_SIZE若设置了container_limit_且sizei container_limit_抛SIZE_LIMIT。Python 的 readListBegin 同样调用_check_container_length。关于容器大小上限List/Set 的最大大小可配置。默认情况下没有限制即上限为 int32 最大值2147483647。C 中通过setContainerSizeLimit/ 工厂构造参数container_limit_控制TBinaryProtocol.hPython 通过构造参数container_length_limit控制。安全提醒根据 doc/thrift-threat-model.md二进制协议中所有容器binary、list、set、map的尺寸字段都是 32 位有符号 int32。恶意报文可以声明一个 20 亿元素的容器若运维未设置containerSizeLimit运行时将尝试按该声明分配/读取资源——这正是 DoS 防护需要配置容器大小上限的原因。Map 编码Map 的头部声明大小、键元素类型、值元素类型后跟编码后的键值对。BNF 如下map :: key-element-type value-element-type size ( key value )*Binary protocol map (6 bytes) and key value pairs: --------------------------------------------------------...-------- |kkkkkkkk|vvvvvvvv| size | key value pairs | --------------------------------------------------------...--------kkkkkkkk键元素类型编码为 int8。vvvvvvvv值元素类型编码为 int8。sizemap 大小编码为 int32仅允许正值。key value pairs编码后的键与值。元素类型取值同样与 field-type 一致见上文完整列表。C 的 writeMapBegin 依次写键类型、值类型、size共 6 字节读取端 readMapBegin 同样执行负值检查与container_limit_检查。Python 的 writeMapBegin 逐字节写入相同布局。Map 的最大大小同样可配置默认无限制即 int32 最大值2147483647。附本文档使用的 BNF 记法本文所有 BNF 遵循以下约定项后加表示重复该项重复 1 次或多次项后加*表示可选重复该项重复 0 次或多次项之间用|表示选择取第一个匹配的项圆括号()用于对多项分组。结合源码的验证与扩展阅读以上编码规则在仓库各语言实现中高度一致可对照验证CTBinaryProtocol.h类型定义、VERSION_MASK/VERSION_1 常量、string_limit_/container_limit_/strict 配置、TLEBinaryProtocol小端变体与 TBinaryProtocol.tcc全部读写实现含getMinSerializedSize类型最小字节数映射。PythonTBinaryProtocol.pystruct.pack(!...)大端打包、strict 读写逻辑、字符串/容器长度限制与 Thrift.pyTType类型常量与TMessageType消息类型常量。协议类型常量TEnum.hTMessageType: CALL1/REPLY2/EXCEPTION3/ONEWAY4。测试用例AllProtocolTests.cpp 分别对TBinaryProtocol大端与TLEBinaryProtocol小端运行同一套协议往返测试可验证两种字节序下编码/解码的自洽性。理解 binary protocol 是排查 Thrift 跨语言互操作问题如字节序错乱、strict 模式版本不匹配、容器大小上限触发的基础结合 doc/thrift-threat-model.md 中的安全讨论还可以为生产环境正确配置stringSizeLimit、containerSizeLimit与 strict 模式在保持兼容性的同时规避恶意报文风险。【免费下载链接】thriftApache Thrift项目地址: https://gitcode.com/GitHub_Trending/thr/thrift创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考