发布-订阅(ZeroMQ) C++实现

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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

东风吹柳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值