(转)Protocol Buffers for C

我一直不太满意 google protocol buffers 的默认设计。为每个 message type 生成一大坨 C++ 代码让我很难受。而且官方没有提供 C 版本,第三方的 C 版本 也不让我满意。

这种设计很难让人做动态语言的 binding ,而大多数动态语言往往又没有强类型检查,采用生成代码的方式并没有特别的好处,反而有很大的性能损失(和通常做一个 bingding 库的方式比较)。比如官方的 Python 库,完全可以在运行时,根据协议,把那些函数生成出来,而不必用离线的工具生成代码。

去年的时候我曾经写过一个 lua 版本的库 。为了独立于官方版本,我甚至还用 lpeg 写了一个 .proto 文件的解析器。用了大约不到 100 行 lua 代码就可以解析出 .proto 文件内的协议内容。可以让 lua 库直接加载文本的协议描述文件。(这个东西这次帮了我大忙)

这次,我重新做项目,又碰到 protobuf 协议解析问题,想从头好好解决一下。上个月一开始,我想用 luajit 好好编写一个纯 lua 版。猜想,利用 luajit 和 ffi 可以达到不错的性能。但是做完以后,发现和 C++ 版本依然有差距 (大约只能达到 C++ 版本的 25% ~ 33% 左右的速度) ,比我去年写的 C + Lua binding 的方式要差。但是,去年写的那一份 C 代码和 Lua 代码结合太多。所以我萌生了重新写一份 C 实现的想法。

做到一半的时候,有网友指出,有个 googler 最近也在做类似的工作。μpb 这个项目在这里 。这里他写了一大篇东西阐述为什么做这样一份东西,大体上和我的初衷一致。不过他的 api 设计的不太好,我觉得太难用。所以这个项目并不妨碍我完成我自己的这一份。



C 版本之所以很难把 api 设计好,是因为 C 缺乏必要的数据结构。而且没有垃圾回收,缺乏数据类型的元信息。

考虑再三,我决定提供两套 api ,满足不同的需求。

当性能要求不太高的时候,仅仅满足 C 语言开发的便捷需要,提供一套简单易用的 api 操作 protobuf 格式的 message 。我称之为 message api 。

大体上有两组 api :

对于编码 protobuf 的消息,使用 rmessage 相关 api

struct pbc_rmessage * pbc_rmessage_new(struct pbc_env * env, const char * typename , struct pbc_slice * slice);
void pbc_rmessage_delete(struct pbc_rmessage *);

uint32_t pbc_rmessage_integer(struct pbc_rmessage * , const char *key , int index, uint32_t *hi);
double pbc_rmessage_real(struct pbc_rmessage * , const char *key , int index);
const char * pbc_rmessage_string(struct pbc_rmessage * , const char *key , int index, int *sz);
struct pbc_rmessage * pbc_rmessage_message(struct pbc_rmessage *, const char *key, int index);
int pbc_rmessage_size(struct pbc_rmessage *, const char *key);

对于解码消息,使用 wmessage 相关 api

struct pbc_wmessage * pbc_wmessage_new(struct pbc_env * env, const char *typename);
void pbc_wmessage_delete(struct pbc_wmessage *);

void pbc_wmessage_integer(struct pbc_wmessage *, const char *key, uint32_t low, uint32_t hi);
void pbc_wmessage_real(struct pbc_wmessage *, const char *key, double v);
void pbc_wmessage_string(struct pbc_wmessage *, const char *key, const char * v, int len);
struct pbc_wmessage * pbc_wmessage_message(struct pbc_wmessage *, const char *key);
void * pbc_wmessage_buffer(struct pbc_wmessage *, struct pbc_slice * slice);

pbc_rmessage_new 和 pbc_rmessage_delete 用来构造和释放 pbc_rmessage 结构。从结构中取出的子消息,字符串,都是由它来保证生命期的。这样不需要用户做过于繁杂的对象构建和销毁工作。

对于 repeated 的数据,没有额外再引入新的数据类型。而是把 message 内部的所有域都视为 repeated 。这种设计,可以极大的精简需要的 api 。

我们用 pbc_rmessage_size 可以查询 message 中某个 field 被重复了多少次。如果消息中并没有编码入这个 field ,它能返回 0 感知到。

我把所有的基本数据类型全部统一成了三种:integer , string , real 。bool 类型被当成 integer 处理。enum 类型即可以是 string ,也可以是 integer 。用 pbc_rmessage_string 时,可以取到 enum 的名字;用 pbc_rmessage_integer 则取得 id 。

pbc_rmessage_message 可以获得一个子消息,这个返回的对象不必显式的销毁,它的生命期挂接在父节点上。即使消息中没有编码入某个子消息,这个 api 依然可以正确的返回。从中取出的子域都将是默认值。

integer 不区分 32bit 数和 64bit 数。当你能肯定你需要的整数可以用 32bit 描述时,pbc_rmessage_integer 的最后一个参数可以传 NULL ,忽略高 32bit 的数据。

