我的Protobuf消息设计原则

拥有回忆 提交于 2019-12-04 01:04:43

网络通信涉及到消息的定义,不管是使用二进制模式、xml、json等格式。消息都可以大体的分为 命令消息、请求消息、应答消息和指示消息4大消息类型。一般情况下每个消息还还有包含一个序列号和一个能够唯一区分类型类型的消息编号,编号可以使用字符串、整数或者枚举等。

1. 使用 protobuf 的enum定于消息的编号,也就是消息的类型。

我会为每个系统都定义一个MSG枚举。包含系统用到的所有消息的枚举编号


enum MSG
{
  Login_Request  = 0x00001001;
  Login_Response = 0x00001002;

  XXX_Request  = 0x00001003;
  XXX_Request  = 0x00001004;

  XXX_Command = 0x00002001;

  XXX_Indication = 0x00003001;
}

2. 会为每个具有消息体的消息定义一个对应的protobuf message。例如Login_Request会有一个对应LoginRequest消息。



message LoginRequest
{
  required bytes username = 1;
  required string password = 2;
}

3. 会为每个消息大类定义一个消息,例如命令消息全部包含在message Command中,请求消息全部包含在Request消息中,应答消息全部包含在Response消息中,指示消息全部包含在Indication消息中。


也就是我会有下面4个protobuf message:


message Command
{// 包含所有的 XXXCommand 消息
}
message Request
{// 包含所有的 XXXRequest消息
}
message Response
{// 包含所有的Response消息
}
message Indication
{// 包含所有的Indication消息。
}

4. 对于应答消息,并非总是成功的,因此在应答消息中还会包含另外2个字段。一个用于描述应答是否成功,一个用于描述失败时的字符串信息。 对于有多个应答的消息来说,可能会包含是否为最后一个应答消息的标识。应答的序号(类似与网络数据包被分包以后,协议要合并时,需要知道分片在包中的具体位置)。因此Response看起来想这样:



message Response 
{
  required bool result = 1;
  optional bytes error_description = 2;
  required bool last_block = 3;
  required fixed32 block_index = 4;
  .....//其他的字段为 XXXResponse..
}

5. 最后我会定义一个大消息,把Command、Request、Response、Indication全部封装在一起,让后在通信的时候都动大消息开始编解码。大消息看起来想下面这样。。



message Message
{
   required MSG type = 1;
   required fixed32 sequence = 2;
   
   optional Request request = 3;
   optional Response response = 4;
   optional Command  command = 5;
   optional Indication indication = 6;
}

6. 发送数据和接收数据。


用于UDP的时候比较简单,因为每个数据包就是一个独立的Message消息,可以直接解码,或者编码后直接发送。

但是如果是使用于TCP的时候,由于涉及到粘包、拆包等处理,而且Message消息里面也没有包含长度相关的字段(不好处理),因此把Message编码后的消息嵌入另外一个二进制消息中。

使用4字节消息长度+Message(二进制数据)+(2字节CRC校验(可选))

其中4字节的内容,只包含Message的长度,不包含自身和CRC的长度。如果需要也可以包含,当要记得通信双方必须一致。

6. 消息处理(C++)

  编解码后,根据Message.type字段,可以知道要处理的消息,进行分发。不过一般情况下我不喜欢if、switch。所以我比较倾向于使用虚函数来处理。因此一般情况下我会定义一下的处理方法。

 

#pragma once

#include <Message.pb.h>
#include <memory>
#include <map>

#include "Client.h"

using std::shared_ptr;

class BaseHandler
{
public:
    BaseHandler(pbmsg::MSG type):type_(type){
        Register (this);
    }
    virtual ~BaseHandler(){}

    pbmsg::MSG GetType() const { return type_; }
	//具体处理方法,由派生类实现.
    virtual void Process( const shared_ptr<pbmsg::Message> & msg, const shared_ptr<Client> & client) = 0;

	//注册消息处理方法
    static void Register( BaseHandler *);
	//执行指定的消息,查询处理方法,调用Process。
    static void Execute( const shared_ptr<pbmsg::Message> & msg, const shared_ptr<Client> & client);
private:
    pbmsg::MSG type_;
    
   
private:
    static std::map<pbmsg::MSG , BaseHandler *> handers;
};
// 每个消息都实现Process的一个特化版本...
template< pbmsg::MSG Type>
class MessageHandler : public BaseHandler
{
public:
    MessageHandler(void):BaseHandler(Type){}
    ~MessageHandler(void){}

    void Process( const shared_ptr<pbmsg::Message> & msg, const shared_ptr<Client> & client);
private:
    static MessageHandler thisHandler;
   
};

///放在.cpp\.cxx文件中.

void BaseHandler::Register( BaseHandler * h )
{
    handers[h->GetType ()] = h;
}


void BaseHandler::Execute( const shared_ptr<pbmsg::Message> & msg , ...其它参数)
{
    auto it = handers.find(msg->type());
    if( it != handers.end ())
    {
        it->second->Process(msg,client);
    }else{
        LOG(ERROR) <<"消息 "<<msg->type()<<" 没有对应的处理方法.\n";;
    }
}
//对每个MSG 枚举的消息值,都会特化一个Process方法。
template<>
void MessageHandler<pbmsg::Login_Request>::Process( const shared_ptr<pbmsg::Message> & msg , ...其它参数){}
//并且在全局空间创建对象,系统启动时,自动创建。如果需要在堆空间中分配,另行封装方法,并调用下面的代码,让编译器实例化类。
MessageHandler<pbmsg::Login_Request> MessageHandler<pbmsg::Login_Request>::thisHandler;
// 最后消息处理:非常的easy:shared_ptr<pbmsg::Message> recvMessage( new pbmsg::Message());
bool parserOk = recvMessage->ParseFromArray((msg.rd_ptr ()+4), msg.size ()-4);
if( parserOk ){

    BaseHandler::Execute (recvMessage, ...其它参数);
  
 }

7. wireshark抓包

   protobuf是二进制的消息,wireshark抓包是无法直接分析的。不过google上面已经有了插件。 不过插件只支持UDP.本人在google上面的protobuf-wireshark的基础上修改了支持TCP的抓包解析,前提是顶层Message只有一个,而且封装在4个字节的长度后面。插件下载地址http://download.csdn.net/detail/chenxiaohong3905/5271945(wireshark 1.8.6版本). CSDN没分数的可以call me,留下你的邮箱。


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!