Google的Protobuf协议分析

摘要:
Protobuf和Thrift一样,也是一种串行化协议实现,简称PB。数字是定义的原型文件中每个字段的编号。类型是定义的原型文件中每个字段的类型。三位用于表示类型,可以表示0到7。总共有八种类型。PB只使用0、1、2、3、4和5。要详细了解variant编码,请参阅我的另一篇文章中对ThriftTCompactProtocol协议分析中variant的介绍。阅读了链接中描述的变码和锯齿码后,继续分析。

protobuf和thrift类似,也是一个序列化的协议实现,简称PB(下文出现的PB代表protobuf)。

Github:https://github.com/google/protobuf

上图,说明一下protobuf协议。

Google的Protobuf协议分析第1张

PB以“1-5个字节”的编号和类型开头,格式:编号左移3位和类型取或得到。

编号是什么?

编号就是 定义的proto文件中各个字段的编号。

如:

Google的Protobuf协议分析第2张

类型是什么?

类型就是 定义的proto文件中各个字段类型,使用3位表示类型,可以表示0到7,共8种类型,PB类型只用了0,1,2,3,4,5这6种类型。

详细描述参考如下表格:

类型描述使用于哪些类型
0varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages, packed repeated fields
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, sfixed32, float

看到图和表格时是不是有很多迷惑的地方?

1. 为什么编号类型,32位数值,32位负载长度数值都占用 “1-5个字节”? 

2. 为什么64为的数值占用“1-10个字节”?

3. Varint是什么?

4. ZigZag是什么? 

解决这些问题的关键:PB对数值进行压缩,压缩算法就是Varint,负数进行zigzag编码后再做varint编码,什么是Varint数值压缩?

  为了详细的了解varint的编码,可以参考我的另一篇文章 Thrift TCompactProtocol协议分析的varint介绍部分。

看完链接中描述的varint编码和zigzag编码后,继续分析。

编写一个demo分析一下PB协议。

1. 编写proto接口文件

package demo;

enum AuctionType {
  FIRST_PRICE = 1;
  SECOND_PRICE = 2;
  FIXED_PRICE = 3;
}

message VarintMsg {
  required int32  argI32  = 1;
  required int64  argI64  = 2;
  required uint32 argUI32 = 3;
  required uint64 argUI64 = 4;
  required sint32 argSI32 = 5;
  required sint64 argSI64 = 6;
  repeated bool   argBool = 7;
  optional AuctionType argEnum = 8;
}

message Bit64 {
  required fixed64  argFixed64  = 1;
  required sfixed64 argSFixed64 = 2;
  required double   argDouble   = 3;
}

message Bit32 {
  required fixed32  argFixed32  = 1;
  required sfixed32 argSFixed32 = 2;
  required float    argFloat    = 3;
}

message LenPayload {
  repeated string argStrList = 1;
  optional VarintMsg argVarintMsg = 2;
  optional Bit64 argBit64 = 3;
  optional Bit32 argBit32 = 4;
}

2. 编写测试代码

/*
** Copyright (C) 2014 Wang Yaofu
** All rights reserved.
**
**Description: The source file of demo.
*/

#include "demo.pb.h"
#include <string>
#include <iostream>
#include <fstream>
using namespace std;

int appendFile(const string& file, const char* dataPtr, int len) {
    std::ofstream ofs(file, std::ofstream::app | std::ofstream::binary);
    if (ofs.is_open() && ofs.good()) {
        ofs.write(dataPtr, len);
    }
    return len;
}

int main(int argc, char *argv[]) {
    demo::VarintMsg* varintMsg = new demo::VarintMsg();
    varintMsg->set_argi32(0x41);
    varintMsg->set_argi64(0x12345678);
    varintMsg->set_argui32(0x332211);
    varintMsg->set_argui64(0x998877);
    varintMsg->set_argsi32(-100);
    varintMsg->set_argsi64(-200);
    varintMsg->add_argbool(true);
    varintMsg->add_argbool(false);
    varintMsg->set_argenum(demo::SECOND_PRICE);

    demo::Bit64* bit64 = new demo::Bit64();
    bit64->set_argfixed64(0x123456);
    bit64->set_argsfixed64(-100);
    bit64->set_argdouble(3.1415926);

    demo::Bit32* bit32 = new demo::Bit32();
    bit32->set_argfixed32(0x1234);
    bit32->set_argsfixed32(-10);
    bit32->set_argfloat(3.1415);

    demo::LenPayload* lenPayload = new demo::LenPayload();
    lenPayload->add_argstrlist("String 1.");
    lenPayload->add_argstrlist("String 2.");
    lenPayload->set_allocated_argvarintmsg(varintMsg);
    lenPayload->set_allocated_argbit64(bit64);
    lenPayload->set_allocated_argbit32(bit32);
    std::string content;
    lenPayload->SerializeToString(&content);
    appendFile("pb.bin", content.data(), content.length());
    delete lenPayload;
    return 0;
}

