首页 > 其他分享 >protobuf原理(一):编码原理

protobuf原理(一):编码原理

时间:2022-12-12 16:14:01浏览次数:72  
标签:varint 编码 wire 字节 field 原理 message protobuf

protobuf自身是语言无关的,但是它所提供的编译器以及插件机制可以将我们编写的proto文件生成任意语言的代码,所以可以用来做IDL定义服务接口,可以很方便地让个类型的语言接入。

protobuf自身也是序列化协议,将结构体对象序列化为二进制数据。protobuf的编码原理其实在我们protobuf的使用中基本上是用不到的,不过了解其原理更方便我们理解protobuf的优势与调优。

Base 128 Varints

可变宽度的整数是protobuf编码格式的核心。可以将任意一个64位无符号整数编码为1~10个字节,值越小则使用的字节数越少。

varint中的每个字节的最高有效位MSB用作标志位,表示这个字节是否是这个varint的一部分。后7位是有效的数据位,varint就是将其对应的多个字节的后7位来构建的。

varint就是根据数值的大小,以7位为单位进行编码。而7位二进制能够表示的最大数为\(2^7-1\)。编码的过程每次取出7位,根据当前值设置MSB是否为1,来编码成字节。对应的go语言实现编码过程

一个大于\(2^{21}\)小于\(2^{28}\)的值的编码示例如下图
image
根据值的大小我们可以知道需要编码成4个字节,前三个字节都需要将MSB位设置为1,最后一个字节的MSB为0,表示为这个varint的最后一个字节。

message

对于整数有上面的varint编码,但是我们在编写proto文件的时候,一般都是以message为单位来声明的,整数只是message中的一个字段。
例如

message Msg {
	int32 type = 1;
	string msg = 2;
}

message是由键值对组成的,有着各种类型的字段,但是序列化的结果是二进制的,所以这些字段都是需要编码成二进制的,并且最终还要能够根据二进制反序列化回message,所以二进制信息中需要能够获取到如何解码这段二进制信息以及这个数据属于message的哪个字段。

message中的每个键值对都被编码为field-number, wire-type,payload的格式。field-number表示了这段二进制数据属于message的哪个字段,wire-type告诉了解析器payload的长度,这样还可以让不支持新类型的解析器跳过。这种格式有时也叫做叫做

message采用Tag-Length-Value的格式编码成二进制。tag是通过(field_number << 3) | wire_type公式编码的varint。tag中的field_number和wire_type告诉来我们数据属于哪个字段以及采用的编码方式。

通过下面的方式编码tag以及从tag中获取field_number和wire_type。

// DecodeTag decodes the field Number and wire Type from its unified form.
// The Number is -1 if the decoded field number overflows int32.
// Other than overflow, this does not check for field number validity.
func DecodeTag(x uint64) (Number, Type) {
	// NOTE: MessageSet allows for larger field numbers than normal.
	if x>>3 > uint64(math.MaxInt32) {
		return -1, 0
	}
	return Number(x >> 3), Type(x & 7)
}

// EncodeTag encodes the field Number and wire Type into its unified form.
func EncodeTag(num Number, typ Type) uint64 {
	return uint64(num)<<3 | uint64(typ&7)
}

Length说明了value的长度,对于proto更新添加新字段,仍然使用旧的proto的解析的时候会根据length跳过不认识的字段,这样可以保证字段兼容。但是不是所有的wire_type都有length这一部分的,例如varint,可以根据MSB来获取数据长度。

ID Name 格式 Userd For
0 varint T-V int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 I64 T-V 固定8字节 fixed64, sfixed64, double
2 LEN T-L-V string, bytes, embedded messages, packed repeated fields
3 SGROUP 已废弃 group start (deprecated)
4 EGROUP 已废弃 group end (deprecated)
5 I32 T-V 固定4字节 fixed32, sfixed32, float

More Integer types

从wire_type的表中可以看出,bool和enum也是采用varint进行编码的,也就是说bool和enum也是作为整形处理的。

还有有符号数,varint编码是无符号的。所以对于负数的编码有所不同。intN类型采用二进制的补码来表示负数,然后使用varint编码,使用全部10个字节,例如-2的编码如下:

11111110 11111111 11111111 11111111 11111111
11111111 11111111 11111111 11111111 00000001

使用无符号定义的话就是~0-2+1,其中~0表示二进制位全1的64位整数。

SintN采用的则是ZigZap的处理值,然后再使用varint编码。统一使用正数来表示并且,正数n被编码为2n(偶数),负数n被编码为|2n+1|(奇数),例如-2采用ZigZap编码的结果就是3。代码中是采用位运算完成的,这点比较巧妙

uint64((uint32(v)<<1)^uint32((int32(v)>>31)))

这里用的是sint32类型,将值左移以为来乘以2,然后与最高位异或,来决定最后一位是否置1。当然只有负数的最高位位1。

对于浮点数以及fixedN这样的类型,采用了非varint的编码,而是I32和I64这样的编码,固定使用4个字节或者8个字节。例如1使用I32编码后为

1 0 0 0

LEN

与前面的不同就是在于编码的结果多了表示长度的L部分,最常见的就是string类型。

// proto
// message Msg {
//    string str = 1;
// }
func main() {
	msg := hello.Msg{Str: "testing"}
	bs, _ := proto.Marshal(&msg)
	fmt.Println(bs)
}

输出的结果为[10 7 116 101 115 116 105 110 103],10为tag(1<< 3 | 2), 7表示value的长度,后续的部分就是string中的字符的字节码了。

总结

其实到了这里,我们已经知道了message中编码方式,就是按照T-L-V的方式编码一个个键值对,最为核心的就是varint的编码,即使Tag的处理方式,也是大部分整型的处理方式。有关wire-type的其它编码也都进行了介绍,可以尝试在自本地编码对应类型的值,查看是否符合预期。

标签:varint,编码,wire,字节,field,原理,message,protobuf
From: https://www.cnblogs.com/smarticen/p/16960364.html

相关文章

  • iOS UI 自动化测试原理以及在 Trip.com 的应用实践
    前言笔者入职​​Trip.com​​已满一年,回顾这一年的工作历程,约一半的时间都在做UI自动化测试相关内容。从而,笔者更深入地研究了iOS平台下的自动化测试技术,目前也在负......
  • DHorse日志收集原理
    实现原理基于k8s的日志收集主要有两种方案,一是使用daemoset,另一种是基于sidecar。两种方式各有优缺点,目前DHorse是基于daemoset实现的。如图1所示:图1在每个k8s集群中启......
  • 《3D计算机视觉:原理、算法及应用》一本全搞定
       1966年,人工智能学家Minsky在给学生布置的作业中,要求学生通过编写一个程序让计算机告诉我们它通过摄像头看到了什么,这也被认为是计算机视觉(ComputerVision,CV)最早的......
  • 能读懂硬件原理图,熟悉常见的硬件接口。
    能读懂硬件原理图,熟悉常见的硬件接口。能读懂硬件原理图,熟悉常见的硬件接口。能读懂硬件原理图,熟悉常见的硬件接口。能读懂硬件原理图,熟悉常见的硬件接口。能读懂硬件原......
  • 说说Nodejs高并发的原理
    导读ALLTHETIME,我们写的的大部分javascript代码都是在浏览器环境下编译运行的,因此可能我们对浏览器的事件循环机制了解比Node.JS的事件循环更深入一些,但是最近写开始深......
  • Vue响应式系统原理并实现一个双向绑定
    这一章就着重讲两个点:响应式系统如何收集依赖响应式系统如何更新视图我们知道通过Object.defineProperty做了数据劫持,当数据改变的时候,get方法收集依赖,进而set方法调用......
  • Vue响应式依赖收集原理分析-vue高级必备
    背景在Vue的初始化阶段,_init方法执行的时候,会执行initState(vm),它的定义在src/core/instance/state.js中。在初始化data和propsoption时我们注意initProps......
  • vue源码分析-diff算法核心原理
    这一节,依然是深入剖析Vue源码系列,上几节内容介绍了VirtualDOM是Vue在渲染机制上做的优化,而渲染的核心在于数据变化时,如何高效的更新节点,这就是diff算法。由于源码中关于d......
  • redux原理是什么
    前言相信很多人都在使用redux作为前端状态管理库进去项目开发,但仍然停留在“知道怎么用,但仍然不知道其核心原理”的阶段,接下来带大家分析一下redux和react-redux两个库的......
  • SpringBoot+Vue+kkFielView实现文件预览时提示:Illegal base64 character 3a以及Vue中
    场景kkFileViewhttps://kkfileview.keking.cn/zh-cn/index.htmlkkFileView为文件文档在线预览解决方案,该项目使用流行的springboot搭建,易上手和部署,基本支持主流办公......