【protobuf源码探秘】编码、序列化(1)


这里编码数字 1,Varints 只使用了 1 个字节。而正常情况下 int32 将使用 4 个字节存储数字 1。

msb 实际上就起到了 Length 的作用,正因为有了 msb(Length),所以我们可以摆脱原来那种无论数字大小都必须分配四个字节的窘境。通过 Varints 我们可以让小的数字用更少的字节表示。从而提高了空间利用和效率。


负数的 Varints 编码情况

不多说,直接上例子:

int32 val = -1

原码:1000 … 0001 // 注意这里是 8 个字节

补码:1111 … 1111 // 注意这里是 8 个字节

再次复习 Varints 编码:对补码取 7 bit 一组,低位放在前面。

上述补码 8 个字节共 64 bit,可分 9 组(负数的补码和正数不一样)且这 9 组均为 1,这 9 组的 msb 均为 1,最后剩下一个 bit 的 1,用 0 补齐作为最后一组放在最后,最后得到 Varints 编码:

1#1111111 … 0#000 0001 (FF FF FF FF FF FF FF FF FF 01)

注意,因为负数必须在最高位(符号位)置 1,这一点意味着无论如何,负数都必须占用所有字节,所以它的补码总是占满 8 个字节。你没法像正数那样去掉多余的高位(都是 0)。再加上 msb,最终 Varints 编码的结果将固定在 10 个字节。

这也就是说为什么在 protoc 里直接用 int 存储负数不好。


ZigZag 编码

存储负数推荐 sint 族,在使用 sint 的时候,默认采用 ZigZag 编码。

一张图其实就明白了:

在这里插入图片描述

这里的“映射”是以移位实现的。


bool

当 boolVal = false 时,其编码结果为空。

这里是 ProtoBuf 为了提高效率做的又一个小技巧:规定一个默认值机制,当读出来的字段为空的时候就设置字段的值为默认值。而 bool 类型的默认值为 false。也就是说将 false 编码然后传递(消耗一个字节),不如直接不输出任何编码结果(空),终端解析时发现该字段为空,它会按照规定设置其值为默认值(也就是 false)。如此,可进一步节省空间提高效率。


fixed族

Varints 编码在一定范围内是有高效的,超过某一个数字占用字节反而更多,效率更低。如果现在有场景是存在大量的大数字,那么使用 Varints 就不太合适了。具体的,如果数值比 2^56 大的话,fixed64 这个类型比 uint64 高效,如果数值比 2^28 大的话,fixed32 这个类型比 uint32 高效。

example.set_fixed64val(1)

example.set_sfixed64val(-1)

example.set_doubleval(1.2)

// 编码结果,总是 8 个字节

09 # 01 00 00 00 00 00 00 00

11 # FF FF FF FF FF FF FF FF (没有 ZigZag 编码)

19 # 33 33 33 33 33 33 F3 3F

fixed32、sfixed32、float 与 64-bit 同理


不定长数据类型

string、bytes、EmbeddedMessage、repeated。Length-delimited 类型的编码结构为 Tag - Length - Value:

syntax = “proto3”;

// message 定义

message Example1 {

string stringVal = 1;

bytes bytesVal = 2;

}

Example1 example1;

example1.set_stringval(“hello,world”);

example1.set_bytesval(“are you ok?”);

0A 0B 68 65 6C 6C 6F 2C 77 6F 72 6C 64

12 0B 61 72 65 20 79 6F 75 20 6F 6B 3F


repeat

原先的 repeated 字段的编码结构为 Tag-Length-Value-Tag-Length-Value-Tag-Length-Value…,因为这些 Tag 都是相同的(同一字段),因此可以将这些字段的 Value 打包,即将编码结构变为 Tag-Length-Value-Value-Value…

syntax = “proto3”;

// message 定义

message Example1 {

repeated int32 repeatedInt32Val = 4;

repeated string repeatedStringVal = 5;

}

example1.add_repeatedint32val(2);

example1.add_repeatedint32val(3);

example1.add_repeatedstringval(“repeated1”);

example1.add_repeatedstringval(“repeated2”);

22 02 02 03

2A 09 72 65 70 65 61 74 65 64 31 2A 09 72 65 70 65 61 74 65 64 32

repeatedInt32Val 字段的编码结果为:

22 | 02 02 03

22 即 00100010 -> wire_type = 2(Length-delimited), field_number = 4(repeatedInt32Val 字段),02 字节长度为 2,则读取两个字节,之后按照 Varints 解码出数字 2 和 3。


