博客:blog.shinelee.me | 博客园 | CSDN

Message Structure

在上一篇文章中我们提到,对于序列化后字节流,需要回答的一个重要问题是“从哪里到哪里是哪个数据成员”。

message中每一个field的格式为:

required/optional/repeated FieldType FieldName = FieldNumber(a unique number in current message)

在序列化时,一个field对应一个key-value对,整个二进制文件就是一连串紧密排列的key-value对,key也称为tag,先上图直观感受一下,图片来自Encoding and Evolution

key由wire type和FieldNumber两部分编码而成, 具体地key = (field_number << 3) | wire_typefield_number 部分指示了当前是哪个数据成员,通过它将cc和h文件中的数据成员与当前的key-value对应起来

key的最低3个bit为wire type,什么是wire type?如下表所示:



wire type被如此设计,主要是为了解决一个问题,如何知道接下来value部分的长度(字节数),如果

  • wire type = 0、1、5,编码为 key + 数据,只有一个数据,可能占数个字节,数据在编码时自带终止标记
  • wire type = 2,编码为 key + length + 数据,length指示了数据长度,可能有多个数据,顺序排在length后

解码代码一窥

接下来,我们直接看一下example.pb.cc及相关的源码,看下key-value对是如何解析的。解码过程相对简单,理解了解码过程,编码也就比较显然了

// example.proto
package example; message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
}
// in example.pb.cc
bool Person::MergePartialFromCodedStream(
::google::protobuf::io::CodedInputStream* input) {
#define DO_(EXPRESSION) if (!PROTOBUF_PREDICT_TRUE(EXPRESSION)) goto failure
::google::protobuf::uint32 tag;
// @@protoc_insertion_point(parse_start:example.Person)
for (;;) {
::std::pair<::google::protobuf::uint32, bool> p = input->ReadTagWithCutoffNoLastTag(127u);
tag = p.first;
if (!p.second) goto handle_unusual;
switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
// required string name = 1;
case 1: {
if (static_cast< ::google::protobuf::uint8>(tag) == (10 & 0xFF)) { // 10 = (1 << 3) + 2
DO_(::google::protobuf::internal::WireFormatLite::ReadString(
input, this->mutable_name()));
::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
this->name().data(), static_cast<int>(this->name().length()),
::google::protobuf::internal::WireFormat::PARSE,
"example.Person.name");
} else {
goto handle_unusual;
}
break;
} // required int32 id = 2;
case 2: {
if (static_cast< ::google::protobuf::uint8>(tag) == (16 & 0xFF)) { // 16 = (2 << 8) + 0
HasBitSetters::set_has_id(this);
DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
input, &id_)));
} else {
goto handle_unusual;
}
break;
} // optional string email = 3;
case 3: {
if (static_cast< ::google::protobuf::uint8>(tag) == (26 & 0xFF)) {
DO_(::google::protobuf::internal::WireFormatLite::ReadString(
input, this->mutable_email()));
::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
this->email().data(), static_cast<int>(this->email().length()),
::google::protobuf::internal::WireFormat::PARSE,
"example.Person.email");
} else {
goto handle_unusual;
}
break;
} default: {
handle_unusual:
if (tag == 0) {
goto success;
}
DO_(::google::protobuf::internal::WireFormat::SkipField(
input, tag, _internal_metadata_.mutable_unknown_fields()));
break;
}
}
}
success:
// @@protoc_insertion_point(parse_success:example.Person)
return true;
failure:
// @@protoc_insertion_point(parse_failure:example.Person)
return false;
#undef DO_
}

整段代码在循环地解析input流,遇到1个tag(key),根据其wire type和数据类型调用相应的解析函数,如果是string,则调用ReadStringReadString会一直调用到ReadBytesToString,如果是int32,则调用ReadPrimitiveReadPrimitive中会进一步调用ReadVarint32。可以看到,生成的example.pb.cc决定了遇到哪个tag调用哪个解析函数,从输入流中解析出值,赋给对应的成员变量,而真正进行解析的代码实际上是Protobuf的源码,如下所示:

// in wire_format_lit.cc
inline static bool ReadBytesToString(io::CodedInputStream* input,
string* value) {
uint32 length;
return input->ReadVarint32(&length) &&
input->InternalReadStringInline(value, length);
} // in wire_format_lit.h
template <>
inline bool WireFormatLite::ReadPrimitive<int32, WireFormatLite::TYPE_INT32>(
io::CodedInputStream* input,
int32* value) {
uint32 temp;
if (!input->ReadVarint32(&temp)) return false;
*value = static_cast<int32>(temp);
return true;
} // in coded_stream.h
inline bool CodedInputStream::ReadVarint32(uint32* value) {
uint32 v = 0;
if (PROTOBUF_PREDICT_TRUE(buffer_ < buffer_end_)) {
v = *buffer_;
if (v < 0x80) {
*value = v;
Advance(1);
return true;
}
}
int64 result = ReadVarint32Fallback(v);
*value = static_cast<uint32>(result);
return result >= 0;
}