wmessage 的用法更像是不断的向一个未关闭的消息包类压数据。当你把整个消息的内容都填完后,可以用 pbc_wmessage_buffer 返回一个 slice 。这个 slice 里包含了 buffer 的指针和长度。

需要注意的是,如果使用 pbc_wmessage_integer 压入一个负数,一定要将高位传 -1 。因为接口一律把传入参数当成是无符号的整数。

考虑到某些内部实现的性能,以及后面讲提到的 pattern api 的方便性(如果你完全拿这个库做 C/S 通讯)。建议所有的 string 都在末尾加上 \\0 。因为,这样在解码的时候,可以将字符串指针直接指向数据包内,而不需要额外复制一份出来。

pbc_wmessage_string 可以压入非 \\0 结尾的字符串,因为压入的数据长度是由参数制定的。当然你也可以不自己计算长度。如果长度参数传 <=0 的话,库会帮你调用 strlen 检测。并且将最终的长度减去这个负数。即,如果你传 -1 ,就会帮你多压入最后的一个 \\0 字节。



Pattern API 可以得到更高的性能。更快的速度和更少的内存占用量。更重要的是,对于比较小的消息包,如果你使用得当,使用 pattern api 甚至不会触发哪怕一次堆上的内存分配操作。api 工作时的所有的临时内存都在栈上。

相关 api 如下:

struct pbc_pattern * pbc_pattern_new(struct pbc_env * , const char * message, const char *format, ...);
void pbc_pattern_delete(struct pbc_pattern *);
int pbc_pattern_pack(struct pbc_pattern *, void *input, struct pbc_slice * s);
int pbc_pattern_unpack(struct pbc_pattern *, struct pbc_slice * s , void * output);

我们首先需要创建一个 pattern 做编码和解码用。一个简单的例子是这样的:

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
}

这样一个消息,对于在 C 的结构体中,你可能希望是这样:

struct Person { pbcslice name; int32t id; pbc_slice email; } 这里使用 pbc_slice 来表示一个 string 。因为对于 message 来说,里面的字符串是有长度的。并且不一定以 \\0 结尾。slice 同样可以表示一个尚未解开的子消息。

我们使用 pbc_pattern_new 可以让 pbc 认识这个结构的内存布局。

struct pbc_pattern * Person_p = pbc_pattern_new(env , "Person" ,
  "name %s id %d email %s",
  offsetof(struct Person , name),
  offsetof(struct Person , id),
  offsetof(struct Person , email));

然后就可以用 pbc_pattern_pack 和 pbc_pattern_unpack 编码和解码了。pattern 的定义过程冗长而且容易出错(你也可以考虑用机器生成它们)。但我相信在性能及其敏感的场合,这些是值得的,如果你觉得写这些不值得,可以考虑用回上面的 message api 。

对于 repeated 的数据,pattern api 把他们看成一个数组 pbc_array 。

有这样一组 api 可以用来操作它:

int pbc_array_size(pbc_array);
uint32_t pbc_array_integer(pbc_array array, int index, uint32_t *hi);
double pbc_array_real(pbc_array array, int index);
struct pbc_slice * pbc_array_slice(pbc_array array, int index);

void pbc_array_push_integer(pbc_array array, uint32_t low, uint32_t hi);
void pbc_array_push_slice(pbc_array array, struct pbc_slice *);
void pbc_array_push_real(pbc_array array, double v);

数组是个略微复杂一些的数据结构,但如果你的数据不多的话,它也不会牵扯到堆上的额外内存分配。不过既然有可能调用这些 api 可能额外分配内存,那么你必须手工清除这些内存。而且在第一次使用前,必须初始化这个数据结构 (memset 为 0 是可以的)。

void pbc_pattern_set_default(struct pbc_pattern * , void *data);
void pbc_pattern_close_arrays(struct pbc_pattern *, void *data);

pbc_pattern_set_default 可以把一块内存,以一个 pattern 的形式,初始化所有的域。包括其中的数组的初始化。

pbc_pattern_close_arrays 使用完一块数据,需要手工调用这个 api ,关闭这个数据块中的数组。



关于 Extension ,我最后放弃了直接支持。没有提供类似 get extension 的 api 。这是因为,我们可以更简单的去处理 extension 。我把所有的 extension field 都加了前缀,如果需要,可以用拼接字符串的方式获得消息包内的扩展域。



最后介绍的是 pbc 的环境。

struct pbc_env * pbc_new(void);
void pbc_delete(struct pbc_env *);
int pbc_register(struct pbc_env *, struct pbc_slice * slice);

pbc 库被设计成没有任何全局变量,这样你想在多线程环境用会比较安全。虽然库并没有考虑线程安全问题,但在不同的线程中使用不同的环境是完全没有问题的。

每个环境需要独立注册需要的消息类型,传入一个 protobuf 库官方工具生成的 .pb 数据块即可。以 slice 的形式传入,register 完后,这块数据内存可以释放。

