欢迎关注微信公众号「随手记技术团队」,查看更多随手记团队的技术文章。转载请注明出处
本文做者:丁同舟
原文连接:mp.weixin.qq.com/s/cyOHe1LS-…html
随手记客户端与服务端交互的过程当中,对部分数据的传输大小和效率有较高的要求,普通的数据格式如 JSON 或者 XML 已经不能知足,所以决定采用 Google 推出的 Protocol Buffers 以达到数据高效传输。linux
Protocol buffers 为 Google 提出的一种跨平台、多语言支持且开源的序列化数据格式。相对于相似的 XML 和 JSON,Protocol buffers 更为小巧、快速和简单。其语法目前分为proto2
和proto3
两种格式。json
相对于传统的 XML 和 JSON, Protocol buffers 的优点主要在于:更加小、更加快。对于自定义的数据结构,Protobuf 能够经过生成器生成不一样语言的源代码文件,读写操做都很是方便。缓存
假设如今有下面 JSON 格式的数据:bash
{
"id":1,
"name":"jojo",
"email":"123@qq.com",
}
复制代码
使用 JSON 进行编码,得出byte
长度为43
的的二进制数据:微信
7b226964 223a312c 226e616d 65223a22 6a6f6a6f 222c2265 6d61696c 223a2231 32334071 712e636f 6d227d
复制代码
若是使用 Protobuf 进行编码,获得的二进制数据仅有20
个字节网络
0a046a6f 6a6f1001 1a0a3132 33407171 2e636f6d
复制代码
相对于基于纯文本的数据结构如 JSON、XML等,Protobuf 可以达到小巧、快速的最大缘由在于其独特的编码方式。IBM 的 developerWorks 上面有一篇Google Protocol Buffer 的使用和原理对 Protobuf 的 Encoding 做了很好的解析数据结构
例如,对于int32
类型的数字,若是很小的话,protubuf 由于采用了Varint
方式,能够只用 1 个字节表示。函数
Varint 中每一个字节的最高位 bit 表示此 byte 是否为最后一个 byte 。1
表示后续的 byte 也表示该数字,0
表示此 byte 为结束的 byte。ui
例如数字 300 用 Varint 表示为 1010 1100 0000 0010
Note
须要注意解析的时候会首先将两个 byte 位置互换,由于字节序采用了 little-endian 方式。
但 Varint 方式对于带符号数的编码效果比较差。由于带符号数一般在最高位表示符号,那么使用 Varint 表示一个带符号数不管大小就必需要 5 个 byte(最高位的符号位没法忽略,所以对于 -1
的 Varint 表示就变成了 010001
)。
Protobuf 引入了 ZigZag
编码很好地解决了这个问题。
关于 ZigZag 的编码方式,博客园上的一篇博文整数压缩编码 ZigZag作出了详细的解释。
ZigZag 编码按照数字的绝对值进行升序排序,将整数经过一个 hash 函数h(n) = (n<<1)^(n>>31)
(若是是 sint64 h(n) = (n<<1)^(n>>63)
)转换为递增的 32 位 bit 流。
n | 补码 | h(n) | ZigZag (hex) |
---|---|---|---|
0 | 00 00 00 00 | 00 00 00 00 | 00 |
-1 | ff ff ff ff | 00 00 00 01 | 01 |
1 | 00 00 00 01 | 00 00 00 02 | 02 |
... | ... | ... | ... |
-64 | ff ff ff c0 | 00 00 00 7f | 7f |
64 | 00 00 00 40 | 00 00 00 80 | 80 01 |
... | ... | ... | ... |
关于为何 64 的 ZigZag 为 80 01
,上面的文章中有关于其编码惟一可译性的解释。
经过 ZigZag 编码,只要绝对值小的数字,均可以用较少位的 byte 表示。解决了负数的 Varint 位数会比较长的问题。
Protobuf 的消息结构是一系列序列化后的Tag-Value
对。其中 Tag 由数据的 field
和 writetype
组成,Value 为源数据编码后的二进制数据。
假设有这样一个消息:
message Person {
int32 id = 1;
string name = 2;
}
复制代码
其中,id
字段的field
为1
,writetype
为int32
类型对应的序号。编码后id
对应的 Tag 为 (field_number << 3) | wire_type = 0000 1000
,其中低位的 3 位标识 writetype
,其余位标识field
。
每种类型的序号能够从这张表获得:
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64 | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
5 | 32-bit | fixed32, sfixed32, float |
须要注意,对于string
类型的数据(在上表中第三行),因为其长度是不定的,因此 T-V
的消息结构是不能知足的,须要增长一个标识长度的Length
字段,即T-L-V
结构。
Protobuf 自己具备很强的反射机制,能够经过 type name 构造具体的 Message 对象。陈硕的文章中对 GPB 的反射机制作了详细的分析和源码解读。这里经过 protobuf-objectivec 版本的源码,分析此版本的反射机制。
陈硕对 protobuf 的类结构作出了详细的分析 —— 其反射机制的关键类为Descriptor
类。
每一个具体 Message Type 对应一个 Descriptor 对象。尽管咱们没有直接调用它的函数,可是Descriptor在“根据 type name 建立具体类型的 Message 对象”中扮演了重要的角色,起了桥梁做用
同时,陈硕根据 GPB 的 C++ 版本源代码分析出其反射的具体机制:DescriptorPool
类根据 type name 拿到一个 Descriptor
的对象指针,在经过MessageFactory
工厂类根据Descriptor
实例构造出具体的Message
对象。示例代码以下:
Message* createMessage(const std::string& typeName)
{
Message* message = NULL;
const Descriptor* descriptor = DescriptorPool::generated_pool()->FindMessageTypeByName(typeName);
if (descriptor)
{
const Message* prototype = MessageFactory::generated_factory()->GetPrototype(descriptor);
if (prototype)
{
message = prototype->New();
}
}
return message;
}
复制代码
Note
DescriptorPool 包含了程序编译的时候所连接的所有 protobuf Message types MessageFactory 能建立程序编译的时候所连接的所有 protobuf Message types
在 OC 环境下,假设有一份 Message 数据结构以下:
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
复制代码
解码此类型消息的二进制数据:
Person *newP = [[Person alloc] initWithData:data error:nil];
复制代码
这里调用了
- (instancetype)initWithData:(NSData *)data error:(NSError **)errorPtr {
return [self initWithData:data extensionRegistry:nil error:errorPtr];
}
复制代码
其内部调用了另外一个构造器:
- (instancetype)initWithData:(NSData *)data
extensionRegistry:(GPBExtensionRegistry *)extensionRegistry
error:(NSError **)errorPtr {
if ((self = [self init])) {
@try {
[self mergeFromData:data extensionRegistry:extensionRegistry];
//...
}
@catch (NSException *exception) {
//...
}
}
return self;
}
复制代码
去掉一些防护代码和错误处理后,能够看到最终由mergeFromData:
方法实现构造:
- (void)mergeFromData:(NSData *)data extensionRegistry:(GPBExtensionRegistry *)extensionRegistry {
GPBCodedInputStream *input = [[GPBCodedInputStream alloc] initWithData:data]; //根据传入的`data`构造出数据流对象
[self mergeFromCodedInputStream:input extensionRegistry:extensionRegistry]; //经过数据流对象进行merge
[input checkLastTagWas:0]; //校检
[input release];
}
复制代码
这个方法主要作了两件事:
GPBCodedInputStream
对象实例GPBCodedInputStream
负责的工做很简单,主要是把源数据缓存起来,并同时保存一系列的状态信息,例如size
, lastTag
等。其数据结构很是简单:
typedef struct GPBCodedInputStreamState {
const uint8_t *bytes;
size_t bufferSize;
size_t bufferPos;
// For parsing subsections of an input stream you can put a hard limit on
// how much should be read. Normally the limit is the end of the stream,
// but you can adjust it to anywhere, and if you hit it you will be at the
// end of the stream, until you adjust the limit.
size_t currentLimit;
int32_t lastTag;
NSUInteger recursionDepth;
} GPBCodedInputStreamState;
@interface GPBCodedInputStream () {
@package
struct GPBCodedInputStreamState state_;
NSData *buffer_;
}
复制代码
merge 操做内部实现比较复杂,首先会拿到一个当前 Message 对象的 Descriptor 实例,这个 Descriptor 实例主要保存 Message 的源文件 Descriptor 和每一个 field 的 Descriptor,而后经过循环的方式对 Message 的每一个 field 进行赋值。
Descriptor 简化定义以下:
@interface GPBDescriptor : NSObject<NSCopying>
@property(nonatomic, readonly, strong, nullable) NSArray<GPBFieldDescriptor*> *fields;
@property(nonatomic, readonly, strong, nullable) NSArray<GPBOneofDescriptor*> *oneofs; //用于 repeated 类型的 filed
@property(nonatomic, readonly, assign) GPBFileDescriptor *file;
@end
复制代码
其中GPBFieldDescriptor
定义以下:
@interface GPBFieldDescriptor () {
@package
GPBMessageFieldDescription *description_;
GPB_UNSAFE_UNRETAINED GPBOneofDescriptor *containingOneof_;
SEL getSel_;
SEL setSel_;
SEL hasOrCountSel_; // *Count for map<>/repeated fields, has* otherwise.
SEL setHasSel_;
}
复制代码
其中GPBMessageFieldDescription
保存了 field 的各类信息,如数据类型、filed 类型、filed id等。除此以外,getSel
和setSel
为这个 field 在对应类的属性的 setter 和 getter 方法。
mergeFromCodedInputStream:
方法的简化版实现以下:
- (void)mergeFromCodedInputStream:(GPBCodedInputStream *)input
extensionRegistry:(GPBExtensionRegistry *)extensionRegistry {
GPBDescriptor *descriptor = [self descriptor]; //生成当前 Message 的`Descriptor`实例
GPBFileSyntax syntax = descriptor.file.syntax; //syntax 标识.proto文件的语法版本 (proto2/proto3)
NSUInteger startingIndex = 0; //当前位置
NSArray *fields = descriptor->fields_; //当前 Message 的全部 fileds
//循环解码
for (NSUInteger i = 0; i < fields.count; ++i) {
//拿到当前位置的`FieldDescriptor`
GPBFieldDescriptor *fieldDescriptor = fields[startingIndex];
//判断当前field的类型
GPBFieldType fieldType = fieldDescriptor.fieldType;
if (fieldType == GPBFieldTypeSingle) {
//`MergeSingleFieldFromCodedInputStream` 函数中解码 Single 类型的 field 的数据
MergeSingleFieldFromCodedInputStream(self, fieldDescriptor, syntax, input, extensionRegistry);
//当前位置+1
startingIndex += 1;
} else if (fieldType == GPBFieldTypeRepeated) {
// ...
// Repeated 解码操做
} else {
// ...
// 其余类型解码操做
}
} // for(i < numFields)
}
复制代码
能够看到,descriptor
在这里是直接经过 Message 对象中的方法拿到的,而不是经过工厂构造:
GPBDescriptor *descriptor = [self descriptor];
//`desciptor`方法定义
- (GPBDescriptor *)descriptor {
return [[self class] descriptor];
}
复制代码
这里的descriptor
类方法其实是由GPBMessage
的子类具体实现的。例如在Person
这个消息结构中,其descriptor
方法定义以下:
+ (GPBDescriptor *)descriptor {
static GPBDescriptor *descriptor = nil;
if (!descriptor) {
static GPBMessageFieldDescription fields[] = {
{
.name = "name",
.dataTypeSpecific.className = NULL,
.number = Person_FieldNumber_Name,
.hasIndex = 0,
.offset = (uint32_t)offsetof(Person__storage_, name),
.flags = GPBFieldOptional,
.dataType = GPBDataTypeString,
},
//...
//每一个field都会在这里定义出`GPBMessageFieldDescription`
};
GPBDescriptor *localDescriptor = //这里会根据fileds和其余一系列参数构造出一个`Descriptor`对象
descriptor = localDescriptor;
}
return descriptor;
}
复制代码
接下来,在构造出 Message 的 Descriptor 后,会对全部的 fields 进行遍历解码。解码时会根据不一样的fieldType
调用不一样的解码函数,例如对于
fieldType == GPBFieldTypeSingle
复制代码
会调用 Single 类型的解码函数:
MergeSingleFieldFromCodedInputStream(self, fieldDescriptor, syntax, input, extensionRegistry);
复制代码
MergeSingleFieldFromCodedInputStream
内部提供了一系列宏定义,针对不一样的数据类型进行数据解码。
#define CASE_SINGLE_POD(NAME, TYPE, FUNC_TYPE) \ case GPBDataType##NAME: { \ TYPE val = GPBCodedInputStreamRead##NAME(&input->state_); \ GPBSet##FUNC_TYPE##IvarWithFieldInternal(self, field, val, syntax); \ break; \ }
#define CASE_SINGLE_OBJECT(NAME) \ case GPBDataType##NAME: { \ id val = GPBCodedInputStreamReadRetained##NAME(&input->state_); \ GPBSetRetainedObjectIvarWithFieldInternal(self, field, val, syntax); \ break; \ }
CASE_SINGLE_POD(Int32, int32_t, Int32)
...
#undef CASE_SINGLE_POD
#undef CASE_SINGLE_OBJECT
复制代码
例如对于int32
类型的数据,最终会调用int32_t GPBCodedInputStreamReadInt32(GPBCodedInputStreamState *state);
函数读取数据并赋值。这里内部实现其实就是对于 Varint 编码的解码操做:
int32_t GPBCodedInputStreamReadInt32(GPBCodedInputStreamState *state) {
int32_t value = ReadRawVarint32(state);
return value;
}
复制代码
在对数据解码完成后,拿到一个int32_t
,此时会调用GPBSetInt32IvarWithFieldInternal
进行赋值操做,其简化实现以下:
void GPBSetInt32IvarWithFieldInternal(GPBMessage *self, GPBFieldDescriptor *field, int32_t value, GPBFileSyntax syntax) {
//最终的赋值操做
//此处`self`为`GPBMessage`实例
uint8_t *storage = (uint8_t *)self->messageStorage_;
int32_t *typePtr = (int32_t *)&storage[field->description_->offset];
*typePtr = value;
}
复制代码
其中typePtr
为当前须要赋值的变量的指针。至此,单个 field 的赋值操做已经完成。
总结一下,在 protobuf-objectivec 版本中,反射机制中构建 Message 对象的流程大体为: