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

本文介绍如何使用Protobuf设计网络通信消息,包括定义消息编号、消息体结构、响应处理及消息分发机制。提供了C++消息处理的具体实现示例。

目录[-]

  • 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大消息类型。一般情况下每个消息还还有包含一个序列号和一个能够唯一区分类型类型的消息编号,编号可以使用字符串、整数或者枚举等。

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消息,可以直接解码,或者编码后直接发送。

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

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

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

6. 消息处理(C++)

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

 

?
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.youkuaiyun.com/detail/chenxiaohong3905/5271945(wireshark 1.8.6版本). 优快云没分数的可以call me,留下你的邮箱。


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


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值