这个数据块其实是以 google.protobuf.FileDescriptorSet 类型来编码的。这个数据类型非常繁杂,使得 bootstrap 过程及其难写,这个在后面会谈到。



全部代码我已经开源方在 github 上了,可以在 https://github.com/cloudwu/pbc 取到代码。详细的用法也可以从那些 test 文件中找到例子。

这个东西很难写,所以代码很乱,在写这篇 blog 的时候我还没有开始整理代码的结构。大家想用的将就用,请善待 bug 和它的朋友们。

用一个复杂的 protobuf 协议来描述协议本身,真的很淡疼。当我们没有任何一个可用的协议解析库前,我们无法理解任何 protobuf 协议。这是一个先有鸡还是先有蛋的问题。就是说,我很难凭空写出一个 pbc_register 的 api ,因为它需要先 register 一个 google.protobuf.FileDescriptorSet 类型才能开始分析输入的包。

不依赖库本身去解析 google.protobuf.FileDescriptorSet 本身的定义是非常麻烦的。当然我可以利用 google 官方的工具生成 google.protobuf.FileDescriptorSet 的 C++ 解析类开始工作。但我偏偏又不希望给这个东西带来过多的依赖。

一开始我希望自定义一种更简单的格式来描述协议本身,没有过多的层次结构,只是一个平坦的数组。这样手工解析就有可能。本来我想给 protoc 写一个 plugin ,生成自定义的协议格式。后来放弃了这个方案,因为希望库用起来更简单一些。

但是这个方案还是部分使用了。这就是源代码中 bootstrap.c 部分的缘由。它读入一个更简单版本的 google.protobuf.FileDescriptorSet 的描述。这块数据是事先生成好的,放在 descriptor.pbc.h 里。生成这块数据使用了我去年完成的 lua 库。相关的 lua 代码就没有放出来了。当然到了今天,pbc 本身足够完善,我们可以用 pbc 写一个 C 版本。有兴趣的同学,可以在 test_pbc.c 的基础上修改。



事情总算告一段落了。连续写了 5000 行代码,我需要休息一下。

原文地址: <Protocol Buffers for C>

时间: 2024-10-12 16:30:43

(转)Protocol Buffers for C的相关文章

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

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

Java使用Protocol Buffers入门四步骤

Protocol Buffers(简称protobuf)是谷歌的一项技术,用于将结构化的数据序列化.反序列化,经常用于网络传输. 这货实际上类似于XML生成和解析,但protobuf的效率高于XML,不过protobuf生成的是字节码,可读性比XML差.类似的还有json.Java的Serializable等. protobuf支持各种语言.本文以Java为例,简单介绍protobuf如何使用.其他语言使用方法类似. 首先需要下载: http://download.csdn.net/downlo

如何在 PHP 中处理 Protocol Buffers 数据

Protocol Buffers是谷歌定义的一种跨语言.跨平台.可扩展的数据传输及存储的协议,因为将字段协议分别放在传输两端,传输数据中只包含数据本身,不需要包含字段说明,所以传输数据量小,解析效率高.感兴趣的可以访问这里.Protocol Buffers官方只支持C++, Java, Python, C#, Go,如果想在PHP中使用Protocol Buffers,需要借助于第三方的扩展,使用方法如下. 安装protoc编译器 第一步,安装Google的protoc编译器,这个工具可以把pr

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

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

FlatBuffers vs Protocol Buffers

FlatBuffers去年发布,最近看了一下,与同是出自Google之手的Protocol Buffers非常类似.在官网上介绍,FlatBuffers(简称FB)主要针对game development和对性能有要求的应用.相对于Protocol Buffers(简称PB),FB不需要解析,只通过序列化后的二进制buffer即可完成数据访问. FB的主要特点有: 1)数据访问不需要解析 将数据序列化成二进制buffer,之后的数据访问直接读取这个buffer,所以读取的效率很高. 2)内存高效

Protocol Buffers(protobuf)java初体验

由于项目需要所以简单的研究了下protobuf.我也是参照网上的博客,所以大部分内容我也就不重复造轮子了.首先protobuf介绍点击这里,使用介绍点击这里,使用demo看这里.我个人的第一个例子也是参照这个demo来的,不过其中我有遇到一些问题,所以揪出来说说,也就给自己做个笔记,方便查阅. 基本的东西相信大家也了解了,直接步入主题了: 1.限定修饰符介绍 required\optional\repeated,之前给定的博客已经有这个介绍了我也不多说,这里把一些小玩儿拿出来讲讲 ①.requi

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

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

理解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可以

Google Protocol Buffers 概述 转

Google Protocol Buffers 概述 个人小站,正在持续整理中,欢迎访问:http://shitouer.cn 小站博文地址:Google Protocol Buffers 概述 推荐阅读顺序,希望给你带来收获~ <Google Protocol Buffers 概述> <Google Protocol Buffers 入门> <Protocol Buffers 语法指南> <Google Protocol Buffers 编码(Encoding)