可以看到,如果遇到int32tag,直接读取接下来的数据,如果遇到stringtag,会先读一个Varint32的length,然后再读length个字节的数据。

这里频繁出现了varint,length是varint,存储的int32数据也是varint,那varint是什么?

varint

varint是一种可变长编码,使用1个或多个字节对整数进行编码,可编码任意大的整数,小整数占用的字节少,大整数占用的字节多,如果小整数更频繁出现,则通过varint可实现压缩存储。

varint中每个字节的最高位bit称之为most significant bit (MSB),如果该bit为0意味着这个字节为表示当前整数的最后一个字节,如果为1则表示后面还有至少1个字节,可见,varint的终止位置其实是自解释的

在Protobuf中,tag和length都是使用varint编码的lengthtag中的field_number都是正整数int32,这里提一下tag,它的低3位bit为wire type,如果只用1个字节表示的话,最高位bit为0,则留给field_number只有4个bit位,1到15,如果field_number大于等于16,就需要用2个字节,所以对于频繁使用的field其field_number应设置为1到15。

比如正整数150,其使用varint编码如下(小端存储):

// proto file
message Test1 {
optional int32 a = 1;
} // c++ file
// set a = 150 // binary file, in hex
// 08 96 01

其中08为key, 96 01为150的varint编码,解释如下



有关varint的更多内容,可以参见wiki Variable-length quantity

至此,key-value的编码方式我们已经解决了一半,还剩value部分没有解决,接下来看看Protobuf数据部分是如何编码的。

Protobuf中的整数和浮点数

Protobuf中整数也是通过varint进行编码,移除每个字节的MSB,然后拼接在一起,可以得到一个含有数个字节的buffer,这个buffer该怎么解释还需要参考具体的数据类型

对于int32int64,正数直接按varint编码,数据类型为int32int64的负数统一被编码为10个字节长的varint(补码)。

如果是sint32sint64,则采用ZigZag方式进行编码,如下表所示:



sint32 n被编码为 (n << 1) ^ (n >> 31)对应的varint,sint64 n被编码为 (n << 1) ^ (n >> 63)对应的varint,这样,绝对值较小的整数只需要较少的字节就可以表示

至于浮点数,对应的wire type为1或5,直接按小端存储。

Length-delimited相关类型

主要有3类:string、嵌套message以及packed repeated fields。它们的编码方式统一为 tag + length + 数据,只是数据部分有所差异。

string的编码为 key + length + 字符,参看开篇的图片已经很清晰了。

嵌套message也很简单,直接将嵌套message部分的编码接在length后即可,如下所示:

// proto file
message Test1 {
optional int32 a = 1;
}
message Test3 {
optional Test1 c = 3;
} // cpp file
// set a = 150 // message Test3 binary file, in hex
// 1a 03 08 96 01

其中,1ac的key,03c的长度,接下来的08 96 01a的key+value。

packed repeated fields,指的是proto2中声明了[packed=true]的repeated varint、32bit or 64bit数据,proto3中repeated默认packed,如下所示

// in proto2
message Test4 {
repeated int32 d = 4 [packed=true];
} // in proto3
message Test4 {
repeated int32 d = 4;
} // 3, 270, 86942压缩存储如下,in hex
22 // key (field number 4, wire type 2), 0x22 = 34 = (4 << 3) + 2
06 // payload size (6 bytes), length
03 // first element (varint 3)
8E 02 // second element (varint 270)
9E A7 05 // third element (varint 86942)

6个字节根据varint的MSB可自动分割成3个数据。对这种packed repeated fields,在Protobuf中会以RepeatedField对象承载,支持get-by-index、set-by-index和add(添加元素)操作。

小结

至此,二进制文件中key-value对的编码方式已基本介绍完毕,后面将通过一个相对复杂的例子,将这些琐碎的编码方式串起来,以加深理解。

参考

