1、目的
自从发了《发布-订阅(Publish-Subscribe)C++实现》博文,收到不少反馈:主要的问题就是无法跨主机使用。
本次实现主要解决:
- 简化ZeroMQ的开发过程;
- 尽可能简化发布订阅的API调用;
- 订阅者消息处理采用守护线程模式;
- 支撑跨主机的发布订阅(By ZeroMQ)
下载地址:【免费】PublishSubscribe-ZMQ发布订阅C++实现资源-CSDN文库
2、ZeroMQ库选择
一般,在可以选择的情况下,我比较偏爱C接口的库,主要原因:一致性好,至少比C++接口的库兼容性强。
鉴于这个理由,我就选择了libzmq,库版本4.3.5。
推荐优先使用MD模式的库:libzmq-v142-mt-4_3_5.lib。
3、实现思路
TOPIC是满足:
1)可compare的,组合数据类型至少重载 == 操作;
2)可直接网络传输的,常见的比如整型、结构体。
主题对应的数据一般多见结构体,也可以是字符串、二进制等任何适用于网络传输的数据类型。
4、主要代码
#ifndef __PUBLISHER_HPP__
#define __PUBLISHER_HPP__
#include "TOPIC_DEFS.h"
#include "zmq.h"
#include <string>
template<typename _TOPIC_>
class Publisher
{
public:
//addr :"tcp://*:port"
Publisher(const char * addr)
{
// 初始化 ZeroMQ 上下文
mContext = zmq_ctx_new();
// 创建 PUB 套接字
mPublisher = zmq_socket(mContext, ZMQ_PUB);
int linger = 0;
zmq_setsockopt(mPublisher, ZMQ_LINGER, &linger, sizeof(int64_t));
// 绑定地址
zmq_bind(mPublisher, addr);
}
virtual ~Publisher()
{
// 关闭套接字和 ZeroMQ 上下文
if (mPublisher) zmq_close(mPublisher);
if(mContext) zmq_ctx_destroy(mContext);
}
void Publish(_TOPIC_ topic, void* msg, int msg_size)
{
zmq_send(mPublisher, &topic, sizeof(_TOPIC_), ZMQ_SNDMORE);
zmq_send(mPublisher, msg, msg_size, 0);
}
private:
void* mContext; // ZeroMQ 上下文
void* mPublisher; // PUB 套接字
};
#endif // !__PUBLISHER_HPP__
#ifndef __SUBCORE_HPP__
#define __SUBCORE_HPP__
#include "zmq.h"
#include <thread>
#include <mutex>
#include <map>
#include <list>
#define _MAX_SUBSCRIBE_ 1024
template<typename _TOPIC_>
class Subcore
{
public:
Subcore()
{
// 初始化 ZeroMQ 上下文
mContext = zmq_ctx_new();
mSubscribeSize = 0;
memset(mSubscribe, 0, sizeof(zmq_pollitem_t) * _MAX_SUBSCRIBE_);
mStop = false;
mSubTopic.clear();
mSubPollitem.clear();
mRecvThreadPtr.reset(new std::thread(&Subcore::EventProcess, this));
}
virtual ~Subcore()
{
// 关闭监听事件
for (int i = 0; i < mSubscribeSize; ++i)
{
if (mSubscribe[i].socket != nullptr)
mSubscribe[i].events = 0;
}
// 等待事件处理线程退出
mStop = true;
if (mRecvThreadPtr->joinable()) {
mRecvThreadPtr->join();
}
// 关闭套接字
for (int i = 0; i < mSubscribeSize; ++i)
{
if (mSubscribe[i].socket != nullptr) {
zmq_close(mSubscribe[i].socket);
mSubscribe[i].socket = nullptr;
}
}
mSubscribeSize = 0;
// 销毁 ZeroMQ 上下文
if (mContext) {
zmq_ctx_destroy(mContext);
mContext = nullptr;
}
}
//addr :"tcp://ip:port"
bool Subscribe(_TOPIC_ topic, const char* addr)
{
auto it = mSubTopic.find(addr);
if (it != mSubTopic.end())
{
for (auto itt = it->second.begin(); itt != it->second.end(); ++itt)
{
if (*itt == topic)
{
return false;
}
}
// 订阅主题
auto sock = mSubPollitem.find(addr);
if (sock != mSubPollitem.end())
{
zmq_setsockopt(sock->second.socket, ZMQ_SUBSCRIBE, &topic, sizeof(_TOPIC_));
mSubscribeMutex.lock();
mSubTopic[addr].push_back(topic);
mSubscribeMutex.unlock();
return true;
}
}
// 创建 SUB 套接字
void* subscriber_temp = zmq_socket(mContext, ZMQ_SUB);
if(!subscriber_temp) return false;
//连接套接字
if(zmq_connect(subscriber_temp, addr)==-1)
return false;
// 订阅主题
zmq_setsockopt(subscriber_temp, ZMQ_SUBSCRIBE, &topic, sizeof(_TOPIC_));
int linger = 0;
zmq_setsockopt(subscriber_temp, ZMQ_LINGER, &linger, sizeof(int64_t));
//准备IO复用
zmq_pollitem_t tmp{ subscriber_temp, 0, ZMQ_POLLIN, 0 };
mSubscribeMutex.lock();
mSubscribe[mSubscribeSize] = tmp;
mSubscribeSize++;
//更新记录
mSubTopic[addr].push_back(topic);
mSubPollitem[addr] = tmp;
mSubscribeMutex.unlock();
return true;
}
void UnSubscribe(_TOPIC_ topic)
{
for (auto it = mSubTopic.begin(); it != mSubTopic.end(); it++)
{
for (auto itt = it->second.begin(); itt != it->second.end(); ++itt)
{
if (*itt == topic)
{
auto sock = mSubPollitem.find(it->first);
if (sock != mSubPollitem.end())
{
zmq_setsockopt(sock->second.socket, ZMQ_UNSUBSCRIBE, &topic, sizeof(_TOPIC_));
}
}
}
}
}
virtual void EnventHandler(_TOPIC_, void*, int) = 0;
private:
void EventProcess()
{
while (!mStop)
{
if (mSubscribeSize > 0)
{
int size = zmq_poll(mSubscribe, mSubscribeSize, 10);
if (size == -1)
continue;
if (size > 0)
{
for (size_t i = 0; i < _MAX_SUBSCRIBE_; i++)
{
if (mSubscribe[i].revents & ZMQ_POLLIN)
{
zmq_msg_t msg;
zmq_msg_init(&msg);
zmq_msg_recv(&msg, mSubscribe[i].socket, 0);
// 第一部分消息(主题,整数类型)
_TOPIC_ topic;
memcpy(&topic, zmq_msg_data(&msg), sizeof(_TOPIC_));
// 第二部分消息(数据)
int more;
size_t more_size = sizeof(more);
zmq_getsockopt(mSubscribe[i].socket, ZMQ_RCVMORE, &more, &more_size);
if (more)
{
zmq_msg_init(&msg);
zmq_msg_recv(&msg, mSubscribe[i].socket, 0);
size_t datasize = zmq_msg_size(&msg);
char* data = static_cast<char*>(zmq_msg_data(&msg));
EnventHandler(topic, data, datasize);
}
zmq_msg_close(&msg);
}
}
}
}
}
}
private:
void* mContext; // ZeroMQ 上下文
int mSubscribeSize;
zmq_pollitem_t mSubscribe[_MAX_SUBSCRIBE_]; // IO多路复用
std::map<std::string, std::list<_TOPIC_>> mSubTopic; // 记录订阅的主题 key is addr
std::map<std::string, zmq_pollitem_t> mSubPollitem; // 记录订阅的远程主机 key is addr
std::mutex mSubscribeMutex; // 订阅或者取消订阅时保护写数据
std::unique_ptr<std::thread> mRecvThreadPtr;
bool mStop;
};
#endif // !__SUBCORE_HPP__
测试代码
#define _CRT_SECURE_NO_WARNINGS
#include "Publisher.hpp"
#include "Subscriber.hpp"
#define ADDRESS_S5555 "tcp://*:5555" //发布者地址1
#define ADDRESS_S5557 "tcp://*:5557" //发布者地址2
#define ADDRESS_C5555 "tcp://localhost:5555"
#define ADDRESS_C5557 "tcp://localhost:5557"
void TESTTHREAD()
{
Hello tpc1{ "sma",20 };
Publisher<TOPIC_TYPE> pub1(ADDRESS_S5557);
while (true)
{
zmq_sleep(1);
pub1.Publish(TOPIC_WORLD, &tpc1, sizeof tpc1);
}
}
int main()
{
//测试一个Subscriber 订阅来自不同Publisher的主题------------------------------
{
Hello tpc{ "wxq",18 };
Subscriber<TOPIC_TYPE> sub;
Publisher<TOPIC_TYPE> pub(ADDRESS_S5555);
new std::thread(TESTTHREAD);
sub.Subscribe(TOPIC_HELLO, ADDRESS_C5555);
//sub.Subscribe(TOPIC_WORLD, ADDRESS_C5555);
sub.Subscribe(TOPIC_WORLD, ADDRESS_C5557);
//sub.UnSubscribe(TOPIC_WORLD);
while (1)
{
zmq_sleep(1);
pub.Publish(TOPIC_HELLO,&tpc,sizeof tpc);
}
}
//测试一个Subscriber 订阅来自同一个Publisher的不同主题------------------------------
//{
// Hello tpc{ "wxq",18 };
// Hello tpc1{ "sma",20 };
// Publisher<TOPIC_TYPE> pub(ADDRESS_S5555);
// Publisher<TOPIC_TYPE> pub1(ADDRESS_S5555);
// Subscriber<TOPIC_TYPE> sub;
// sub.Subscribe(TOPIC_HELLO, ADDRESS_C5555);
// sub.Subscribe(TOPIC_WORLD, ADDRESS_C5555);
// while (1)
// {
// zmq_sleep(1);
// pub.Publish(TOPIC_HELLO, &tpc, sizeof tpc);
// pub.Publish(TOPIC_WORLD, &tpc1, sizeof tpc1);
// }
//}
return 0;
}