Protobuf消息设计原则(值得借鉴)

2020年01月12日 阅读数:54
这篇文章主要向大家介绍Protobuf消息设计原则(值得借鉴),主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

目录[-]shell

  • 1. 使用 protobuf 的enum定于消息的编号,也就是消息的类型。
  • 2. 会为每一个具备消息体的消息定义一个对应的protobuf message。例如Login_Request会有一个对应LoginRequest消息。
  • 3. 会为每一个消息大类定义一个消息,例如命令消息所有包含在message Command中,请求消息所有包含在Request消息中,应答消息所有包含在Response消息中,指示消息所有包含在Indication消息中。
  • 4. 对于应答消息,并不是老是成功的,所以在应答消息中还会包含另外2个字段。一个用于描述应答是否成功,一个用于描述失败时的字符串信息。 对于有多个应答的消息来讲,可能会包含是否为最后一个应答消息的标识。应答的序号(相似与网络数据包被分包之后,协议要合并时,须要知道分片在包中的具体位置)。所以Response看起来想这样:
  • 5. 最后我会定义一个大消息,把Command、Request、Response、Indication所有封装在一块儿,让后在通讯的时候都动大消息开始编解码。大消息看起来想下面这样。。
  • 6. 发送数据和接收数据。
  • 6. 消息处理(C++)
  • 7. wireshark抓包
  • ?

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

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

    我会为每一个系统都定义一个MSG枚举。包含系统用到的全部消息的枚举编号服务器


    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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消息。



    ?
    1
    2
    3
    4
    5
    message LoginRequest
    {
       required bytes username = 1;
       required string password = 2;
    }

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


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


    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    message Command
    { // 包含全部的 XXXCommand 消息
    }
    message Request
    { // 包含全部的 XXXRequest消息
    }
    message Response
    { // 包含全部的Response消息
    }
    message Indication
    { // 包含全部的Indication消息。
    }

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



    ?
    1
    2
    3
    4
    5
    6
    7
    8
    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所有封装在一块儿,让后在通讯的时候都动大消息开始编解码。大消息看起来想下面这样。。



    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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消息,能够直接解码,或者编码后直接发送。session

    可是若是是使用于TCP的时候,因为涉及到粘包、拆包等处理,并且Message消息里面也没有包含长度相关的字段(很差处理),所以把Message编码后的消息嵌入另一个二进制消息中。函数

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

    其中4字节的内容,只包含Message的长度,不包含自身和CRC的长度。若是须要也能够包含,当要记得通讯双方必须一致。ui

    6. 消息处理(C++)

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

     

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    #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,留下你的邮箱。google


    8. 附件:聊天服务器(Chat)定义google protobuf的协议接口文件

    接口主要遵循 Request、Response、Notification(Indication),Command(本文未出现)四大消息分类,而且使用Message顶层消息把Request、Response,Notification等包含起来;并定义一个MSG枚举值,用于表示具体的消息值(在google protobuf RPC过程当中,其实 每一个service方法就是一个Request和Response的应答对,只不过其消息值的编码是RPC自动分配的)

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    package chat;  // 定义protobuf的包名称空间,对应C++,C #的nanmespace,Java的package
    enum MSG
    {
      Login_Request  = 10001;
      Login_Response  = 10002;
      Logout_Request  = 10003;
      Logout_Response  = 10004;
      Keepalive_Request = 10005;
      Keepalive_Response = 10006;
      Get_Friends_Request = 10007;
      Get_Friends_Response = 10008;
      Send_Message_Request = 10009;
      Send_Message_Response = 10010;
      Friend_Notification = 20001;
      Message_Notification = 20002;
      Welcome_Notification = 20003;
    }
    /*下面定义具体的消息内容,MSG枚举中的每一个消息ID,若是有消息体,则会对应一个message 定义,若是无消息体则没必要要*/ 
    /*Login_Request 消息ID对应的消息名称为LoginRequest ; 规则为取掉下划线,有利于某些自动化编码工具编写自动化代码*/ 
    message LoginRequest
    {
      required bytes username = 1;
      optional string password = 2;
    }
    message LoginResponse
    {
      required fixed32 ttl = 1;
    }
    /*没有对应的MSG  id ,则为其它 消息的字段,做为子消息,能够消息嵌套定义,也能够放在外面,我的习惯放在外部。*/ 
    message Friend
    {
      required bytes name  = 1;
      optional bool  online = 2;
    }
    message GetFriendsResponse
    {
      repeated Friend  friends  = 1;
    }
    message SendMessageRequest
    {
      optional bytes receiver = 1;
      required bytes  text  = 2;
    }
    message FriendNotification
    {
      required bytes name  = 1;
      optional bool online = 2;
    }
    message MessageNotification
    {
      required bytes sender = 1;
      required bytes text = 2;
      required string timestamp = 3;
    }
    message WelcomeNotification
    {
      required  bytes text = 1;
    }
    /*请求消息集合,把全部的 XxxxxRequest消息所有集合在一块儿,使用起来相似于C语言的联合体,所有使用optional字段,任什么时候刻根据MSG 的 id 值,最多只有一个有效性, 从程序的逻辑上去保证,编译器(不论是protoc仍是具体语言的编译器都没法保证)*/ 
    message Request
    {
      optional LoginRequest login = 1;
      optional SendMessageRequest send_message = 2;
    }
    /*与Request做用相同,把全部的XxxxResponse消息集合在一块儿,看成联合体使用,不过额外多了几个字段用于表示应答的结果*/ 
    message Response
    {
      required bool result = 1;   //true 表示应答成功, false 表示应答失败
      required bool last_response = 2; //  一个请求能够包含多个应答,用于指示是否为最后一个应答
      optional bytes error_describe = 3; //  result ==  false 时,用于描述错误信息
      optional LoginResponse login = 4;
      optional GetFriendsResponse get_friends = 5;
    }
    /*与Request相同,把全部的XxxxxNotification消息集合在一块儿看成联合体使用.*/ 
    message Notification 
    {
      optional FriendNotification friend = 1;
      optional MessageNotification msg = 2;
      optional WelcomeNotification welcome = 3;
    }
    /*顶层消息,包含全部的Request,Response,Notification,具体包含哪一个消息又 MSG msg_type字段决定,程序逻辑去保证msg_type和具体的消息进行匹配*/ 
    message Message 
    {
      required MSG  msg_type = 1;
      required fixed32 sequence = 2; // 消息系列号,主要用于Request和Response,Response的值必须和Request相同,使得发送端能够进行事务匹配处理
      optional fixed32 session_id = 3;
      optional Request request  = 4;
      optional Response response = 5;
      optional Notification notification = 6;
    }

    本文摘自:http://my.oschina.net/cxh3905/blog/159122