找回密码
 立即注册
查看: 309|回复: 0

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

[复制链接]
发表于 2022-3-5 09:54 | 显示全部楼层 |阅读模式
网络编程中使用 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就简单了。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2025-5-10 01:15 , Processed in 0.143620 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2025 Discuz! Team.

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