简介

Protocol Buffers v3 是 google 推广的一种数据处理方式,其中 v3 是版本。
ProtoBuf 和 json 功能类似,定义了数据的结构。但是因为ProtoBuf是用字节码来存储数据,因此会比json性能、内存占用表现更好,以及因为 ProtoBuf数据不是完全自描述的,所以ProtoBuf的数据要搭配对应的.proto结构定义文件才能完成解析。

Quote

这里主要基于官方ProtoBuf的描述进行梳理,并加上自己的理解。

数据结构

消息的基本结构如下:

syntax = "proto3";
 
message [消息名称] {
    [Proto Type: 数据类型] [数据名称] = [field_number: 数据编号];
}

数据类型

Proto TypeC++ TypeDefaultNotesEncoding
int32int32_t0使用可变长度编码。对负数编码效率低——如果你的字段可能包含负值,请使用 sint32varints
int64int64_t0使用可变长度编码。对负数编码效率低——如果你的字段可能包含负值,请使用 sint64varints
sint32int32_t0使用可变长度编码。有符号整数值。这比普通 int32 更高效地编码负数。zigzag+varints
sint64int64_t0使用可变长度编码。有符号整数值。这比普通 int64 更高效地编码负数。zigzag+varints
uint32uint32_t0使用可变长度编码。varints
uint64uint64_t0使用可变长度编码。varints
fixed32uint32_t0始终占用四个字节。如果值通常大于 228,则比 uint32 更高效。
fixed64uint64_t0始终占用八个字节。如果值通常大于 256,则比 uint64 更高效。
sfixed32int32_t0始终占用四个字节。
sfixed64int64_t0始终占用八个字节。
floatfloat0
doubledouble0
boolboolfalsevarints
enumenum首个定义枚举,必须为0枚举必须含有0,且0在首位。建议第一个默认值除了“此值未指定”之外没有语义含义。varints
stringstd::string空字符串字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,且长度不能超过 2³²。
bytesstd::string空字节可以包含任意字节序列,但长度不能超过 2³²。
messagestruct语言相关可嵌套消息类型
map<k, v>std::map<k, v>k只能是整数或字符串,不可为枚举,message,浮点数等;v可以是任意类型。功能描述
oneofunion共用体类型,支持除了 map 字段和 repeated 字段的其他类型,需要关注注意事项
Anystd::any支持任意类型,要求开启反射,需要导入any.proto后才能使用

数据编号

数据编号注意点较多,这里直接引用指南的描述:

Quote

你必须为消息定义中的每个字段指定一个介于 1 和 536,870,911 之间的数字,并具有以下限制

  • 给定的数字对于该消息的所有字段必须是唯一的
  • 字段编号 19,000 到 19,999 已为 Protocol Buffers 实现保留。如果你在消息中使用这些保留的字段编号之一,protocol buffer 编译器将报错。
  • 你不能使用任何先前保留的字段编号或已分配给扩展的任何字段编号。

一旦你的消息类型投入使用,此编号就不能更改,因为它标识了消息 wire 格式中的字段。“更改”字段编号相当于删除该字段并创建一个具有相同类型但编号不同的新字段。有关如何正确执行此操作,请参阅删除字段

字段编号永远不应该被重用。永远不要将字段编号从保留列表中取出以用于新的字段定义。请参阅重用字段编号的后果

你应该为最常设置的字段使用字段编号 1 到 15。较低的字段编号值在 wire 格式中占用的空间更少。例如,范围在 1 到 15 之间的字段编号占用一个字节进行编码。范围在 16 到 2047 之间的字段编号占用两个字节。你可以在Protocol Buffer 编码中找到更多相关信息。

重用字段编号的后果

重用字段编号会使解码 wire 格式消息变得模棱两可。

protobuf wire 格式是精简的,并且没有提供一种方法来检测使用一种定义编码并使用另一种定义解码的字段。

Danger

使用一个定义编码字段,然后使用不同的定义解码同一字段可能会导致

  • 开发者时间浪费在调试上
  • 解析/合并错误(最佳情况)
  • PII/SPII 泄露
  • 数据损坏

重用字段编号的常见原因

  • 重新编号字段(有时是为了实现更美观的字段编号顺序而完成的)。重新编号实际上会删除并重新添加所有涉及重新编号的字段,从而导致不兼容的 wire 格式更改。
  • 删除字段并且不保留该编号以防止将来重用。

字段编号限制为 29 位而不是 32 位,因为三位用于指定字段的 wire 格式。有关更多信息,请参阅编码主题

数据编码

Quote

