卓航论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 213|回复: 3
打印 上一主题 下一主题

一种自动反射消息类型的 Google Protobuf 网络传输方案

[复制链接]
[至尊红钻5级]发帖数量≥8000篇 [未点亮至尊黄钻]威望不足10点 [未点亮至尊蓝钻]在线时间不足10小时 [未点亮至尊绿钻]贡献度不足10点 [至尊紫钻4级]金币≥20000个 [未点亮至尊粉钻]精华贴数不足10贴 [未点亮至尊黑钻]活跃不足8个
 等级: 
 级别: 论坛元老
 UID:  7   [未点亮普号显示]钻石不足3个
 阁 分: 36567
 阁 望: 0
 阁 献: 0
 活 跃: 0
 发 贴: 12405 (0)
 阁 币: 24162  
性 别: I'm 火星人!
阅读权限: 90
在线时长: 0 小时
注册时间: 2016-10-16
最后登录: 2016-10-18
go
楼主
发表于 2016-10-17 16:22:42 |只看该作者 |倒序浏览
本帖发表于 2016-10-17 16:22:42...阅读 214 人...加油,亲爱的楼主:[db:作者]
这篇文章要解决的问题是:在接收到 protobuf 数据之后,如何自动创建具体的 Protobuf Message 对象 ,再做的反序列化。“自动”的意思是:当程序中新增一个 protobuf Message 类型时,这部分代码不 需要 修改,不需要自己去注册消息类型。其实,Google Protobuf 本身具有很强的反射(reflection)功能, 可以 根据 type name 创建具体类型的 Message 对象,我们直接利用即可。
本文假定读者了解 Google Protocol Buffers 是什么,这不是一篇 protobuf 入门教程。
本文以 C++ 语言举例,其他语 言估计 有类似的解法,欢迎补充。
本文的示例代码在: https://github.com/chenshuo/recipes/tree/master/protobuf
网络编程中使用 protobuf 的两个问题
Google Protocol Buffers (Protobuf) 是一款非常优秀的库,它定义了一种紧凑的可扩展二进制消 息格 式,特别适合网络数据传输。它为多种语言提供 binding,大大方便了分布式程序的开发,让系统不再 局限 于用某一种语言来编写。
在网络编程中使用 protobuf 需要解决两个问题:
长度,protobuf 打包的数据没有自带长度信息或终结符,需要由应用程序自己在发生和接收的时候 做正 确的切分;
类型,protobuf 打包的数据没有自带类型信息,需要由发送方把类型信息传给给接收方,接收方创 建具 体的 Protobuf Message 对象,再做的反序列化。
第一个很好解决,通常的做法是在每个消息前面加个固定长度的 length header,例如我在 《Muduo 网 络编程示例之二: Boost.Asio 的聊天服务器》 中实现的 LengthHeaderCodec,代码见 http://code.google.com/p/muduo/source/browse/trunk/examples/asio/chat/codec.h
第二个问题其实也很好解决,Protobuf 对此有内建的支持。但是奇怪的是,从网上简单搜索的情况 看, 我发现了很多山寨的做法。
山寨做法
以下均为在 protobuf data 之前加上 header,header 中包含 int length 和类型信息。类型信息 的山 寨做法主要有两种:
在 header 中放 int typeId,接收方用 switch-case 来选择对应的消息类型和处理函数;
在 header 中放 string typeName,接收方用 look-up table 来选择对应的消息类型和处理函数。
这两种做法都有问题。
第一种做法要求保持 typeId 的唯一性,它和 protobuf message type 一 一对应。如果 protobuf message 的使用范围不广,比如接收方和发送方都是自己维护的程序,那么 typeId 的唯一性不难保证,用版本管理工具即可。如果 protobuf message 的使用范围很大,比如全公司都在 用, 而且不同部门开发的分布式程序可能相互通信,那么就需要一个公司内部的全局机构来分配 typeId, 每次增 加新 message type 都要去注册一下,比较麻烦。
第二种做法稍好一点。typeName 的唯一性比 较好 办,因为可以加上 package name(也就是用 message 的 fully qualified type name),各个部门事 先分 好 namespace,不会冲突与重复。但是每次新增消息类型的时候都要去手工修改 look-up table 的初 始化代 码,比较麻烦。
其实,不需要自己重新发明轮子,protobuf 本身已经自带了解决方案。
根据 type name 反射自动创建 Message 对象
Google Protobuf 本身具有很强的反射(reflection)功能,可以根据 type name 创建具体类型的 Message 对象。但是奇怪的是,其官方教程里没有明确提及这个用法,我估计还有很多人不知道这个用 法, 所以觉得值得写这篇 blog 谈一谈。
以下是陈硕绘制的 Protobuf  class diagram。

               
我估计大家通常关心和使用的是图的左半部分:MessageLite、Message、Generated Message Types (Person, AddressBook) 等,而较少注意到图的右半部分:Descriptor, DescriptorPool, MessageFactory 。
上图中,其关键作用的是 Descriptor class,每个具体 Message Type 对应一个 Descriptor 对 象。尽管我们没有直接调用它的函数,但是Descriptor在“根据 type name 创建具体类型的 Message 对象 ”中扮演了重要的角色,起了桥梁作用。上图的红色箭头描述了根据 type name 创建具体 Message 对 象的 过程,后文会详细介绍。
原理简述
Protobuf Message class 采用了 prototype pattern,Message class 定义了 New() 虚函数,用 以返回 本对象的一份新实例,类型与本对象的真实类型相同。也就是说,拿到 Message* 指针,不用知道它的 具体 类型,就能创建和它类型一样的具体 Message Type 的对象。
每个具体 Message Type 都有一个 default instance,可以通过 ConcreteMessage::default_instance () 获得,也可以通过 MessageFactory::GetPrototype(const Descriptor*) 来获得。所以,现在问题 转变 为 1. 如何拿到 MessageFactory;2. 如何拿到 Descriptor*。
当然, ConcreteMessage::descriptor() 返回了我们想要的 Descriptor*,但是,在不知道 ConcreteMessage 的时 候,如何调用它的静态成员函数呢?这似乎是个鸡与蛋的问题。
我们的英雄是 DescriptorPool ,它 可以根据 type name 查到 Descriptor*,只要找到合适的 DescriptorPool,再调用 DescriptorPool::FindMessageTypeByName(const string& type_name) 即可。眼前一亮?
在最 终解决问题之前,先简单测试一下,看看我上面说的对不对。
简单测试
本文用于举例的 proto 文件:query.proto,见 https://github.com/chenshuo/recipes/blob/master/protobuf/query.proto
package muduo;

message Query {
  required int64 id = 1;
  required string questioner = 2;

  repeated string question = 3;
}

message Answer {
  required int64 id = 1;
  required string questioner = 2;
  required string answerer = 3;

  repeated string solution = 4;
}

message Empty {
  optional int32 id = 1;
}
其中的 Query.questioner 和 Answer.answerer 是我在前一篇文章这提到的《分布式系
统中的
进程标识》。
               
以下代码验证 ConcreteMessage::default_instance()、ConcreteMessage::descriptor()、 MessageFactory::GetPrototype()、DescriptorPool::FindMessageTypeByName() 之间的不变式 (invariant):
https://github.com/chenshuo/recipes/blob/master/protobuf/descriptor_test.cc#L15
  typedef muduo::Query T;

  std::string type_name = T::descriptor()->full_name();
  cout FindMessageTypeByName
(type_name);
  assert(descriptor == T::descriptor());
  cout GetPrototype(descriptor);
  assert(prototype == &T::default_instance());
  cout New());
  assert(new_obj != NULL);
  assert(new_obj != prototype);
  assert(typeid(*new_obj) == typeid(T::default_instance()));
  cout New() = "
根据 type name 自动创建 Message 的关键代码
好了,万事具备,开始行动:
用 DescriptorPool::generated_pool() 找到一个 DescriptorPool 对象,它包含了程序编译的 时候所 链接的全部 protobuf Message types。
用 DescriptorPool::FindMessageTypeByName() 根据 type name 查找 Descriptor。
再用 MessageFactory::generated_factory() 找到 MessageFactory 对象,它能创建程序编译的 时候 所链接的全部 protobuf Message types。
然后,用 MessageFactory::GetPrototype() 找到具体 Message Type 的 default instance。
最后,用 prototype->New() 创建对象。
示例代码见 https://github.com/chenshuo/recipes/blob/master/protobuf/codec.h#L69
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;
}
调用方式: https://github.com/chenshuo/recipes/blob/master/protobuf/descriptor_test.cc#L49
  Message* newQuery = createMessage("muduo.Query");
  assert(newQuery != NULL);
  assert(typeid(*newQuery) == typeid(muduo::Query::default_instance()));
  cout
               
古 之人不余欺也 :-)
注意,createMessage() 返回的是动态创建的对象的指针,调用方有责任释 放它, 不然就会内存泄露。在 muduo 里,我用 shared_ptr 来自动管理 Message 对象的生命期。
线 程安全 性
Google 的文档说,我们用到的那几个 MessageFactory 和 DescriptorPool 都是线程安全的, Message::New() 也是线程安全的。并且它们都是 const member function。
关键问题解决了, 那么 剩下工作就是设计一种包含长度和消息类型的 protobuf 传输格式。
Protobuf 传输格式
陈硕设计了一个简单的格式,包含 protobuf data 和它对应的长度与类型信息,消息的末尾还有 一个 check sum。格式如下图,图中方块的宽度是 32-bit。

用 C struct 伪代码描述:
struct ProtobufTransportFormat __attribute__
((__packed__))
{
   int32_t  len;
   int32_t  nameLen;
   char     typeName[nameLen];
   char     protobufData[len-nameLen-8];
   int32_t  checkSum; // adler32 of nameLen, typeName and protobufData
};
注意,这个格式不要求 32-bit 对齐,我们的 decoder 会自动处理非对齐的消息。
例子
用这个格式打包一个 muduo.Query 对象的结果是:

查看本栏目更多精彩内容:http://www.bianceng.cn/Programming/cplus/
               
设计决策
以下是我在设计这个传输格式时的考虑:
signed int。消息中的长度字段只使用了 signed 32-bit int,而没有使用 unsigned int,这是 为了移 植性,因为 Java 语言没有 unsigned 类型。另外 Protobuf 一般用于打包小于 1M 的数据,unsigned int 也没用。
check sum。虽然 TCP 是可靠传输协议,虽然 Ethernet 有 CRC-32 校验,但是网络传输必须要考 虑数 据损坏的情况,对于关键的网络应用,check sum 是必不可少的。对于 protobuf 这种紧凑的二进制格 式而 言,肉眼看不出数据有没有问题,需要用 check sum。
adler32 算法。我没有选用常见的 CRC-32,而是选用 adler32,因为它计算量小、速度比较快, 强度和 CRC-32差不多。另外,zlib 和 java.unit.zip 都直接支持这个算法,不用我们自己实现。
type name 以 '\0' 结束。这是为了方便 troubleshooting,比如通过 tcpdump 抓下来 的包可 以用肉眼很容易看出 type name,而不用根据 nameLen 去一个个数字节。同时,为了方便接收方处理 ,加入 了 nameLen,节省 strlen(),空间换时间。
没有版本号。Protobuf Message 的一个突出优点是用 optional fields 来避免协议的版本号(凡 是在 protobuf Message 里放版本号的人都没有理解 protobuf 的设计),让通信双方的程序能各自升级, 便于系 统演化。如果我设计的这个传输格式又把版本号加进去,那就画蛇添足了。具体请见本人《分布式系统 的工 程化开发方法》第 57 页:消息格式的选择。
示例代码
为了简单起见,采用 std::string 来作为打包的产物,仅为示例。
打包 encode 的代码: https://github.com/chenshuo/recipes/blob/master/protobuf/codec.h#L35
解包 decode 的代码: https://github.com/chenshuo/recipes/blob/master/protobuf/codec.h#L99
测试代码: https://github.com/chenshuo/recipes/blob/master/protobuf/codec_test.cc
如果以上代码编译通过,但是在运行时出现“cannot open shared object file”错误,一般可以 用 sudo ldconfig 解决,前提是 libprotobuf.so 位于 /usr/local/lib,且 /etc/ld.so.conf 列出了这 个目 录。
$ make all # 如果你安装了 boost,可以 make whole
$ ./codec_test
./codec_test: error while loading shared libraries: libprotobuf.so.6: cannot open shared object file: No such file or directory
$ sudo ldconfig
与 muduo 集 成
muduo 网络库将会集成对本文所述传输格式的支持(预计 0.1.9 版本),我会另外写一篇短文介 绍 Protobuf Message  muduo::net::Buffer 的相互转化,使用 muduo::net::Buffer 来打包比 上面 std::string 的代码还简单,它是专门为 non-blocking 网络库设计的 buffer class。
此外, 我们 可以写一个 codec 来自动完成转换,就行 asio/char/codec.h 那样。这样客户代码直接收到的就是 Message 对象,发送的时候也直接发送 Message 对象,而不需要和 Buffer 对象打交道。
消息的分发 (dispatching)
目前我们已经解决了消息的自动创建,在网络编程中,还有一个常见任务是把不同类型的 Message 分发 给不同的处理函数,这同样可以借助 Descriptor 来完成。我在 muduo 里实现了 ProtobufDispatcherLite 和 ProtobufDispatcher 两个分发器,用户可以自己注册针对不同消息类型的处理函数。预计将会在 0.1.9 版本发布,您可以先睹为快:
初级版,用户需要自己做 down casting: https://github.com/chenshuo/recipes/blob/master/protobuf/dispatcher_lite.cc
高级版,使用模板技巧,节省用户打字: https://github.com/chenshuo/recipes/blob/master/protobuf/dispatcher.cc
基于 muduo 的 Protobuf RPC?
Google Protobuf 还支持 RPC,可惜它只提供了一个框架,没有开源网络相关的代码,muduo 正好 可以 填补这一空白。我目前还没有决定是不是让 muduo 也支持以 protobuf message 为消息格式的 RPC, muduo 还有很多事情要做,我也有很多博客文章打算写,RPC 这件事情以后再说吧。
注:Remote Procedure Call (RPC) 有广义和狭义两种意思。狭义的讲,一般特指 ONC RPC,就是用来实现 NFS 的那个东西; 广义 的讲,“以函数调用之名,行网络通信之实”都可以叫 RPC,比如 Java RMI,.Net Remoting,Apache Thrift,libevent RPC,XML-RPC 等等。
分享到: QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏0 支持支持0 反对反对0

使用道具 举报

[至尊红钻3级]发帖数量≥1000篇 [至尊黄钻3级]威望≥500点 [至尊蓝钻1级]在线时间≥10小时 [至尊绿钻9级]贡献度≥80000点 [至尊紫钻9级]金币≥8000000个 [未点亮至尊粉钻]精华贴数不足10贴 [未点亮至尊黑钻]活跃不足8个
 等级: 
 级别: 管理员
 UID:  1   [未点亮普号显示]钻石不足3个
 阁 分: 2147483647
 阁 望: 993
 阁 献: 2147483647
 活 跃: 0
 发 贴: 1532 (0)
 阁 币: 9430326  
性 别: I'm 火星人!
阅读权限: 200
在线时长: 46 小时
注册时间: 2016-10-14
最后登录: 2023-10-3
沙发
发表于 2016-10-18 00:02:00 |只看该作者
本回复帖发表于 2016-10-18 00:02:00,感谢爱情孤儿对本帖的认真回复,你的回复是对楼主莫大的鼓舞
真是 收益 匪浅

使用道具 举报

[至尊红钻2级]发帖数量≥100篇 [未点亮至尊黄钻]威望不足10点 [未点亮至尊蓝钻]在线时间不足10小时 [未点亮至尊绿钻]贡献度不足10点 [未点亮至尊紫钻]金币不足100个 [未点亮至尊粉钻]精华贴数不足10贴 [未点亮至尊黑钻]活跃不足8个
 等级: 
 级别: 注册会员
 UID:  36   [未点亮普号显示]钻石不足3个
 阁 分: 180
 阁 望: 6
 阁 献: 6
 活 跃: 0
 发 贴: 160 (0)
 阁 币: 2  
性 别: I'm 火星人!
阅读权限: 20
在线时长: 1 小时
注册时间: 2011-1-6
最后登录: 2016-10-21
板凳
发表于 2016-10-19 11:09:46 |只看该作者
本回复帖发表于 2016-10-19 11:09:46,感谢rrssttrst对本帖的认真回复,你的回复是对楼主莫大的鼓舞
没看完~~~~~~ 先顶,好同志

使用道具 举报

[至尊红钻2级]发帖数量≥100篇 [未点亮至尊黄钻]威望不足10点 [未点亮至尊蓝钻]在线时间不足10小时 [未点亮至尊绿钻]贡献度不足10点 [未点亮至尊紫钻]金币不足100个 [未点亮至尊粉钻]精华贴数不足10贴 [未点亮至尊黑钻]活跃不足8个
 等级: 
 级别: 中级会员
 UID:  19   [未点亮普号显示]钻石不足3个
 阁 分: 267
 阁 望: 9
 阁 献: 5
 活 跃: 0
 发 贴: 240 (0)
 阁 币: 4  
性 别: I'm 火星人!
阅读权限: 30
在线时长: 3 小时
注册时间: 2011-1-6
最后登录: 2016-10-21
地板
发表于 2016-10-21 00:38:34 |只看该作者
本回复帖发表于 2016-10-21 00:38:34,感谢a58523对本帖的认真回复,你的回复是对楼主莫大的鼓舞
有道理。。。

使用道具 举报

高级模式
B Color Image Link Quote Code Smilies
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1、请认真发帖,禁止回复纯表情,纯数字等无意义的内容!帖子内容不要太简单!
2、提倡文明上网,净化网络环境!抵制低俗不良违法有害信息。
3、每个贴内连续回复请勿多余3贴,每个版面回复请勿多余10贴!
4、如果你对主帖作者的帖子不屑一顾的话,请勿回帖。谢谢合作!

手机版| 百度搜索:vkee.pw

2012-2015 卓航旗下 GMT+8, 2024-6-2 11:35 , Processed in 0.646950 second(s), 30 queries . Powered by Discuz! X3.2  

禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.如遇版权问题,请及时联系站长(QQ:5213513)

今天是: | 本站已经安全运行: //这个地方可以改颜色

快速回复 返回顶部 返回列表