3. 编写Makefile

CXX      = g++ -g -std=c++11
PB_HOME  = ./tools/protobuf-2.6.1/inbin/
PROTOC   = LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:$(PB_HOME)/lib $(PB_HOME)/bin/protoc
CXXFLAGS = -I$(PB_HOME)/include -I.
LDFLAGS  = -L$(PB_HOME)/lib -lprotobuf

all: demo.pb.h demo
demo.pb.h :
	$(PB_HOME)/bin/protoc --cpp_out=. ./demo.proto
demo :
	${CXX} ${CXXFLAGS} -o demo demo.cpp ${LDFLAGS}

clean:
	rm -rf demo *.pb.*

4. 编译后运行demo,得到二进制文件pb.bin

Google的Protobuf协议分析第3张

 5. 按字节分析

 5.1.消息message LenPayload的第一个字段分析:

  repeated string argStrList = 1;

  字节 0a 表示编号和类型:

  编号为1,类型为2,1 << 3 | 2 = 1000 | 0010 = 1010 = 8+2 = 10 = 0a

  字节 09 表示负载信息的长度为9:

  字节:"53 74 72 69 6e 67 20 31 2e"  为 "String 1. ",长度正好为9.

  字段argStrList是可重复的,所以紧接着的字节 0a 09表示编号类型和长度。

  字节:"53 74 72 69 6e 67 20 32 2e"  为 "String 2. "。

  对应代码:

    lenPayload->add_argstrlist("String 1.");
    lenPayload->add_argstrlist("String 2.");

 5.2. 消息message LenPayload的第二个字段分析:

      optional VarintMsg argVarintMsg = 2;

      字节:"12 1e 08 41 10 f8 ac d1 91 01 18 91 c4 cc 01 20 f7 90 e6 04 28 c7 01 30 8f 03 38 01 38 00 40 02"

  字节 12 表示编号和类型:

  编号为2,类型为2,2 << 3 | 2 = 10000 | 0010 = 10010 = 16+2 = 18 = 0x12

  字节 1e 表示负载信息的长度为30.

      5.2.1. message VarintMsg消息分析

      required int32  argI32  = 1;

      varintMsg->set_argi32(0x41); 

      08 41

      字节08 表示编号和类型:

  编号为1,类型为0,1 << 3 | 0 = 1000 | 0000 = 1000 = 8 = 0x08

      字节 41 表示值为 0x41.

      required int64  argI64  = 2;

      varintMsg->set_argi64(0x12345678);

      10 f8 ac d1 91 01      

      字节10 表示编号和类型:

  编号为2,类型为0,2 << 3 | 0 = 10000 | 0000 = 10000 = 16 = 0x10

      字节 f8 ac d1 91 01二进制表示值为

      1111 1000, 1010 1100, 1101 0001, 1001 0001, 0000 0001

      小端转本地为 0000 0001, 1001 0001, 1101 0001, 1010 1100, 1111 1000

      去掉红色的1,varint恢复为 0001 0010, 0011 0100, 0101 0110, 0111 1000 表示为16进制就是 0x12345678

      required uint32 argUI32 = 3;

      varintMsg->set_argui32(0x332211);

      18 91 c4 cc 01

      字节18 表示编号和类型:

  编号为3,类型为0,3 << 3 | 0 = 11000 | 0000 = 11000 = 16 + 8 = 24 = 0x18

      字节91 c4 cc 01二进制为 1001 0001, 1100 0100, 1100 1100, 0000 0001

      小端转本地为 0000 0001, 1100 1100, 1100 0100 , 1001 0001

      去掉红色的1,varint恢复为 0011 0011, 0010 0010, 0001 0001 表示为16进制就是 0x332211

      required uint64 argUI64 = 4;

      varintMsg->set_argui64(0x998877);

      20 f7 90 e6 04

      字节20 表示编号和类型:

  编号为4,类型为0,4 << 3 | 0 = 100000 | 0000 = 100000 = 32 = 0x20

      字节 f7 90 e6 04二进制为 1111 0111, 1001 0000, 1110 0110, 0000 0100

      小端转本地为 0000 0100, 1110 0110, 1001 0000, 1111  0111

      去掉红色的1,varint恢复为 1001 1001,1000 1000, 0111 0111 表示为16进制就是 0x998877

      required sint32 argSI32 = 5;

      varintMsg->set_argsi32(-100);

      28 c7 01 

      字节28 表示编号和类型:

  编号为5,类型为0,5 << 3 | 0 = 101000 | 0000 = 101000 = 32 + 8 = 40 = 0x28

      字节 c7 01二进制表示为 1100 0111, 0000 0001
      小端转为本地为 0000 0001, 1100 0111

      去掉红色的1,varint恢复为 1100 0111 = 199 = -100 * -2 - 1,正好是-100做zigzag后varint压缩得到的值。

      required sint64 argSI64 = 6;

      varintMsg->set_argsi64(-200);

      30 8f 03

      字节30 表示编号和类型:

  编号为6,类型为0,6 << 3 | 0 = 110000 | 0000 = 110000 = 32 + 16 = 48 = 0x30

      字节 8f 03二进制表示为1000 1111, 0000 0011

      小端转本地为 0000 0011, 1000 1111

      去掉红色的1,varint恢复为11000 1111 = 399 = -200 * -2 -1,正好是-200做zigzag后varint压缩得到的值。

      repeated bool   argBool = 7;

      varintMsg->add_argbool(true);

      38 01

      字节38 表示编号和类型:

  编号为7,类型为0,7 << 3 | 0 = 111000 | 0000 = 111000 = 32 + 16 + 8 = 56 = 0x38

      字节 01 表示值为1, 是true.

      repeated bool   argBool = 7;

      varintMsg->add_argbool(false);

      38 00

      字节38 表示编号和类型:

  编号为7,类型为0,7 << 3 | 0 = 111000 | 0000 = 111000 = 32 + 16 + 8 = 56 = 0x38

      字节 00 表示值为 0,是false.

      optional AuctionType argEnum = 8;

      varintMsg->set_argenum(demo::SECOND_PRICE);

      40 02

      字节40表示编号和类型:

  编号为3,类型为0,8 << 3 | 0 = 1000000 | 0000 = 1000000 = 64 = 0x40

      字节 02 表示值为 2,是枚举的值demo::SECOND_PRICE值为2.

 5.3. 消息message LenPayload的第三个字段分析:

     optional Bit64 argBit64 = 3;

     “1a 1b 09 56 34 12 00 00 00 00 00 11 9c ff ff ff ff ff ff ff 19 4a d8 12 4d fb 21 09 40”

  字节 1a 表示编号和类型:

  编号为3,类型为2,0x1a = 3 << 3 | 2 = 11000 | 0010 = 11010 = 16+8+2 = 26 = 0x1a

  字节 1b 表示负载信息的长度为27。

      5.3.1. message Bit64消息分析

      required fixed64  argFixed64  = 1;

      bit64->set_argfixed64(0x123456);

      09 56 34 12 00 00 00 00 00

      字节 09 表示编号和类型:

  编号为1,类型为1,0x0d = 1 << 3 | 1 = 1000 | 0001 = 1001 = 8+1 = 9 = 0x09

      接着8个字节表示64位数的负载信息。

  "56 34 12 00 00 00 00 00":从小端表示转成本地表示为 00 00 00 00 00 12 34 56,  表示 0x123456 

      required sfixed64 argSFixed64 = 2;

      bit64->set_argsfixed64(-100);

      11 9c ff ff ff ff ff ff ff

      字节 09 表示编号和类型:

  编号为2,类型为1,0x11 = 2 << 3 | 1 = 10000 | 0001 = 10001 = 16+1 = 17 = 0x11

      接着8个字节表示64位数的负载信息。

  "9c ff ff ff ff ff ff ff":从小端表示转成本地表示为 ff ff ff ff ff ff ff 9c,  表示 -100

      required double argDouble = 3;

      bit64->set_argdouble(3.1415926);

      19 4a d8 12 4d fb 21 09 40

      字节 19 表示编号和类型:

  编号为3,类型为1,0x19 = 1 << 3 | 1 = 11000 | 0001 = 11001 = 16+8+1 = 25 = 0x19

      接着8个字节表示64位数的负载信息。

  "4a d8 12 4d fb 21 09 40":表示: 3.1415926

 5.4. 消息message LenPayload的第四个字段分析:

      optional Bit32 argBit32 = 4;

  “22 0f 0d 34 12 00 00 15 f6 ff ff ff 1d 56 0e 49 40”

  字节 22 表示编号和类型:

  编号为4,类型为2,0x22 = 4 << 3 | 2 = 100000 | 0010 = 100010 = 32+2 = 34 = 0x22

  字节 0f 表示负载信息的长度为15:

     5.4.1. message Bit32消息分析

  required fixed32 argFixed32 = 1;

      bit32->set_argfixed32(0x1234);

      0d 34 12 00 00

  字节 0d 表示编号和类型:

  编号为1,类型为5,0x0d = 1 << 3 | 5 = 1000 | 0101 = 1101 = 8+4+1 = 13 = 0x0d

      接着4个字节表示32位数的负载信息。

  "34 12 00 00":从小端表示转成本地表示为 00 00 12 34,  表示 0x1234 

  required sfixed32 argSFixed32 = 2;

      bit32->set_argsfixed32(-10);

  15 f6 ff ff ff 1d 

      字节 15 表示编号和类型:

  编号为2,类型为5,0x15 = 1 << 3 | 5 = 10000 | 0101 = 10101 = 16+4+1 = 21 = 0x15
     接着4个字节表示32位数的负载信息。

  "ff ff ff 1d": 表示 -10

  required float argFloat = 3;

      bit32->set_argfloat(3.1415);

  1d 56 0e 49 40

      字节 1d 表示编号和类型:

  编号为3,类型为5,0x1d = 3 << 3 | 5 = 11000 | 0101 = 11101 = 16+8+4+1 = 29 = 0x1d

      接着4个字节表示32位数的负载信息。

  "56 0e 49 40": 表示 3.1415

测试代码:https://github.com/gityf/utils/tree/master/pb_analysis_demo

Done.

免责声明:文章转载自《Google的Protobuf协议分析》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Ansible之roles介绍CentOS7使用RPM安装Package遇到 error: Failed dependencies,解决方案。下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

windows下使用python googleprotobuf

  首先下载:protobuf-2.5.0.tar.gz 和protoc-2.5.0-win32.zip。两者的版本要对应; 将下载的google protobuf解压,会看到一个python目录,Windows下可将vsprojectsDebug下的protoc.exe拷贝到python目录下。 在cmd下,切换到该目录,执行python setup.p...

mac关于stf开源工具安装

准备: 1.安装brew工具,后续安装stf依赖会用到 ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 2.安装yasm brew install yasm #安装jpegturbo会用到(不安装这个,jpegturbo安装会...

Protobuf动态解析那些事儿

需求背景 在接收到 protobuf 数据之后,如何自动创建具体的 Protobuf Message 对象,再做反序列化。“自动”的意思主要有两个方面:(1)当程序中新增一个 protobuf Message 类型时,这部分代码不需要修改,不需要自己去注册消息类型,不需要重启进程,只需要提供protobuf文件;(2)当protobuf Message修...

protobuf编译

win10   cmake   vs2017编译 protobuf编译 cmake Selecting Windows SDK version 10.0.17134.0 to target Windows 10.0.19042. The C compiler identification is MSVC 19.15.26732.1 The CXX com...

Protobuf 语言指南(proto3)

Protobuf 语言指南(proto3)Protocol Buffer是Google的语言中立的,平台中立的,可扩展机制的,用于序列化结构化数据 - 对比XML,但更小,更快,更简单。您可以定义数据的结构化,然后可以使用特殊生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。 定义消息类型 先来看一个非常简单的例子。假设你想定义一个“搜索...

进行API开发选gRPC还是HTTP APIs?

上一篇文章我带着大家体验了一把《ASP.NET Core 3.0 上的gRPC服务模板初体验(多图)》,如果有兴趣的可以点击链接进行查看,相信跟着做的你,也是可以跑起来的。这篇文章我们将一起来探讨下gRPC服务如何与HTTP APIs进行比较。用于为应用程序提供API的技术是一个重要的选择,与HTTP API相比,gRPC提供了独特的优势。本文从gRPC的...