repeated string 不进行默认 packed
  1. 因为int32采用的是varints编码,省去了TLV中的 L,实际上是TV格式的,所以 repeated int32 是 TLVVV 格式的

  2. string采用的是 TLV 编码,故 repeated string 采用的是TLVLVLV格式


嵌套字段

上文没有提及嵌套字段,因为:

依据元信息(即 .proto 文件,使用 protoc 编译时,.proto 文件会被编译成字符串保存在代码 xxx.pb.cc 中)可以区分该字段是否是嵌套字段。简单来说,你是无法直接从 pb 二进制数据直接解码出信息的,一定是需要有 .proto 文件的配合。只是在代码层面, .proto 文件早就在 protoc 的时候就已经以某种形式存在于 protobuf 生成的客户端代码中,代码可以随时拿到 .proto 文件中表达的元信息,例如一个字段是否为嵌套字段。


序列化与反序列化


文章标题写的是源码探秘是吧。是得放点代码出来。

SerializeToString

当某个 Message 调用 SerializeToString 时,经过一层层调用最终会调用底层的关键编码函数 WriteVarint32ToArray 或 WriteVarint64ToArray,整个过程如下图所示:

在这里插入图片描述

inline uint8* CodedOutputStream::WriteVarint32ToArray(uint32 value, uint8* target) {

// 0x80 -> 1000 0000

// 大于 1000 0000 意味这进行 Varints 编码时至少需要两个字节

// 如果 value < 0x80,则只需要一个字节,编码结果和原值一样,则没有循环直接返回

// 如果至少需要两个字节

while (value >= 0x80) {

// 如果还有后续字节,则 value | 0x80 将 value 的最后字节的最高 bit 位设置为 1,并取后七位

*target = static_cast(value | 0x80);

// 处理完七位,后移,继续处理下一个七位

value >>= 7;

// 指针加一,(数组后移一位)

++target;

}

// 跳出循环,则表示已无后续字节,但还有最后一个字节

// 把最后一个字节放入数组

*target = static_cast(value);

// 结束地址指向数组最后一个元素的末尾

return target + 1;

}

// Varint64 同理

inline uint8* CodedOutputStream::WriteVarint64ToArray(uint64 value,

uint8* target) {

while (value >= 0x80) {

*target = static_cast(value | 0x80);

value >>= 7;

++target;

}

*target = static_cast(value);

return target + 1;

}


关于 fixed 族的编码

inline uint8* WireFormatLite::WriteFixed32ToArray(int field_number,

uint32 value, uint8* target) {

// WriteTagToArray: Tag 依然是 Varint 编码,与上一节 Varint 类型是一致的

// WriteFixed32NoTagToArray:固定写四个字节即可

target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);

return WriteFixed32NoTagToArray(value, target);

}

inline uint8* WireFormatLite::WriteSFixed32NoTagToArray(int32 value,

uint8* target) {

return io::CodedOutputStream::WriteLittleEndian32ToArray(

static_cast(value), target);

}

inline uint8* CodedOutputStream::WriteLittleEndian32ToArray(uint32 value,

uint8* target) {

#if defined(PROTOBUF_LITTLE_ENDIAN)

memcpy(target, &value, sizeof(value));

#else

target[0] = static_cast(value);

target[1] = static_cast(value >> 8);

target[2] = static_cast(value >> 16);

target[3] = static_cast(value >> 24);

#endif

return target + sizeof(value);

}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

Kafka进阶篇知识点

image

Kafka高级篇知识点

image

44个Kafka知识点(基础+进阶+高级)解析如下

image

由于篇幅有限,小编已将上面介绍的**《Kafka源码解析与实战》、Kafka面试专题解析、复习学习必备44个Kafka知识点(基础+进阶+高级)都整理成册,全部都是PDF文档**

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
14a7895254671a72faed303032d36.jpg" alt=“img” style=“zoom: 33%;” />

Kafka进阶篇知识点

[外链图片转存中…(img-eEQqppei-1713402602732)]

Kafka高级篇知识点

[外链图片转存中…(img-skcrmmS5-1713402602732)]

44个Kafka知识点(基础+进阶+高级)解析如下

[外链图片转存中…(img-CuT7lXe3-1713402602733)]

由于篇幅有限,小编已将上面介绍的**《Kafka源码解析与实战》、Kafka面试专题解析、复习学习必备44个Kafka知识点(基础+进阶+高级)都整理成册,全部都是PDF文档**

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值