Protocol Buffers(2):编码与解码的更多相关文章

  1. 理解netty对protocol buffers的编码解码

    一,netty+protocol buffers简要说明 Netty是业界最流行的NIO框架之一优点:1)API使用简单,开发门槛低:2)功能强大,预置了多种编解码功能,支持多种主流协议:3)定制能力 ...

  2. protocol buffers的编码原理

    protocol buffers使用二进制传输格式传递消息,因此相比于xml,json来说要轻便很多. 示例:假设定义了一个Message message Test1 { required int32 ...

  3. 【笔记】直接使用protocol buffers的底层库,对特定场景的PB编解码进行处理,编码性能提升2.4倍,解码性能提升4.8倍

    接上一篇文章:[笔记]golang中使用protocol buffers的底层库直接解码二进制数据 最近计划优化prometheus的remote write协议,因为业务需要,实现了一个remote ...

  4. Protocol Buffers编码详解,例子,图解

    Protocol Buffers编码详解,例子,图解 本文不是让你掌握protobuf的使用,而是以超级细致的例子的方式分析protobuf的编码设计.通过此文你可以了解protobuf的数据压缩能力 ...

  5. 【笔记】golang中使用protocol buffers的底层库直接解码二进制数据

    背景 一个简单的代理程序,发现单核QPS达到2万/s左右就上不去了,40%的CPU消耗在pb的decode/encode上面. 于是我想,对于特定的场景,直接从[]byte中取出字段,而不用完全的把整 ...

  6. 消息编解码Nanopb - protocol buffers

    Google Protocol Buffer 有各种版本的代码包,Python C/C++.JAVA.C.OBJ-C..NET等,嵌入式设备中使用的protobuf版本,我们选择的是nanoprobu ...

  7. Protocol Buffers(Protobuf) 官方文档--Protobuf语言指南

    Protocol Buffers(Protobuf) 官方文档--Protobuf语言指南 约定:为方便书写,ProtocolBuffers在下文中将已Protobuf代替. 本指南将向您描述如何使用 ...

  8. Protocol Buffers(Protobuf)开发者指南---概览

    Protocol Buffers(Protobuf)开发者指南---概览 欢迎来到protocol buffers的开发者指南文档,protocol buffers是一个与编程语言无关‘.系统平台无关 ...

  9. Go 支持Protocol Buffers的配置

    安装 protoc (The protocol compiler)是由C++写的,支持的 C++.Java.Python.Objective-C.C#.JavaNano.JavaScript.Ruby ...

随机推荐

  1. spring容器和springmvc容器,以及web容器的关系

    说到spring和springmvc,其实有很多人分不清他们有什么区别,认为它俩是一样的,如果你问他项目里用的什么MVC技术,他会说我们用的spring和mybatis,或者spring和hibern ...

  2. mysql数据库的安装步骤

    Redhat6.5 1.准备工作 卸载使用rpm包安装的mysql-server.mysql软件包 安装自带的ncurses-devel包 rpm -ivh /mnt/Packages/ncurses ...

  3. JAVA小记 (1)

    JVM: Java虚拟机  JVM个数取决于同时执行的程序个数 JDK:JAVA 开发工具包 Java利用JVM实行跨平台 JRE:Java运行环境 JavaSE:企业版 GC:垃圾回收机制 命名规范 ...

  4. iframe跨域动态设置主窗口宽高

    Q:在A项目的a页面嵌入一个iframe,src是B项目的b页面,怎样让a页面的高度跟b页面的高度一样? A:解决跨域方案:增加一个A项目的c页面. 操作步骤: 一,a页面的iframe设置: 获取到 ...

  5. 数组、ArrayList、List、LinkedList的区别

    一.数组 数组在内存中是连续存储的,所以它的索引速度非常快,而且赋值与修改元素也很简单. 1.一维数组 声明一个数组: ]; 初始化一个数组: ] { , , , , }; //定长 声明并初始化: ...

  6. SSM-SpringMVC-13:SpringMVC中XmlViewResolver视图解析器

     ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 还记得上篇博客提出来的问题吗? BeanNameViewResolver视图解析器每使用一道视图,就得手工配 ...

  7. SSM-SpringMVC-01:SpringMVC是什么?SpringMVC执行流程

     ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- SpringMVC简介: SpringMVC也叫Spring Web  mvc,属于表现层的框架.Sprin ...

  8. YII框架视图模块化

    在实际项目开发中,有很多地方我们在不同页面我们会重复利用它,如菜单栏... 下面是将这类型进行模块化处理,使其能在不同页面中重复利用... 案例: 效果图如下: 1.在YII中的路径如下: 2.在le ...

  9. Spring mvc 原理浅析

    2.2. 数据的绑定 前面说过了,SpringMVC是方法级的映射,那么Spring是如何处理方法签名的,又是如何将表单数据绑定到方法参数中的?下面我们就来讨论这个问题.2.2.1. 处理方法签名 首 ...

  10. bootstrap模态框内容替换时会重新触发模态框?<a>标签到底有哪些特殊的特性呢?

    segmentfault提问 这个问题我将bootstrap导航栏的<a>去除就解决了,那么问题来了,<a>标签到底有哪些特殊的特性呢? 主要属性href 链接href 这是一 ...