protobuf 的运用

关于Protobuf

    protobuf是google的一个开源序列化框架,基于二进制数据交换格式,兼顾了效率和灵活性。详见http://code.google.com/p/protobuf/html

    本文假定读者对protobuf已经有了初步接触,故略过一些基本和细节的描述,着重于介绍protobuf在笔者项目中的应用思路。因项目主要编程语言 是c++,因此本文的示例着眼于如何在c++中借助protobuf简化一些通用模块的处理。对于其它语言(java,python等),若是语言自己没 有更好用的特性或者更方便的库的话,也可采起相似作法。前端


不只仅是网络消息序列化

    虽然protobuf最普遍的应用是网络数据交换,但在使用过程当中发现其框架自己的设计具备极灵活的扩展性,对于服务器来讲,彻底能够做为通用的数据解析层。一言以蔽之,即能够将各类形式的外部数据以很是易用的方式存取,经过最天然的本地语言对象进行操做(在本文中是c++)。借助protobuf框架的如下特性,构建protobuf生态圈将会是一个轻松愉快的过程:java

1.代码生成器

    protobuf提供了自定义代码生成器的支持——“代码生成器”远没有它听起来那么复杂,相反,得益于protobuf合理的架构设计,笔者项目中的各种生成器通常只有几百行代码。python

    protobuf的代码生成器能够跟静态语言的编译器进行类比,编译器:mysql

    源码 ---> (编译器前端) ---> 中间代码 ---> (编译器后端) ---> 机器码c++

    而protobuf的代码生成过程以下:sql

    proto文件 ---> (解析器) --->  本地语言对象模型 ---> (代码生成器) ---> 本地语言源码数据库

    咱们要作的仅仅是自定义代码生成后端,只需关心proto文件中定义的对象模型便可,复杂的proto文件解析由protoc内置的解析器完成。编程

2.反射机制

    protobuf提供了反射机制(包括c++的版本),以实现对运行期对象进行内省。相关示例代码见http://code.google.com/apis/protocolbuffers/docs/reference/cpp/google.protobuf.message.html后端

    借助反射,能够完成一些在传统c++设计领域中难以完成的事情,好比对一个对象的全部属性进行迭代,好比根据属性的名字进行存取等等。其实现也至关高效,例如以下代码:

Message* message = ...;  
const Reflection* reflection = ...;  
const FieldDescriptor* field = ...;  
reflection->SetInt32(message, field);

最后一行对于message相应属性的定位是直接经过地址偏移来实现的(有点hack的意味),其偏移值经过代码生成器生成并保存在FieldDescriptor中。

案例一:利用Protobuf实现数据库粘合层

   

动机:

    因为语言特性的缘由,c++中对数据库的操做一直是个比较繁琐的过程。以mysql为例,官方的c api远不适合不加封装直接使用,mysql++库的Specialized SQL Structures尚可,但也还不够便利,好比没有has语义使得只update一个结构的其中几个字段的写法比较冗长,好比表的字段有增删须要手动维护c++中相应的结构等等。

    利用protobuf,彻底能够实现更易用而强大的数据库粘合层。

   

简单示例:

    如下是笔者项目中数据库粘合层的用法示例(为了便于说明,作了些简化,没有演示索引、字符集、存储引擎等功能)。

    首先,定义proto文件。

test_data.proto:

message TestData  
{  
    required int32 id = 1;  
    optional string name = 2;  
}

这样,自定义的代码生成器会生成如下sql(在笔者的项目中,这一步是自动完成的,每当有须要导出sql的proto文件,则编译时自动生成相应sql并执行,相应的DROP语句是版本降级时用的):

CREATE TABLE TESTDATA(  
    id INT NOT NULL,  
    name VARCHAR);  
DROP TABLE TESTDATA;

  在c++中,就能够用如下方式操做:

Query query = database->Query();  
TestData data;    // proto文件中定义的对象  
data.set_id(1);  
data.set_name("Snake");  
if (!query.Insert(data)) { // INSERT INTO TESTDATA (id, name) VALUES(1, "Snake");  
    ....  
}  
data.set_name("Raiden");  
TestData where;  
where.set_id(1);  
if (!query.Update(data, where)) { // 经过同类型对象来描述where子句, 至关于where id=1  
    ...  
}  
std::vector<TestData> messages;  
query.Select(messages,     // 把结果集储存到messages中,接受std顺序容器, 也接受单个message  
             Columns("id")("name"),   // 指定column, SELECT * 可用AllColumns()  
             Where("id") == 1 && Where("name") == "Raiden",    // 另外一种形式的WHERE子句  
             OrderBy("id").Desc(),   // ORDER BY ID DESC  
             Limit(10));   // LIMIT 10, 仅做为示例  
for (size_i = 0; i < messages.size(); ++i) {  
    std::cout << messages[i].DebugString();  
}  
query.Delete<TestData>();    // DELETE FROM TESTDATA, 一样支持两种形式的WHERE

以上只是基本用法示例。在笔者项目中实现的一些扩展功能还有(主要是利用protobuf的option机制实现):

    一、导出控制。能够灵活设置单个proto文件、单个消息、单个字段是否须要导出。

    二、版本号控制。能够指明某个字段的版本号,自动化生成相应脚本,方便运营时升级/降级。

    四、对复杂结构的支持。原生数据类型基本上均可以跟sql中的数据类型一一对应,对于repeated字段和子message,采用blob类型存储其序列化后的数据。

    五、数据库相应选项声明。好比设置表索引、所用引擎、字符集、字段长度、auto_increment等等。

    六、增量update。能够自动比较message与上次update的差别,只update有改动的字段。

   

    考虑以下案例:给原有的数据库表增长一个字段,读写,发送给其它网络主机进行处理。采用最原始的纯手工方式须要经历如下步骤:

    一、手动写alter table并执行;

    二、给c++中相应结构增长字段;

    三、修改insert,select,update等等涉及该字段的sql;

    四、修改相关网络同步的消息代码,增长此字段的同步;

    很快,涉及数据库的改动成为了bug滋生的温床。

    再看看前述方案,好比须要给TestData增长一新字段new_column,只须要作这么一件事,很好的体现了SPOT原则:

message TestData  
{  
    required int32 id = 1;  
    optional string name = 2;  
    optional float new_column = 3 [(sql.column_version) = "1.0.1"];  
}

new_column 为新增字段,其后跟随的选项描述了其版本号。不用再作其它操做——alter table的sql语句被自动生成并执行,update、insert、select等调用自动兼容(内部实现是经过反射机制来拼接sql语句的),结构 天生为网络数据交换而设计,因此不用改动网络同步相关代码,也不用操心版本不匹配的问题。Perfect!

 

 

              ...待续,下篇将介绍protobuf与lua结合、用protobuf进行配置文件解析等方案。

相关文章
相关标签/搜索