ProtoBuf使用 Tag-(Length)-Value 或称 TLV 的方式进行数据流编码。
如果是不需要记录长度的类型,则 (Length) 可以省去,使用 Tag-Value方式进行数据流编码。

编码类型

wire_typeNameUsed For
0VARINTint32, int64, uint32, uint64, sint32, sint64, bool, enum
164bitfixed64, sfixed64, double
2LENstring, bytes, embedded messages, packed repeated fields
3SGROUPgroup start (deprecated)
4EGROUPgroup end (deprecated)
532bitfixed32, sfixed32, float

wire_type是字段类型,wire_type和字段编号组合在一起存储,存储的结构名称为varint结构,当field_number < 16的时候,varint结构由以下方式构成: (field_number << 3) | wire_type

field_number >= 16 的时候,field_number 使用类似 varints编码方式,第一个字节的低3位给wire_type使用,剩余位置为 MSB + 数据位,使用小端方式存储。

Example

比如 int32 a = 1;field_number = 1 < 16, wire_type = 0, 因此 varint(a) = 08

比如 int32 b = 64;field_number = 64 > 16wire_type = 0
64 的 二进制为 0100 0000,其中低4位先存储,剩余位数后存储,
也就是 varint(b) = 1(MSB) 0000(field_number数据低位) 000(wire_type) 0(MSB) 0000100(field_number数据高位) = 10000000 00000100

Varints编码

默认 int32_t 类型无论数据大小,都会占用4字节;为了压缩数据,varints编码每字节仅用7bit存储数据,剩余一个bit称为MSB,MSB是0表示无剩余数据,1表示还有剩余数据。

Example

存储1的时候, int32_t 表示为 00000000 00000000 00000000 00000001,占用4字节;而 varints编码后表示为 00000001,其中最高位的MSB=0表示已无剩余数据,而低7位0000001表示数据1,共占用1字节。

存储255的时候,varints编码表示为 11111111 00000001,其中低7位为1111111,伴随MSB=1,高位为1,伴随MSB=0表示结束,占用2字节。

由于varints编码每字节仅使用7bit存储数据,因此对于较大的数会占用更多的空间,如int32存储 32 >= bit > 28的数据时,需要5个字节,这时使用fixed32来存储会更合适,fixed32每个字节使用8bit存储数据,fixed32固定占用4字节。

ZigZag编码

Info

在varints编码存储负数的时候,因为负数有符号位,也就是最高有效位是1,会导致占用的字节反而更多,因此引入了zigzag编码编码。

zigzag编码通过将负数符号位移动到最低位置来实现压缩的目的,使用zigzag编码后,可以继续使用varints编码来压缩数据。
zigzag编码方式为(n << 1) ^ (n >> 31), 对于-5而言,转换流程如下:

// -5 补码 = 5源码按位取反后 +1得到。
n = -5 = \
11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111011
 
// 数据位,左移低位补零,高位舍弃。
n << 1 = \
11111111 11111111 11111111 11111111 11111111 11111111 11111111 11110110
 
// 符号位,负数右移高位补1,正数右移高位补0。
n >> 31 = \
11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
 
// 数据位 ^ 符号位。
(n << 1) ^ (n >> 31) = \
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001001

经过zigzag编码后,继续使用varints编码,则 -5 表示为 00001001
还原的时候,进行逆操作,也就是(n >> 1) ^ -(n & 1) 即可。

数据流

非LEN型数据流编码

对于非LEN型数据流编码,也就是Tag != 2的数据流编码,除了弃用类型(3,4)外,使用的格式是Tag-Value,不需要同时传输长度。

wire_type = 0int32 类型的数据流编码计算如下:

message Test1 {
  optional int32 a = 1;
}

如果设置Test1.a = 150然后把消息序列化,会得到如下十六进制内容:

08 96 01

Tag:其中 08 的二进制为 00001 000, 前面第一个字节是 (field_number << 3) | wire_type,也就是 field_number = 1wire_type = 0,表示为0x08
Value:因为a是int32类型,使用了 varints编码, 150 的二进制是 10010110, 因为 varints编码 使用7个bit和MSB存储数据,150 的二进制为 10010110,通过varints编码后为 1(MSB) 0010110(数据低位) 和 0(MSB)0000001(数据高位),其中数据低位在前方,也就是 varints(150) = 10010110 00000001 = 0x9601

二者组合在一起就是 Tag-Value08 96 01

LEN类型数据流编码

对于LEN类型数据流编码,也就是Tag = 2的数据流编码,格式是Tag-Length-Value,其中 Length表示数据的长度。

message Test2 {
  string s1 = 1;
  string s2 = 2;
}

当赋值 Test2.s1 = "1";Test2.s2 = "1234";的时候内容如下:

VariableProto TypeRaw Valuefield_numberwire_type
Test2.s1string"1"12
Test2.s2string"1234"22

也就是说:

VariableTagLengthValueTag-Length-Value
Test2.s10a = 00001 0101310a 01 31
Test2.s212 = 00010 010431 32 33 3412 04 31 32 33 34

对于使用了repeated关键字修饰的类型,可以参考序列化分析,里面的 record 有用到 repeated 修饰。

关键字

ProtoC++NotesExample
repeated[]搭配数据类型使用的数组标记,0个或任意长度数组repeated bool light_enable = 1;
optionalstd::optional显式指定为可选字段(不指定也是可选字段),指定后追加has_xxx函数可以判断用户是否真的赋值,可和默认值区分开。optional bool light_enable = 1;
packageusing namespace定义命名空间,如package pt; message Test 情况下,其他.proto通过pt.Test方式引用类型。package pt;
option开启某些功能选项。option allow_alias = true;
reserved保留字段reserved 2, 9 to 11;, reserved "foo", "bar";

option选项

Quote

proto2 支持 optional PhoneType type = 2 [default = PHONE_TYPE_HOME];方式,指定[default = ]设置默认值,但是proto3是不支持的。

allow_alias枚举别名

枚举定义在一个消息内部或消息外部都是可以的,如果枚举是 定义在 message 内部,而其他 message 又想使用,那么可以通过 MessageType.EnumType 的方式引用。
定义枚举的时候,我们要保证第一个枚举值必须是0,枚举值不能重复,除非使用 option allow_alias = true 选项来开启别名。

enum EnumType {
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
}

optimize_for优化级别

option optimize_for = LITE_RUNTIME;

optimize_for 是⽂件级别的选项,Protocol Buffer 定义三种优化级别 SPEED/CODE_SIZE/LITE_RUNTIME。缺省情况下是SPEED

Name
SPEED缺省情况下是SPEED,表示⽣成的代码运⾏效率⾼,但是由此⽣成的代码编译后会占⽤更多的空间。
CODE_SIZE和SPEED恰恰相反,代码运⾏效率较低,但是由此⽣成的代码编译后会占⽤更少的空间,通常⽤于资源有限的平台。
LITE_RUNTIME⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常少。这是以牺牲Protocol Buffer提供的反射功能为代价的。
因此我们在C++中链接Protocol Buffer库时仅需链接 libprotobuf-lite,⽽⾮libprotobuf。

reserved保留字段

如果通过完全删除某个字段或对其进行注释来更新消息类型,将来的用户可以在对该类型进行自己的更新时重用该字段编号。
如果他们以后加载旧版本的相同.proto文件,这可能会导致严重的问题 ,包括数据损坏、隐私漏洞等。
可以把它的变量名或 字段编号 用 reserved 标注,这样,当这个 field_number 或者变量名字被重新使用的时候,编译器会报错。

message Foo {
    // 注意,同一个 reserved 语句不能同时包含变量名和 field_number 
    reserved 2, 15, 9 to 11;
    reserved "foo", "bar";
}

未知字段

引用未知字段的描述:

Quote

未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧的二进制文件解析由具有新字段的新二进制文件发送的数据时,这些新字段将成为旧的二进制文件中的未知字段。

Proto3 消息保留未知字段,并在解析和序列化输出期间包含它们,这与 proto2 行为相匹配。

保留未知字段

某些操作可能会导致未知字段丢失。例如,如果您执行以下操作之一,则会丢失未知字段

  • 将 proto 序列化为 JSON。
  • 迭代消息中的所有字段以填充新消息。

要避免丢失未知字段,请执行以下操作

  • 使用二进制;避免使用文本格式进行数据交换。
  • 使用面向消息的 API,例如 CopyFrom() 和 MergeFrom(),以复制数据,而不是按字段复制

TextFormat 有点特殊情况。序列化为 TextFormat 会使用其字段编号打印未知字段。但是,如果存在使用字段编号的条目,则将 TextFormat 数据解析回二进制 proto 会失败。

更新消息类型

更新操作比较复杂,这里直接引用协议描述。

Quote

如果现有的消息类型不再满足您的所有需求 – 例如,您希望消息格式具有额外的字段 – 但您仍然希望使用使用旧格式创建的代码,请不要担心!当您使用二进制线路格式时,更新消息类型而不会破坏任何现有代码非常简单。

