目录[-]
- 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抓包
1 |
网络通信涉及到消息的定义,不管是使用二进制模式、xml、json等格式。消息都可以大体的分为 命令消息、请求消息、应答消息和指示消息4大消息类型。一般情况下每个消息还还有包含一个序列号和一个能够唯一区分类型类型的消息编号,编号可以使用字符串、整数或者枚举等。
1. 使用 protobuf 的enum定于消息的编号,也就是消息的类型。
我会为每个系统都定义一个MSG枚举。包含系统用到的所有消息的枚举编号
01 | enum MSG |
02 | { |
03 | Login_Request = 0x00001001; |
04 | Login_Response = 0x00001002; |
05 |
06 | XXX_Request = 0x00001003; |
07 | XXX_Request = 0x00001004; |
08 |
09 | XXX_Command = 0x00002001; |
10 |
11 | XXX_Indication = 0x00003001; |
12 | } |
2. 会为每个具有消息体的消息定义一个对应的protobuf message。例如Login_Request会有一个对应LoginRequest消息。
1 | message LoginRequest |
2 | { |
3 | required bytes username = 1; |
4 | required string password = 2; |
5 | } |
3. 会为每个消息大类定义一个消息,例如命令消息全部包含在message Command中,请求消息全部包含在Request消息中,应答消息全部包含在Response消息中,指示消息全部包含在Indication消息中。
也就是我会有下面4个protobuf message:
01 | message Command |
02 | { // 包含所有的 XXXCommand 消息 |
03 | } |
04 | message Request |
05 | { // 包含所有的 XXXRequest消息 |
06 | } |
07 | message Response |
08 | { // 包含所有的Response消息 |
09 | } |
10 | message Indication |
11 | { // 包含所有的Indication消息。 |
12 | } |
4. 对于应答消息,并非总是成功的,因此在应答消息中还会包含另外2个字段。一个用于描述应答是否成功,一个用于描述失败时的字符串信息。 对于有多个应答的消息来说,可能会包含是否为最后一个应答消息的标识。应答的序号(类似与网络数据包被分包以后,协议要合并时,需要知道分片在包中的具体位置)。因此Response看起来想这样:
1 | message Response |
2 | { |
3 | required bool result = 1; |
4 | optional bytes error_description = 2; |
5 | required bool last_block = 3; |
6 | required fixed32 block_index = 4; |
7 | ..... //其他的字段为 XXXResponse.. |
8 | } |
5. 最后我会定义一个大消息,把Command、Request、Response、Indication全部封装在一起,让后在通信的时候都动大消息开始编解码。大消息看起来想下面这样。。
01 | message Message |
02 | { |
03 | required MSG type = 1; |
04 | required fixed32 sequence = 2; |
05 | |
06 | optional Request request = 3; |
07 | optional Response response = 4; |
08 | optional Command command = 5; |
09 | optional Indication indication = 6; |
10 | } |
6. 发送数据和接收数据。
用于UDP的时候比较简单,因为每个数据包就是一个独立的Message消息,可以直接解码,或者编码后直接发送。
但是如果是使用于TCP的时候,由于涉及到粘包、拆包等处理,而且Message消息里面也没有包含长度相关的字段(不好处理),因此把Message编码后的消息嵌入另外一个二进制消息中。
使用4字节消息长度+Message(二进制数据)+(2字节CRC校验(可选))
其中4字节的内容,只包含Message的长度,不包含自身和CRC的长度。如果需要也可以包含,当要记得通信双方必须一致。
6. 消息处理(C++)
编解码后,根据Message.type字段,可以知道要处理的消息,进行分发。不过一般情况下我不喜欢if、switch。所以我比较倾向于使用虚函数来处理。因此一般情况下我会定义一下的处理方法。
01 | #pragma once |
02 |
03 | #include <Message.pb.h> |
04 | #include <memory> |
05 | #include <map> |
06 |
07 | #include "Client.h" |
08 |
09 | using std::shared_ptr; |
10 |
11 | class BaseHandler |
12 | { |
13 | public : |
14 | BaseHandler(pbmsg::MSG type):type_(type){ |
15 | Register ( this ); |
16 | } |
17 | virtual ~BaseHandler(){} |
18 |
19 | pbmsg::MSG GetType() const { return type_; } |
20 | //具体处理方法,由派生类实现. |
21 | virtual void Process( const shared_ptr<pbmsg::Message> & msg, const shared_ptr<Client> & client) = 0; |
22 |
23 | //注册消息处理方法 |
24 | static void Register( BaseHandler *); |
25 | //执行指定的消息,查询处理方法,调用Process。 |
26 | static void Execute( const shared_ptr<pbmsg::Message> & msg, const shared_ptr<Client> & client); |
27 | private : |
28 | pbmsg::MSG type_; |
29 | |
30 | |
31 | private : |
32 | static std::map<pbmsg::MSG , BaseHandler *> handers; |
33 | }; |
34 | // 每个消息都实现Process的一个特化版本... |
35 | template < pbmsg::MSG Type> |
36 | class MessageHandler : public BaseHandler |
37 | { |
38 | public : |
39 | MessageHandler( void ):BaseHandler(Type){} |
40 | ~MessageHandler( void ){} |
41 |
42 | void Process( const shared_ptr<pbmsg::Message> & msg, const shared_ptr<Client> & client); |
43 | private : |
44 | static MessageHandler thisHandler; |
45 | |
46 | }; |
47 |
48 |
49 | ///放在.cpp\.cxx文件中. |
50 |
51 | void BaseHandler::Register( BaseHandler * h ) |
52 | { |
53 | handers[h->GetType ()] = h; |
54 | } |
55 |
56 |
57 | void BaseHandler::Execute( const shared_ptr<pbmsg::Message> & msg , ...其它参数) |
58 | { |
59 | auto it = handers.find(msg->type()); |
60 | if ( it != handers.end ()) |
61 | { |
62 | it->second->Process(msg,client); |
63 | } else { |
64 | LOG(ERROR) << "消息 " <<msg->type()<< " 没有对应的处理方法.\n" ;; |
65 | } |
66 | } |
1 |
1 | //对每个MSG 枚举的消息值,都会特化一个Process方法。 |
1 | template <> |
2 | void MessageHandler<pbmsg::Login_Request>::Process( const shared_ptr<pbmsg::Message> & msg , ...其它参数){} |
1 | //并且在全局空间创建对象,系统启动时,自动创建。如果需要在堆空间中分配,另行封装方法,并调用下面的代码,让编译器实例化类。 |
01 | MessageHandler<pbmsg::Login_Request> MessageHandler<pbmsg::Login_Request>::thisHandler; |
02 |
03 | |
04 | |
05 | |
06 | |
07 | < div > |
08 |
09 |
10 | |
11 | |
12 | |
13 | |
14 | </ div > |
01 | <p> |
02 | // 最后消息处理:非常的easy: |
03 | </p> |
04 |
05 | <p> |
06 | shared_ptr<pbmsg::Message> recvMessage( new pbmsg::Message()); |
07 | bool parserOk = recvMessage->ParseFromArray((msg.rd_ptr ()+4), msg.size ()-4); |
08 | if ( parserOk ){ |
09 |
10 | BaseHandler::Execute (recvMessage, ...其它参数); |
11 | |
12 | } |
13 | </p> |
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,留下你的邮箱。
分布式应用中基于事件驱动的应用开发模型
本文将为您讲述如何为分布式应用建立事件驱动的开发模型。并以Linux系统为例,展示了事件驱动开发的基本框架。
1 评论:
前言
从目前看,大量数据的流动仍然主要分布在局域网的分布式系统中,该类系统的大流量、实时性的特点要求系统具有实时响应、交互动作异步非耦合、高可用性、高可得到性等特征。而因为系统主要局限在局域网内运行,因而在系统的构建上应用要具有灵活多样可靠稳定的性能。事实上,良好的局域网应用是联入广域网的前提。
在该类分布式系统中,引导数据流动和分布式动作的往往是事件的作用,或者称之为消息。事件是激活和驱动分布式系统的直接原因,也是进一步构建分布式对象管理的基础。利用事件驱动开发模型,可以快速构建分布式系统应用,提高分布式应用和整个分布式系统的运行效率。
事件驱动的开发模型
首先,在分布式系统中,事件是异步非耦合的,这是系统实时性的要求。因为在分布式系统中,往往各个关键应用既是服务器应用,也是客户应用,相互之间从功能上看是对等关系,如果在同步情况下,一个应用驱动了另一个应用的事件,这个应用则必须堵塞,以等待事件执行的返回状态,这样这个应用在这段时间内则不能处理实时事件。而各个应用之间相互不完全依赖的情况也决定了分布式系统中事件的两端必须为非耦合。
事件驱动的开发模型包括如下几个部分:
- 事件定义库:该库定义了应用所有认知的事件描述,基本信息包括事件标识,事件特征,事件处理例程,事件所属软件包等。
- 事件检测模块:该模块负责处理事件的收集和分发,当接收事件后,分析事件的类型、特征,然后作出相应的处理,该模块是开发模型的核心。
- 事件执行模块:负责获知事件入口,解码事件携带数据,正确执行相应事件。事件执行模块可以支持事件的持久性,持久性事件的接收方如果中间停止,而在再次启动后仍可以接收事件。
- 事件信道:是分布式应用之间事件流动的通道。事件信道是单向的,适应分布式应用的多样性要求,事件信道存在多种类型,主要有:1)永久可靠的信道。应用之间具有频繁的事件往来或固定的周期性的关键事件要采用这类信道;2)一次性可靠信道。事件的收发具有偶然性,不经常性,但是事件不可丢失;3)广播数据报不可靠信道。信道上的事件是一对多的关系,可以容忍偶然的数据报丢失或失序,如某一些关键应用的指示存在的心跳事件可采用该类信道;4)单播数据报信道;5)数据报可靠信道。这类信道主要适应于一对多的可靠事件,该信道在不可靠数据报基础上增加了数据报序列号和事件重发请求,以便形成可靠的事件通道。事件通道为分布式应用提高运行效率提供了基础设施。
- 事件输入源:是驱动事件的源应用。包括了事件数据的编码和封装,编码是为了使得事件可以在不同的系统平台上流动,封装的事件数据中,头部信息固定的为公共可以识别的信息。
- 最后要提供事件驱动应用API:包括了事件的注册API,以及实现以上各功能的各类API。API要求简单明了,为应用程序员快速构建分布式应用提供支持。
Linux系统上事件驱动的开发框架
目前,Linux操作系统逐渐为人们所接受,在Linux上的分布式系统也逐渐获得了更广泛的应用。本文在这里描绘了在Linux系统上基于以上事件驱动模型的简化开发框架。基于这个框架,还可以在Linux上开发出实用、可靠、稳定的事件驱动软件包。
2.1 事件定义库
事件定义库记录了应用所关心的所有事件信息,是事件测试模块的扫描数据区。定义库要求查询方便快速,定义库可以自由扩展。例如可以用以下的结构实现:
typedef struct { // 指向下一个事件信息的指针 void *ptr; // 事件ID int event_id; // 事件属性,指示事件是否活动等。 int event_propertiy; // 定义事件的数据内容 struct event_info info; // 事件处理例程,包括了数据信息和信道信息 int (int *)(*event_process*)(void *data_buff, void *channel_info); } event_table struct event_db { // 有效的事件数量 int event_num; // 事件列表 event_table *event; }
2.2 事件检测模块
事件的检测是通过对一系列感兴趣的文件描述符的检测来完成的。感兴趣的文件描述符包括在文件描述符集内,一个感兴趣的文件描述符对应于一类事件。当我们检测到一个文件描述符可读时,就知道应用内已经驱动了一个事件。通过扫描事件定义库可以找到相应的事件,然后就可以提交给事件执行模块。
可以用如下代码实现事件检测模块:
int rts; fd_set fdset; ...... rts = select(FD_SETSIZE,&fdset,NULL,NULL,NULL,0); if(rts > 0 ) { if(rts == ......) { /* 如果检测到I/O描述符为某一类型的描述符,则*/ ...... /* 将数据提交给事件执行模块 */ } else if(rts == ......) { ...... } }
2.3 事件注册模块
事件要注册后,应用才可以辨识并驱动该事件。事件的注册过程实际上是定义事件信息,将信息写入事件定义库的过程。
2.4 事件信道
事件信道不仅定义了事件流动的基础设施,也定义了相应的事件I/O描述符。例如在一个应用内部的驱动事件,事件信道的实现可以采用命名管道,返回的描述符用于检测内部的驱动事件。可以用以下代码实现:
int io_inner mkfifo(fifo_filename,mode) // 创建一个FIFO文件 io_inner = open(fifo_filename, mode); // 获得该文件描述符
应用之间的驱动事件的信道要用到socket描述符。应用之间建立了socket链路后即建立了事件流动的信道,返回的socket描述符用于检测应用之间的驱动事件。各种类型的信道均可加入检测的I/O描述符集。注意,对应于服务器端,执行如下代码:
int sock = socket(......),
返回的描述符可以直接加入检测描述符集,而当在检测到客户连接后,返回新的描述符也要加入检测描述符集,因为这个描述符才是检测驱动事件的描述符。如将创建的sock加入检测描述符集:
FD_SET(sock, &fdset);
2.5 事件执行模块
当有驱动事件时,事件检测模块将调用事件的执行模块,事件的执行模块则接收事件数据信息,同时解析事件驱动源信息和信道信息。
对应应用内部驱动事件,当检测到命名管道描述符可读时,则检测事件定义库,当事件处于活动时,则接收定义库内缓冲区的数据执行该事件例程。对应于应用之间的驱动事件,则当检测到socket描述符可读时,启动如下代码:
rts = recv(sock, (void *) buff, ......)
接收到事件数据后,检测头部信息,取出要驱动的事件ID,连通接收的通信数据提交给事件处理例程。
2.6 事件的驱动
对应应用内部事件,一方面通过命名管道通知检测模块,另一方面,要把事件的数据写入定义库,并设置事件处于活动状态。而应用之间的驱动事件,对数据进行封装和编码后,在底层要用如下socket函数发送:
rts = send(sock, (void *) buff, ......);
安装如上的驱动事件开发框架,可以在Linux较快的开发出相应的软件包。
结束语
该开发模型事件驱动的细节都封装在内部,总体结构清晰明了,只留了一些注册、驱动事件的接口给应用开发程序员。有了基于事件驱动软件包后,可以继续开发分布式对象管理,增强分布式系统的构建能力。