|
网络编程中使用 Protobuf的两个先决条件
Google Protocol Buffers (简称Protobuf)是一款非常优秀的库, 它定义了一种紧凑(compact, 相对XML和JSON而言)的可扩展二进制消息格式,特别适合网络数据传输。
在网络编程中使用Protobuf需要解决以下两个问题。
1.长度,Protobuf打包的数据没有自带长度信息或终结符,需要由应用程序自己在发生和接收的时候做正确的切分。
2.类型,Protobuf打包的数据没有自带类型信息,需要由发送方把类型信息传给给接收方,接收方创建具体的Protobuf Message对象,再做反序列化。
根据type name自动创建Message的关键代码
1.用DescriptorPool: : generated_pool() 找到一个DescriptorPool对象,它包含了程序编译的时候所链接的全部Protobuf Message types。
2.根据type name用DescriptorPool: :FindMessageTypeByName()查找Descriptor。
3.再用MessageFactory: : generated_factory() 找到MessageFactory对象,它能创建程序编译的时候所链接的全部Protobuf Message types。
4.然后,用MessageF actory: :GetPrototype()找到具体Message type 的default instance。
5.最后,用prototype->New()创建对象。
示例代码如下。
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 ;
}
注意,createMessage()返回的是动态创建的对象的指针,调用方有责任释放它,不然就会使内存泄漏。在muduo里,我用shared_ptr<Message>来自动管理Message对象的生命期。
拿到Message*之后怎么办呢? 怎么调用这个具体消息类型的处理函数? 这就需要消息分发器( dispatcher)出马了。
Protobuf 传输格式
笔者设计了一个简单的格式,包含Protobuf data和其对应的长度与类型信息,消息的末尾还有一个check sum。格式如图7-26所示,图中方块的宽度是32-bit。
在muduo中实现Protobuf编解码器与消息分发器
为什么Protobuf的默认序列化格式没有包含消息的长度与类型
Protobuf是经过深思熟虑的消息打包方案,它的默认序列化格式没有包含消息的长度与类型,自然有其道理。哪些情况下不需要在Protobuf序列化得到的字节流中包含消息的长度和(或)类型? 能想到的答案有: .
●如果把消息写入文件, 一个文件存一个消息,那么序列化结果中不需要包含长度和类型,因为从文件名和文件长度中可以得知消息的类型与长度。
●如果把消息写入文件,一个文件存多个消息,那么序列化结果中不需要包含类型,因为文件名就代表了消息的类型。
●如果把消息存入数据库(或者NoSQL), 以VARBINARY字段保存,那么序列化结果中不需要包含长度和类型,因为从字段名和字段长度中可以得知消息的类型与长度。
●如果把消息以UDP方式发送给对方,而且对方一个UDP port只接收一种消息类型,那么序列化结果中不需要包含长度和类型,因为从port和UDP packet长度中可以得知消息的类型与长度。
●如果把消息以TCP短连接方式发给对方,而且对方一个TCP port只接收一种消息类型,那么序列化结果中不需要包含长度和类型,因为从port和TCP字节流长度中可以得知消息的类型与长度。
●如果把消息以TCP长连接方式发给对方,但是对方一个TCP port只接收一种消息类型,那么序列化结果中不需要包含类型,因为port代表了消息的类型。
●如果采用RPC方式通信,那么只需要告诉对方method name,对方自然能推断出Request和Response的消息类型,这些可以由protoc生成的RPC stubs自动搞定。
只有在使用TCP长连接,且在一个连接上传递不止一种消息的情况下(比方同时发Heartbeat和Request/Response), 才需要我前文提到的那种打包方案。这时候我们需要一个分发器dispatcher, 把不同类型的消息分给各个消息处理函数。
什么是编解码器(codec)
把网络数据和业务消息之间互相转换。
codec的基本功能之一是做TCP分包: 确定每条消息的长度,为消息划分界限。在non-blocking网络编程中,codec 几乎是必不可少的。
注意CharServer: :onStringMessage()的参数是std:string, 不再是muduo: :net: :Buffer,也就是说LengthHeaderCodec把Buffer解码成了string。另外, 在发送消息的时候,ChatServer通过LengthHeader-Codec: :send()来发送string,LengthHeaderCodec 负责把它编码成Buffer。“编解码器”名字的由来。消息流程如图7-29所示。
Protobuf codec与此非常类似,只不过消息类型从std::string变成了protobuf::Message。对于只接收处理Query消息的QueryServer来说,用ProtobufCodec非常方便,收到protobuf: :Message之后向下转型成Query来用就行(见图7-30 )。
实现ProtobufCodec
解码算法有几个要点:
●protobuf : :Message是new出来的对象,它的生命期如何管理? muduo采用shared_ptr<Message>来自动管理对象生命期,与整体风格保持一致。
●出错如何处理? 比方说长度超出范围、check sum不正确、 message type name不能识别、message parse出错等等。ProtobufCodec定义了ErrorCallback, 用户代码可以注册这个回调。如果不注册,默认的处理是断开连接,让客户重连重试。codec的单元测试里模拟了各种出错情况。
●如何处理一次收到半条消息、一条消息、一条半消息、两条消息等等情况? 这是每个non-blocking网络程序中的codec都要面对的问题。
消息分发器(dispatcher)有什么用
使用TCP长连接,且在一个连接上传递不止一种Protobuf消息的情况下,客户代码需要对收到的消息按类型做分发。比方说,收到Logon消息就交给QueryServer::onLogon()去处理,收到Query消息就交给QueryServer: :onQuery()去处理。这个消息分派机制可以做得稍微有点通用性,让所有muduo+Protobuf程序受益,而且不增加复杂性。
ProtobufDispatcher的两种实现
ProtobufDispatcherlite的结构非常简单(见图7-32), 它有一个map<Descriptor*,ProtobufMessageCallback> 成员,客户代码可以以Descriptor*为key注册回调(每个具体消息类型都有一个全局的Descriptor对象,其地址是不变的,可以用来当key)。在收到Protobuf Message之后,在map中找到对应的ProtobufMessageCallback,然后调用之。如果找不到,就调用defaultCallback。
不过,它的设计也有小小的缺陷,那就是ProtobufMessageCallback 限制了客户代码只能接受基类Message, 客户代码需要自己做向下转型(down cast), 如图7-33所示。
希望QueryServer这么设计: 不想每个消息处理函数自己做down cast, 而是交给dispatcher去处理,客户代码拿到的就已经是想要的具体类型。接口如下:
有一个办法,把多态与模板结合,利用templated derived class来提供类型上的灵活性。设计如图7-35所示。
ProtobufDispatcher有一个模板成员函数,可以接受注册任意消息类型T的回调,然后它创建一个模板化的派生类CallbackT<T>,这样消息的类型信息就保存在了CallbackT<T>中,做down cast就简单了。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|