近段日子在做一个比较复杂的项目,其中用到了开源软件ZMQ和MessagePack。ZMQ对底层网络通信进行了封装,是一个消息处理队列库, 使用起来非常方便。MessagePack是一个基于二进制的对象序列化类库,具有跨语言的特性,同样非常容易使用。在我做的项目中,消息类通过 MessagePack进行压包,然后写入ZMQ的消息结构体,通过ZMQ传递,最后接收者利用MessagePack进行解包,从而分析命令。由于我英 语水平实在不高,所以我并没有通过阅读它们的说明文档来对它们进行了解,而仅仅是通过它们的示例代码进行探索。虽然因此遇到了一些不解问题,但这种方式却 为我节省了很多时间。不过,对于英语好的人,还是应该通过阅读说明文档来去了解它们。
为了说明如何使用它们,在这里构造一个使用场景:有N个Client,一个Server,M个Agent,Client使用ZMQ的请求-响应 模式和Server通信,Server收到Client的命令后,通过ZMQ的发布-订阅模式与各个Agent进行通信。下面的代码封装并使用了ZMQ和 MessagePack,为了简便,我把类的定义和实现都写在了头文件。
1.对ZMQ的简单封装:
1 #include"Msgpack.h" 2 #include<zmq.h> 3 #include<string> 4 #include<cassert> 5 #include<iostream> 6 7 namespace Tool 8 { 9 //网络工具类 10 class Network 11 { 12 public: 13 14 // 功能 :构造函数。 15 // 参数 :无。 16 // 返回 :无。 17 Network() : m_socket(NULL) { } 18 19 // 功能 :初始化socket。 20 // 参数 :zmqType表示ZMQ的模式,address表示socket绑定或连接地址。 21 // 返回 :true表示初始化成功,false表示失败。 22 bool Init(int zmqType,const std::string& address) 23 { 24 try 25 { 26 m_socket = zmq_socket(Context,zmqType); 27 return SetSocket(zmqType,address); 28 } 29 catch(...) 30 { 31 std::cout << "Network初始化失败。" << std::endl; 32 return false; 33 } 34 } 35 36 // 功能 :发送消息。 37 // 参数 :指向Msgpack的指针,isRelease如果为true表示发送消息后即刻释放资源。 38 // 返回 :true表示发送成功,false表示发送失败。 39 bool SendMessage(Msgpack *msgpack,bool isRelease = true) const 40 { 41 try 42 { 43 zmq_msg_t msg; 44 zmq_msg_init(&msg); 45 if(isRelease) 46 { 47 zmq_msg_init_data(&msg,msgpack->GetSbuf().data(),msgpack->GetSbuf().size(),Tool::Network::Release,msgpack); 48 } 49 else 50 { 51 zmq_msg_init_data(&msg,msgpack->GetSbuf().data(),msgpack->GetSbuf().size(),0,0); 52 } 53 zmq_msg_send(&msg,m_socket,0); 54 return true; 55 } 56 catch(...) 57 { 58 std::cout << "Network发送失败。" << std::endl; 59 return false; 60 } 61 } 62 63 // 功能 :接收消息。 64 // 参数 :无。 65 // 返回 :指向消息的指针。 66 zmq_msg_t* ReceiveMessage() const 67 { 68 zmq_msg_t *reply = NULL; 69 try 70 { 71 reply = new zmq_msg_t(); 72 zmq_msg_init(reply); 73 zmq_msg_recv(reply,m_socket,0); 74 return reply; 75 } 76 catch(...) 77 { 78 if( reply != NULL ) 79 { 80 delete reply; 81 } 82 return NULL; 83 } 84 } 85 86 // 功能 :关闭消息。 87 // 参数 :指向消息的指针。 88 // 返回 :无。 89 void CloseMsg(zmq_msg_t* msg) 90 { 91 try 92 { 93 zmq_msg_close(msg); 94 msg = NULL; 95 } 96 catch(...) 97 { 98 msg = NULL; 99 } 100 } 101 102 // 功能 :析构函数。 103 // 参数 :无。 104 // 返回 :无。 105 ~Network() 106 { 107 if( m_socket != NULL ) 108 { 109 zmq_close(m_socket); 110 m_socket = NULL; 111 } 112 } 113 114 private: 115 116 //通信socket 117 void *m_socket; 118 119 //网络环境 120 static void *Context; 121 122 private: 123 124 // 功能 :设置socket。 125 // 参数 :zmqType表示ZMQ的模式,address表示socket绑定或连接地址。 126 // 返回 :true表示设置成功,false表示设置失败。 127 bool SetSocket(int zmqType,const std::string& address) 128 { 129 int result = -1; 130 switch(zmqType) 131 { 132 case ZMQ_REP: 133 case ZMQ_PUB: 134 result = zmq_bind(m_socket,address.c_str()); 135 break; 136 case ZMQ_REQ: 137 result = zmq_connect(m_socket,address.c_str()); 138 break; 139 case ZMQ_SUB: 140 result = zmq_connect(m_socket,address.c_str()); 141 assert(result == 0); 142 result = zmq_setsockopt(m_socket,ZMQ_SUBSCRIBE,"",0); 143 break; 144 default: 145 return false; 146 } 147 assert( result == 0 ); 148 return true; 149 } 150 151 // 功能 :发送完消息后,释放消息资源。 152 // 参数 :function为函数地址,hint指向要释放资源的对象。 153 // 返回 :无。 154 static void Release(void *function, void *hint) 155 { 156 Msgpack *msgpack = (Msgpack*)hint; 157 if( msgpack != NULL ) 158 { 159 delete msgpack; 160 msgpack = NULL; 161 } 162 } 163 }; 164 165 //整个程序共用一个context 166 void *Tool::Network::Context = zmq_ctx_new(); 167 };
说明:
(1)由zmq_ctx_new创建出来的Context,整个应用程序共用一个就可以了,具体的通信是由zmq_socket创建的socket来完成的。上述代码中没有去释放Context指向的资源。
(2)在zmq_msg_init_data函数的参数中,需要传入一个释放资源的函数地址,在ZMQ发送完消息后就调用这个函数来释放资源。 如果没有传入这个参数,而且传入的信息是临时变量,那么接收方很有可能接收不到信息,甚至抛出异常。如果不传入这个参数,那么就要记得由自己去释放资源 了。
2.对MessagePack的简单封装:
1 #include"BaseMessage.h" 2 #include"ClientMessage.h" 3 #include"ServerMessage.h" 4 #include<zmq.h> 5 #include<msgpack.hpp> 6 7 namespace Tool 8 { 9 using namespace Message; 10 11 //压包/解包工具类 12 class Msgpack 13 { 14 public: 15 16 // 功能 :构造函数。 17 // 参数 :无。 18 // 返回 :无。 19 Msgpack(void) { } 20 21 // 功能 :析构函数。 22 // 参数 :无。 23 // 返回 :无。 24 ~Msgpack(void) { } 25 26 // 功能 :压包数据。 27 // 参数 :要压包的数据。 28 // 返回 :true表示压包成功。 29 template<typename T> 30 bool Pack(const T& t) 31 { 32 try 33 { 34 Release(); 35 msgpack::pack(m_sbuf,t); 36 return true; 37 } 38 catch(...) 39 { 40 std::cout << "Msgpack压包数据失败。" << std::endl; 41 return false; 42 } 43 } 44 45 // 功能 :解包数据。 46 // 参数 :zmq消息体。 47 // 返回 :返回指向基类消息的指针。 48 BaseMessage* Unpack(zmq_msg_t& msg) 49 { 50 try 51 { 52 int size = zmq_msg_size(&msg); 53 if( size > 0 ) 54 { 55 Release(); 56 m_sbuf.write((char*)zmq_msg_data(&msg),size); 57 size_t offset = 0; 58 msgpack::zone z; 59 msgpack::object obj; 60 msgpack::unpack(m_sbuf.data(),m_sbuf.size(),&offset,&z,&obj); 61 return GetMessage(obj); 62 } 63 } 64 catch(...) 65 { 66 //吃掉异常 67 } 68 return NULL; 69 } 70 71 // 功能 :获取压包/解包工具。 72 // 参数 :无。 73 // 返回 :压包/解包工具。 74 inline msgpack::sbuffer& GetSbuf() 75 { 76 return m_sbuf; 77 } 78 79 private: 80 81 //压包/解包工具 82 msgpack::sbuffer m_sbuf; 83 84 private: 85 86 // 功能 :释放上一次的数据资源。 87 // 参数 :无。 88 // 返回 :无。 89 void Release() 90 { 91 m_sbuf.clear(); 92 m_sbuf.release(); 93 } 94 95 // 功能 :获取消息。 96 // 参数 :用于转换的msgpack::object。 97 // 返回 :指向消息基类的指针。 98 BaseMessage* GetMessage(const msgpack::object& obj) 99 { 100 BaseMessage bmessage; 101 obj.convert(&bmessage); 102 switch(bmessage.Type) 103 { 104 case 1024: 105 return Convert<ClientMessage>(obj); 106 case 2048: 107 return Convert<ServerMessage>(obj); 108 default: 109 return NULL; 110 } 111 } 112 113 // 功能 :将压包后的数据转换为具体的类。 114 // 参数 :用于转换的msgpack::object。 115 // 返回 :指向T的指针。 116 template<typename T> 117 T* Convert(const msgpack::object& obj) 118 { 119 T *t = new T(); 120 obj.convert(t); 121 return t; 122 } 123 }; 124 };
说明:
压包时将zmq_msg_t消息体压包到msgpack::sbuffer,然后就可以关闭这个消息体了。要将解包后的数据转换成具体的某一个类,需要知道这个类是什么类,这里有三种方法:
(1)可以先发送一个消息告知接收者即将收到什么消息,然后接收者将消息解包后转换成对应的类。这种方式需要额外的一次通信,不建议使用。
(2)所有的消息都继承自一个基类,这个基类存储有消息类型的字段。解包后,先将数据转换为基类,然后根据类型再转换为具体的派生类。这种方式需要多转换一次,上面的代码也正是采用这种方式。
(3)压包时先压包一个消息类,然后再压包一个标识这个消息是什么类型的标 识类,即压包两次。解包时,先解包标识类,得知消息类的具体类型,然后再解包消息类,即解包两次,转换两次。与(2)相比,除了要做更多的压包、解包工作 外,这里还需要对解包的偏移量进行计算,否则容易出错。
3.使用到的消息类:
namespace Message { //消息基类 class BaseMessage { public: MSGPACK_DEFINE(Type); //消息类型 int Type; //默认构造函数 BaseMessage() { Type = 0; } }; //来自客户端的消息 class ClientMessage : public BaseMessage { public: MSGPACK_DEFINE(Type,Information); //信息 std::string Information; //默认构造函数 ClientMessage() { Type = 1024; } }; //来自服务端的消息 class ServerMessage : public BaseMessage { public: MSGPACK_DEFINE(Type,Information); //信息 std::vector<std::string> Information; //默认构造函数 ServerMessage() { Type = 2048; } }; };
说明:
(1)MSPACK_DEFINE标识了一个类的哪些成员可以进行压包/解包。派生类中的MSGPACK_DEFINE还需要写上基类的成员,否则无法使用对MessagePack封装说明的第二个方法。
(2)C++版本的MessagePack压/解包的数据成员,只能是一个类、结构或者联合体,不能使用指针(包括boost库的智能指针)、 数组,枚举值也不适用。因此,BaseMessage使用int值来标识派生类属于哪个类型。C#版本的MessagePack可以对枚举值进行压包。
4.Client的示例代码:
1 int _tmain(int argc, _TCHAR* argv[]) 2 { 3 Network network; 4 bool result = network.Init(ZMQ_REQ,"tcp://192.168.10.179:8888"); 5 if(result) 6 { 7 ClientMessage cmessage; 8 cmessage.Information = "I come form Client."; 9 10 Msgpack msgpack; 11 result = msgpack.Pack<ClientMessage>(cmessage); 12 if(result) 13 { 14 result = network.SendMessageW(&msgpack,false); 15 if(result) 16 { 17 zmq_msg_t *msg = network.ReceiveMessage(); 18 if( msg != NULL ) 19 { 20 BaseMessage *bmessage = msgpack.Unpack(*msg); 21 network.CloseMsg(msg); 22 if( bmessage != NULL && bmessage->Type == 2048 ) 23 { 24 ServerMessage *smessage = static_cast<ServerMessage*>(bmessage); 25 if( smessage != NULL && smessage->Information.size() > 0 ) 26 { 27 std::cout << smessage->Information[0] << std::endl; 28 } 29 delete smessage; 30 smessage = NULL; 31 bmessage = NULL; 32 } 33 } 34 } 35 } 36 } 37 38 system("pause"); 39 return 0; 40 }
5.Server的示例代码:
1 int _tmain(int argc, _TCHAR* argv[]) 2 { 3 Network responder; 4 bool result = responder.Init(ZMQ_REP,"tcp://192.168.10.179:8888"); 5 if(result) 6 { 7 Network publisher; 8 result = publisher.Init(ZMQ_PUB,"tcp://192.168.10.179:9999"); 9 if(result) 10 { 11 Msgpack msgpack; 12 while(true) 13 { 14 zmq_msg_t *msg = responder.ReceiveMessage(); 15 BaseMessage *bmessage = msgpack.Unpack(*msg); 16 responder.CloseMsg(msg); 17 18 ServerMessage smessage; 19 smessage.Information.push_back("I come from Server."); 20 msgpack.Pack<ServerMessage>(smessage); 21 result = responder.SendMessageW(&msgpack,false); 22 23 if( result ) 24 { 25 if( bmessage != NULL && bmessage->Type == 1024 ) 26 { 27 ClientMessage *cmessage = static_cast<ClientMessage*>(bmessage); 28 if( cmessage != NULL ) 29 { 30 std::cout << cmessage->Information << std::endl; 31 for( int counter = 0 ; counter < 100 ; counter++ ) 32 { 33 publisher.SendMessageW(&msgpack,false); 34 } 35 } 36 delete cmessage; 37 cmessage = NULL; 38 bmessage = NULL; 39 } 40 } 41 } 42 } 43 } 44 45 return 0; 46 }
6.Agent的示例代码:
int _tmain(int argc, _TCHAR* argv[]) { Network network; bool result = network.Init(ZMQ_SUB,"tcp://192.168.10.179:9999"); if(result) { zmq_msg_t *msg = network.ReceiveMessage(); if( msg != NULL ) { Msgpack msgpack; BaseMessage *bmessage = msgpack.Unpack(*msg); network.CloseMsg(msg); if( bmessage->Type == 2048 ) { ServerMessage *smessage = static_cast<ServerMessage*>(bmessage); if( smessage->Information.size() > 0 ) { std::cout << smessage->Information[0] << std::endl; } delete smessage; smessage = NULL; bmessage = NULL; } } } system("pause"); return 0; }
7.启动这三个程序,Client将要发送的消息压包后发给Server,Server接收到消息后反馈一个信息给Client,然后循环发布消息给Agent,Agent不需要回复Server。最后着重说明两点:
(1)ZMQ创建的socket发送数据和接收数据要处在同一条线程。Server接收到Client的数据后,不能通过开一条线程来给Client反馈信息,必须要在接收数据的线程中反馈信息。
(2)ZMQ并不要求发送者和接收者有一定的启动顺序,但在Server中如果只发布一次消息,那么Agent很有可能收不到信息。不管是 Agent先启动,还是Server先启动,Agent都有可能收不到信息。在Server的代码中,通过循环发布一百次,来让Agent收到信息。至于 实际应用中,可以结合请求-响应模式来保证订阅消息者都收到了发布者的消息。
参考资料:
ZMQ:http://zguide.zeromq.org/page:all
MessagePack:http://wiki.msgpack.org/pages/viewpage.action?pageId=1081387#QuickStartforC%2B%2B-ImplementationStatus