Protocol Buffers(2):编码与解码

目录

  • Message Structure
  • 解码代码一窥
  • varint
  • Protobuf中的整数和浮点数
  • Length-delimited相关类型
  • 小结
  • 参考

博客: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对的编码方式已基本介绍完毕,后面将通过一个相对复杂的例子,将这些琐碎的编码方式串起来,以加深理解。

参考

原文地址:https://www.cnblogs.com/shine-lee/p/10717521.html

时间: 2024-10-26 15:52:19

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

理解netty对protocol buffers的编码解码

一,netty+protocol buffers简要说明 Netty是业界最流行的NIO框架之一优点:1)API使用简单,开发门槛低:2)功能强大,预置了多种编解码功能,支持多种主流协议:3)定制能力强,可以通过ChannelHandler对通信框架进行灵活的扩展:4)性能高,通过与其它业界主流的NIO框架对比,Netty的综合性能最优:5)成熟.稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼:6)社区活跃,版本迭代周期短,发现的BUG可以

protocol buffers的编码原理

protocol buffers使用二进制传输格式传递消息,因此相比于xml,json来说要轻便很多. 示例:假设定义了一个Message message Test1 { required int32 a = 1; } 实际使用的时候将a设置为150,然后将其序列化到输出流,查看编码后的message,可以看到如下3个byte 08 96 01 解析: 上述三个字节实际分为两部分: 08  96 01.第一部分(08)包含了message成员变量的field number(a=1)和变量类型(V

Google Protocol Buffers 编码(Encoding)

Google Protocol Buffers 编码(Encoding) 1. 概述 前三篇文章<Google Protocol Buffers 概述><Google Protocol Buffers 入门><Protocol Buffers 语法指南> 一步一步将大家带入Protocol Buffers的世界,我们已经基本能够使用Protocol Buffers生成代码,编码,解析,输出级读入序列化数据.该篇主要讲述PB message的底层二进制格式.不了解该部分内

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

本文不是让你掌握protobuf的使用,而是以超级细致的例子的方式分析protobuf的编码设计.通过此文你可以了解protobuf的数据压缩能力来自什么地方,版本兼容如何做到的,其Key-Value编码的设计思路.如果你详细了解此文,你应该就能具备自己造一套编解码轮子的能力(至少基本思路). 测试的例子 阅读图片时请对比前面的例子和表格.每个字段的名称都是包含了tag的. message S2 { optional int32 s2_1 = 1; optional string s2_2 =

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

Protocol Buffers(Protobuf)开发者指南---概览 欢迎来到protocol buffers的开发者指南文档,protocol buffers是一个与编程语言无关‘.系统平台无关.可扩展的结构化数据序列化/反序列化工具,适用于通讯协议,数据存储等场合. ps:为了方便拼写,下文的protobuf就是指protocol buffers. 本文档的面向读者是:希望使用protobuf的 Java.C++.Python的开发者.此概览将向您介绍如何开始使用protobuf,然后您

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

Protocol Buffers(Protobuf) 官方文档--Protobuf语言指南 约定:为方便书写,ProtocolBuffers在下文中将已Protobuf代替. 本指南将向您描述如何使用protobuf定义i结构化Protobuf数据,包括.proto文件语法和如何使用.proto文件生成数据存取类. 作为一个参考指南,本文档将以示例的形式一步步向您介绍Protobuf的特点.您可以参考您所选择的语言的示例.tutorial ----------------------------

使用 Protocol Buffers 代替 JSON 的五个原因

在Ruby和Rails开发者中,面向服务(Service-Oriented)架构有一个当之无愧的名声,它是一个缓解程序规模恶性增长的一个强有力的途径,可在大量应用程序中提取关注点.这些新生小巧的服务通常继续使用Rails或Sinatra,并使用JSON在HTTP上通信.尽管JSON作为一个数据相互交换格式,有很多优点:人类可读.可理解,并通常表现出色. 浏览器和JS并不直接处理数据--尤其是遇到内部服务时.我的观点是,结构化格式,例如谷歌的Protocol Buffers,是一个比JSON在编码

(转)Protocol Buffers for C

我一直不太满意 google protocol buffers 的默认设计.为每个 message type 生成一大坨 C++ 代码让我很难受.而且官方没有提供 C 版本,第三方的 C 版本 也不让我满意. 这种设计很难让人做动态语言的 binding ,而大多数动态语言往往又没有强类型检查,采用生成代码的方式并没有特别的好处,反而有很大的性能损失(和通常做一个 bingding 库的方式比较).比如官方的 Python 库,完全可以在运行时,根据协议,把那些函数生成出来,而不必用离线的工具生

Google Protocol Buffers 入门

1. 前言 这篇入门教程是基于Java语言的,这篇文章我们将会: 创建一个.proto文件,在其内定义一些PB message 使用PB编译器 使用PB Java API 读写数据 这篇文章仅是入门手册,如果想深入学习及了解,可以参看: Protocol Buffer Language Guide, Java API Reference, Java Generated Code Guide, 以及Encoding Reference. 2. 为什么使用Protocol Buffers 接下来用“