Attention

如果您使用 JSON 或 proto 文本格式来存储协议缓冲区消息,那么您可以在 proto 定义中所做的更改是不同的。

查看 Proto 最佳实践和以下规则

  • 不要更改任何现有字段的字段编号。“更改”字段编号相当于删除该字段并添加具有相同类型的新字段。如果要重新编号字段,请参阅有关删除字段的说明。
  • 如果您添加新字段,则使用“旧”消息格式的代码序列化的任何消息仍然可以由新的生成代码解析。您应该记住这些元素的默认值,以便新代码可以与旧代码生成的消息正确交互。同样,您的新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时只会忽略新字段。有关详细信息,请参阅未知字段部分。
  • 可以删除字段,只要在更新的消息类型中不再使用该字段编号即可。您可能需要重命名字段,例如添加前缀“OBSOLETE_”,或者使字段编号保留,以便将来使用您的 .proto 的用户不会意外地重用该编号。
  • int32uint32int64uint64 和 bool 都是兼容的 – 这意味着您可以将字段从这些类型之一更改为另一个类型,而不会破坏向前或向后兼容性。如果从线路解析的数字不适合相应的类型,您将获得与在 C++ 中将数字强制转换为该类型相同的效果(例如,如果将 64 位数字读取为 int32,它将被截断为 32 位)。
  • sint32 和 sint64 彼此兼容,但与_其他_整数类型_不_兼容。
  • string 和 bytes 只要字节是有效的 UTF-8 就兼容。
  • 如果嵌入的消息的字节包含消息的编码实例,则嵌入的消息与 bytes 兼容。
  • fixed32 与 sfixed32 兼容,fixed64 与 sfixed64 兼容。
  • 对于 stringbytes 和消息字段,singular 与 repeated 兼容。给定重复字段的序列化数据作为输入,期望此字段为 singular 的客户端将采用最后一个输入值(如果它是原始类型字段)或合并所有输入元素(如果它是消息类型字段)。请注意,这对于数字类型(包括布尔值和枚举)通常是安全的。默认情况下,数字类型的重复字段以packed 格式序列化,当期望 singular 字段时,这将无法正确解析。
  • enum 在线路格式方面与 int32uint32int64 和 uint64 兼容(请注意,如果值不适合,则会被截断)。但是,请注意,当消息被反序列化时,客户端代码可能会以不同的方式处理它们:例如,无法识别的 proto3 enum 值将保留在消息中,但是当消息被反序列化时,这在语言上是如何表示的是语言相关的。Int 字段始终只保留它们的值。
  • 将单个 optional 字段或扩展名更改为新的 oneof 的成员是二进制兼容的,但是对于某些语言(特别是 Go),生成的代码的 API 将以不兼容的方式更改。因此,Google 不会在其公共 API 中进行此类更改,如 AIP-180 中所述。关于源兼容性的相同警告,如果您确定没有代码一次设置多个字段,则将多个字段移动到新的 oneof 中可能是安全的。将字段移动到现有的 oneof 中是不安全的。同样,将单个字段 oneof 更改为 optional 字段或扩展名是安全的。
  • 在 map<K, V> 和相应的 repeated 消息字段之间更改字段是二进制兼容的(有关消息布局和其他限制,请参阅下面的映射)。但是,更改的安全性取决于应用程序:当反序列化和重新序列化消息时,使用 repeated 字段定义的客户端将产生语义上相同的结果;但是,使用 map 字段定义的客户端可能会重新排序条目并删除具有重复键的条目。

Protocol Buffer API

Quote

protobuf会通过 .proto 定义生成对应的.pb.h.pb.cc,里面包含一系列可使用的API,用来操作.proto中定义的对象。

对于协议:

syntax = "proto3";
 
package tutorial;
 
message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;
 
  enum PhoneType {
    PHONE_TYPE_UNSPECIFIED = 0;
    PHONE_TYPE_MOBILE = 1;
    PHONE_TYPE_HOME = 2;
    PHONE_TYPE_WORK = 3;
  }
 
  message PhoneNumber {
    optional string number = 1;
    optional PhoneType type = 2;
  }
 
  repeated PhoneNumber phones = 4;
}
 
message AddressBook {
  repeated Person people = 1;
}
 

会生成如下API:

  // name
  inline bool has_name() const;
  inline void clear_name();
  inline const ::std::string& name() const;
  inline void set_name(const ::std::string& value);
  inline void set_name(const char* value);
  inline ::std::string* mutable_name();
 
  // id
  inline bool has_id() const;
  inline void clear_id();
  inline int32_t id() const;
  inline void set_id(int32_t value);
 
  // email
  inline bool has_email() const;
  inline void clear_email();
  inline const ::std::string& email() const;
  inline void set_email(const ::std::string& value);
  inline void set_email(const char* value);
  inline ::std::string* mutable_email();
 
  // phones
  inline int phones_size() const;
  inline void clear_phones();
  inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phones() const;
  inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phones();
  inline const ::tutorial::Person_PhoneNumber& phones(int index) const;
  inline ::tutorial::Person_PhoneNumber* mutable_phones(int index);
  inline ::tutorial::Person_PhoneNumber* add_phones();
 

对于message phones类型,可以使用 mutable_phones(index)->CopyFrom(*new_phones)方式来修改值。

使用案例

安装Protocol Buffers编译器

软件包安装:

sudo apt install -y protobuf-compiler

源码安装:

集成到项目内部的话,为了控制版本,也可以选择源码安装。

AB.Base.proto

// AB.Base.proto
 
syntax = "proto3"; // 在开头指定版本
package AB.Base;
option optimize_for = SPEED; // 若开启 Any 则不能启用 LITE_RUNTIME;
 
enum ServiceID {
    SID_DEFAULT = 0;  
    SID_LOGIN   = 0x0001;
}
 
enum LoginCommandID {
    CID_LOGIN_UNDEFINE           = 0; 
    CID_LOGIN_INFO               = 0x0101; // 0x01开始使用的是 SID_LOGIN
    CID_LOGIN_GAME_RECORD        = 0x0102;
    CID_LOGIN_REQUEST            = 0x0103; 
    CID_LOGIN_RESPONSE           = 0x0104; 
}
 
enum ResultType {
    REFUSE_REASON_UNDEFINE       = 0;
    REFUSE_REASON_SUCCESS        = 1;
    REFUSE_REASON_ERROR          = 3;
    REFUSE_REASON_ACCOUNT_NULL   = 4; // 账号不存在
    REFUSE_REASON_ACCOUNT_LOCK   = 5; // 账号锁定
    REFUSE_REASON_PASSWORD_ERROR = 6; // 密码错误
}
 

AB.Login.proto

// AB.Login.proto
 
syntax = "proto3"; // 在开头指定版本
package AB.Login;
option optimize_for = SPEED; // 若开启 Any 则不能启用 LITE_RUNTIME;
 
import "AB.Base.proto";
import "google/protobuf/any.proto";
 
message ABLoginInfo {
    // CmdID: 0x0101
    optional string nickname = 1; // 昵称
    optional string icon     = 2; // 头像
    optional int64  coin     = 3; // 金币
    optional string location = 4; // 所属地
}
 
message ABLoginGameRecord {
    // CmdID: 0x0102
    optional string time   = 1;    // 时间
    optional int32  kill   = 2;    // 击杀数
    optional int32  dead   = 3;    // 死亡数
    optional int32  assist = 4;    // 助攻数
}
 
message ABLoginRequest {
    // CmdID: 0x0103
    optional string username = 1;
    optional string password = 2;
}
 
message ABLoginResponse {
    // CmdID: 0x0104
    optional uint32 user_id                 = 1;
    optional AB.Base.ResultType result_code = 2;
    optional ABLoginInfo user_info          = 3;
    repeated ABLoginGameRecord records      = 4;
    optional google.protobuf.Any any        = 5;
}

生成代码

protoc --cpp_out=. AB.Base.proto AB.Login.proto
ls *pb*

main.cpp

// main.cpp
#include "AB.Login.pb.h"
 
#include "assert.h"
 
#include <iostream>
#include <string>
 
#include <google/protobuf/wrappers.pb.h>
 
// parse hex
std::string &ToHexString(const std::string& bin, std::string &hexRet) {
    for (unsigned char c : bin) {
        hexRet += "0123456789ABCDEF"[c >> 4];
        hexRet += "0123456789ABCDEF"[c & 0xF];
        hexRet += " ";
    }
    return hexRet;
}
 
int main(int argc, char const* argv[])  
{
    AB::Login::ABLoginResponse login_res {};
 
    login_res.set_result_code(AB::Base::REFUSE_REASON_SUCCESS);
    auto user_info = login_res.mutable_user_info();
    user_info->set_nickname("dsw");
    user_info->set_icon("345DS55GF34D774S");
    user_info->set_coin(2000);
    user_info->set_location("zh");
    for (int i = 0; i < 5; ++i) {
        auto record = login_res.add_records();
        record->set_time("2017/4/13 12:22:11");
        record->set_kill(i * 4);
        record->set_dead(i * 2);
        record->set_assist(i * 5);
    }
    // wrappers google提供的 message 包装器
    google::protobuf::Int64Value int_wrapper;
    int_wrapper.set_value(99);
    // Any可以包装任意message类型
    login_res.mutable_any()->PackFrom(int_wrapper); 
    // 包装后可以用 Is判断类型 
    assert(login_res.any().Is<google::protobuf::Int64Value>() && "type error!");
 
    std::string buff {};
    login_res.SerializeToString(&buff);
    std::string buffHex;
    std::cout << "Serialize:" << ToHexString(buff, buffHex) << std::endl;
 
    AB::Login::ABLoginResponse login_res_parse {};
    if (!login_res_parse.ParseFromString(buff)) {
        std::cout << "parse error\n";
    }
 
    auto temp_user_info = login_res_parse.user_info();
    std::cout << "nickname:" << temp_user_info.nickname() << std::endl;
    std::cout << "coin:"     << temp_user_info.coin()     << std::endl;
    std::cout << "location:" << temp_user_info.location() << std::endl;
    for (int m = 0; m < login_res_parse.records_size(); ++m) {
        auto temp_record = login_res_parse.records(m);
        std::cout << "time:"     << temp_record.time()
                  << "\tkill:"   << temp_record.kill()
                  << "\tdead:"   << temp_record.dead()
                  << "\tassist:" << temp_record.assist()
                  << std::endl;
    }
    
    google::protobuf::Int64Value unpacked;
    // Any 类型不符合会解包失败。  
    if (login_res_parse.any().UnpackTo(&unpacked)) {
        std::cout << "Unpacked value: " << unpacked.value() << std::endl;
    } else {
        std::cerr << "Failed to unpack." << std::endl;
    }
}

编译

g++ -o game.bin main.cpp AB.Login.pb.cc AB.Base.pb.cc `pkg-config --cflags --libs protobuf`

运行

./game.bin
Serialize:10 01 1A 1E 0A 03 64 73 77 12 10 33 34 35 44 53 35 35 47 46 33 34 44 37 37 34 53 18 D0 0F 22 02 7A 68 22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 00 18 00 20 00 22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 04 18 02 20 05 22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 08 18 04 20 0A 22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 0C 18 06 20 0F 22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 10 18 08 20 14 2A 34 0A 2E 74 79 70 65 2E 67 6F 6F 67 6C 65 61 70 69 73 2E 63 6F 6D 2F 67 6F 6F 67 6C 65 2E 70 72 6F 74 6F 62 75 66 2E 49 6E 74 36 34 56 61 6C 75 65 12 02 08 63
nickname:dsw
coin:2000
location:zh
time:2017/4/13 12:22:11 kill:0  dead:0  assist:0
time:2017/4/13 12:22:11 kill:4  dead:2  assist:5
time:2017/4/13 12:22:11 kill:8  dead:4  assist:10
time:2017/4/13 12:22:11 kill:12 dead:6  assist:15
time:2017/4/13 12:22:11 kill:16 dead:8  assist:20
Unpacked value: 99 

序列化分析

给定的序列化数据如下:

10 01 
1A 1E 0A 03 64 73 77 12 10 33 34 35 44 53 35 35 47 46 33 34 44 37 37 34 53 18 D0 0F 22 02 7A 68 
22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 00 18 00 20 00 
22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 04 18 02 20 05 
22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 08 18 04 20 0A 
22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 0C 18 06 20 0F 
22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 10 18 08 20 14 
2A 34 
0A 2E 74 79 70 65 2E 67 6F 6F 67 6C 65 61 70 69 73 2E 63 6F 6D 2F 67 6F 6F 67 6C 65 2E 70 72 6F 74 6F 62 75 66 2E 49 6E 74 36 34 56 61 6C 75 65 
12 02 08 63

user_id: Default

Attention

字段 1(uint32 user_id)未出现在数据中,表示使用默认值(0)。

result_code: 10 01

首行数据:

10 01
  • 0x10
    Tag的二进制形式为 (field_number << 3 | wire_type)
    首位为Tag,也就是Tag=0x10,故 field_number = 0x10 >> 3 = 2wire_type = 0x10 & 3 = 0,也就是 字段2,也就是result_code。 wire_type = 0 表示varint方式组织数据,也就意味着是 Tag-Value结构。
  • 0x01:这意味着也就是后续的 01是Value, 即 result_code = 1

user_info: 1A 1E ...

第二行数据:

1A 1E 0A 03 64 73 77 12 10 33 34 35 44 53 35 35 47 46 33 34 44 37 37 34 53 18 D0 0F 22 02 7A 68 
  • 0x1A
    0x1A = 26,26 >> 3 = 3,Wire Type = 26 & 3 = 2(长度限定的消息)。
    → 表示 字段 3,即 user_info(类型 ABLoginInfo)。也就是使用 Tag-Length-Value方式组织数据。
  • 0x1E:长度字节,说明接下来有 Length = 0x1E = 30 个字节构成 ABLoginInfo 消息。

接下来 30 字节为 ABLoginInfo 格式数据。

ABLoginInfo user_info数据如下(每行一个子字段):

0A 03 64 73 77 
12 10 33 34 35 44 53 35 35 47 46 33 34 44 37 37 34 53 
18 D0 0F 
22 02 7A 68

我们逐个字段解析 ABLoginInfo:

  1. 昵称 (nickname) - 字段 1
    • 0A:0x0A = (1 << 3 | 2),字段编号 1,Wire Type 2(字符串)。
    • 0x03:长度为 3 字节。
    • 接下来的 3 字节:64 73 77 → ASCII 分别为 'd' 's' 'w'
      nickname = “dsw”.
  2. 头像 (icon) - 字段 2
    • 12:0x12 = (2 << 3 | 2),字段编号 2,Wire Type 2。
    • 0x10:长度为 16 字节。
    • 接下来的 16 字节:
      33 34 35 44 53 35 35 47 46 33 34 44 37 37 34 53
      转换为 ASCII 得到 "345DS55GF34D774S"
      icon = “345DS55GF34D774S”.
  3. 金币 (coin) - 字段 3
    • 18:0x18 = (3 << 3 | 0),字段编号 3,Wire Type 0(Varint)。
    • 接下来的 Varint 为:D0 0F
      • 0xD0:二进制 1101 0000,去掉最高位后的 7 位为 0x50 = 80。
      • 0x0F:最高位为 0,数值为 15。
        合并:值 = 15 × 128 + 80 = 1920 + 80 = 2000
        coin = 2000.
  4. 所属地 (location) - 字段 4
    • 22:0x22 = (4 << 3 | 2),字段编号 4,Wire Type 2。
    • 0x02:长度为 2。
    • 接下来的 2 字节:7A 68 → ASCII 为 'z' 'h'
      location = “zh”.

record 1: 22 14 ...

record 使用了 repeated 进行修饰,表示可以重复多次。使用Tag-Length-Value结构表示。

record 1数据如下:

22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 00 18 00 20 00
  • Tag22
    字段 4(record)的标签为 0x22((4 << 3 | 2)),后面跟随长度限定的消息。数据中有多个 record 实例,

  • 长度1A → 0x1A = 26 字节
    表示有 0x1A 长度的数据,

  • 数据内容(26 字节):
    根据定义可以知道,这些数据是ABLoginGameRecord数组

    0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 00 18 00 20 00

    解析 ABLoginGameRecord:

    • Field 1 (time)
      • 0A:表示字段 1,Wire Type 2。
      • 0x12:长度为 18 字节。
      • 接下来的 18 字节为 ASCII 字符串:32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31
        转换为文本得到 “2017/4/13 12:22:11”.


    > 若开启 option optimize_for = LITE_RUNTIME,则数据为: 22 14 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31
    > 长度为0x14(20),末尾的 10 00 18 00 20 00 被省略了,这时,记录 1 中只包含了时间字段,其余数字字段未设置,默认值均为 0。

    • Field 2 (kill)
      • 10:表示字段2,Wire Type 0
      • 00: kill = 0
    • Field 3 (dead)
      • 18: 字段3,Wire Type 0
      • 00: dead = 0
    • Field 4 (assist)
      • 20: 字段4, Wire Type 0
      • 00: assist = 0 → Record 1:time = “2017/4/13 12:22:11”, kill = 0, dead = 0, assist = 0.

record 2:22 1A ...

record 2数据:

22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 04 18 02 20 05 
  • Tag22

  • 长度1A → 0x1A = 26 字节

  • 数据内容(26 字节):

    0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 
    10 04 
    18 02 
    20 05

    解析:

    1. Field 1 (time)
      • 0A 12 … 同上,18 字节的字符串 → “2017/4/13 12:22:11”.
    2. Field 2 (kill)
      • 10:0x10 = (2 << 3 | 0),Wire Type 0。
      • 下一个字节 04 → kill = 4.
    3. Field 3 (dead)
      • 18:0x18 = (3 << 3 | 0)。
      • 下一个字节 02 → dead = 2.
    4. Field 4 (assist)
      • 20:0x20 = (4 << 3 | 0)。
      • 下一个字节 05 → assist = 5.

    Record 2:time = “2017/4/13 12:22:11”, kill = 4, dead = 2, assist = 5.

record 3:22 1A ...

record 3数据:

22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 08 18 04 20 0A 
  • Tag22

  • 长度1A26 字节

  • 数据内容:

    0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 
    10 08 
    18 04 
    20 0A

    解析:

    • time:同样解析出 “2017/4/13 12:22:11”
    • kill:Tag 0x10 后的值 0x08 → 8
    • dead:Tag 0x18 后的值 0x04 → 4
    • assist:Tag 0x20 后的值 0x0A → 10
      Record 3:time = “2017/4/13 12:22:11”, kill = 8, dead = 4, assist = 10.

record 4:22 1A ...

record 4数据:

22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 08 18 04 20 0A 
  • Tag22

  • 长度1A26 字节

  • 数据内容:

    0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 
    10 0C 
    18 06 
    20 0F

    解析:

    • time:解析得到 “2017/4/13 12:22:11”
    • kill:Tag 0x10 后的值 0x0C → 12
    • dead:Tag 0x18 后的值 0x06 → 6
    • assist:Tag 0x20 后的值 0x0F → 15
      Record 4:time = “2017/4/13 12:22:11”, kill = 12, dead = 6, assist = 15.

record 5:22 1A ...

record 5数据内容:

22 1A 0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 10 10 18 08 20 14
  • Tag22

  • 长度1A26 字节

  • 数据内容:

    0A 12 32 30 31 37 2F 34 2F 31 33 20 31 32 3A 32 32 3A 31 31 
    10 10 
    18 08 
    20 14

    解析:

    • time:解析得到 “2017/4/13 12:22:11”
    • kill:Tag 0x10 后的值 0x10 → 16
    • dead:Tag 0x18 后的值 0x08 → 8
    • assist:Tag 0x20 后的值 0x14 → 20
      Record 5:time = “2017/4/13 12:22:11”, kill = 16, dead = 8, assist = 20.

any:2A 34 ...

2A 34
0A 2E 74 79 70 65 2E 67 6F 6F 67 6C 65 61 70 69 73 2E 63 6F 6D 2F 67 6F 6F 67 6C 65 2E 70 72 6F 74 6F 62 75 66 2E 49 6E 74 36 34 56 61 6C 75 65
12 02 08 63
  • Tag: 2A field number 5, wire type 2 (length-delimited)
  • 长度: 34 长度为 52 字节(也就是剩下 52 字节组成 Any)
  • 数据内容:
    • type_url: 0A 2E 74 79 70 65 2E 67 6F 6F 67 6C 65 61 70 69 73 2E 63 6F 6D 2F 67 6F 6F 67 6C 65 2E 70 72 6F 74 6F 62 75 66 2E 49 6E 74 36 34 56 61 6C 75 65
      表示封装的数据类型:
        type.googleapis.com/google.protobuf.Int64Value
    • 封装类型内容: 12 02 08 63
      • Tag: 12 field number 2, wire type 2(即:value 字段)
      • 长度: 02 长度为2字节
      • 数据: 08 63 表示Int64Value数据内容 通过源码可以知道:
          message Int64Value {
              int64 value = 1;
          }
        • Tag: 08 表示 Field_number = 1, Wire Type = 0, int64 使用 Varints 编码数据
        • 数据: 63 表示值为 99

汇总还原结果

  • ABLoginResponse:
    • user_id:未设置,默认为 0
    • result_code1 (REFUSE_REASON_SUCCESS)
    • user_info:
      • nickname: “dsw”
      • icon: “345DS55GF34D774S”
      • coin: 2000
      • location: “zh”
    • record(共 5 条记录):
      1. Record 1: time = “2017/4/13 12:22:11”, kill = 0, dead = 0, assist = 0
      2. Record 2: time = “2017/4/13 12:22:11”, kill = 4, dead = 2, assist = 5
      3. Record 3: time = “2017/4/13 12:22:11”, kill = 8, dead = 4, assist = 10
      4. Record 4: time = “2017/4/13 12:22:11”, kill = 12, dead = 6, assist = 15
      5. Record 5: time = “2017/4/13 12:22:11”, kill = 16, dead = 8, assist = 20
    • any
      • type_url: "type.googleapis.com/google.protobuf.Int64Value"
      • value: 99

*****