C++实战专题-消息中间件篇

C++实战专题-消息中间件篇

文章目录

1.C++对接ActiveMQ消息队列

1.1 ActiveMQ消息队列简介

Apache ActiveMQ是最流行的开源、多协议、基于Java的消息代理。它支持行业标准协议,因此用户可以从多种语言和平台的客户端选择中获益。从用JavaScript、C、C++、Python、.Net等编写的客户端连接。使用无处不在的AMQP协议集成您的多平台应用程序。通过网络套接字使用STOMP在web应用程序之间交换消息。使用MQTT管理您的物联网设备。支持您现有的JMS基础架构和其他基础架构。ActiveMQ提供了支持任何消息传递用例的强大功能和灵活性。

1.2 ActiveMQ特性说明

  • 多种语言和协议编写客户端。语言: Java,C,C++,C#,Ruby,Perl,Python,PHP。应用协议: OpenWire,Stomp REST,WS Notification,XMPP,AMQP

  • 完全支持JMS1.1和J2EE 1.4规范 (持久化,XA消息,事务)

  • 对Spring的支持,ActiveMQ可以很容易内嵌到使用Spring的系统里面去,而且也支持Spring2.0的特性

  • 通过了常见J2EE服务器(如 Geronimo,JBoss 4,GlassFish,WebLogic)的测试,其中通过JCA 1.5 resource adaptors的配置,可以让ActiveMQ可以自动的部署到任何兼容J2EE 1.4 商业服务器上

  • 支持多种传送协议:in-VM,TCP,SSL,NIO,UDP,JGroups,JXTA

  • 支持通过JDBC和journal提供高速的消息持久化

  • 从设计上保证了高性能的集群,客户端-服务器,点对点

  • 支持Ajax

  • 支持与Axis的整合

  • 可以很容易得调用内嵌JMS provider,进行测试

1.3 ActiveMQ应用场景

对于ActiveMQ适用何种应用场景,我们必须首先搞清楚它的优缺点,才能很清楚的知道我们在什么场景下适用ActiveMQ。

  • 优点

ActiveMQ采用消息推送方式,所以最适合的场景是默认消息都可在短时间内被消费。数据量越大,查找和消费消息就越慢,消息积压程度与消息速度成反比。

  • 缺点

    • 吞吐量低。由于ActiveMQ需要建立索引,导致吞吐量下降。这是无法克服的缺点,只要使用完全符合JMS规范的消息中间件,就要接受这个级别的TPS。

    • 无分片功能。这是一个功能缺失,JMS并没有规定消息中间件的集群、分片机制。而由于ActiveMQ是为企业级开发设计的消息中间件,初衷并不是为了处理海量消息和高并发请求。如果一台服务器不能承受更多消息,则需要横向拆分。ActiveMQ官方不提供分片机制,需要自己实现。

    • 官方社区现在对ActiveMQ 5.x维护越来越少,较少在大规模吞吐的场景中使用,对于非java生态支持不够友好

1.4 ActiveMQ常见概念

概念
队列(Queue)队列存储,常用与点对点消息模型 ,默认只能由唯一的一个消费者处理,一旦处理消息删除。
主题(Topic)主题存储,用于订阅/发布消息模型,主题中的消息,会发送给所有的消费者同时处理,只有在消息可以重复处 理的业务场景中可使用
连接工厂(ConnectionFactory)连接工厂,客户用来创建连接的对象,例如ActiveMQ提供的ActiveMQConnectionFactory
连接(Connection)JMS Connection封装了客户与JMS提供者之间的一个虚拟的连接。
目的地(Destination)消息的目的地,目的地是客户用来指定它生产的消息的目标和它消费的消息的来源的对象
点对点消息传递域(PTP)每个消息只能有一个消费者。 消息的生产者和消费者之间没有时间上的相关性。无论消费者在生产者发送消息的时候是否处于运行状态,它都可以提取消息。
发布/订阅消息传递域(Pub/Sub)每个消息可以有多个消费者。 生产者和消费者之间有时间上的相关性。 订阅一个主题的消费者只能消费自它订阅之后发布的消息。JMS规范允许客户创建持久订阅,这在一定程度上放松了时间上的相关性要求 。持久订阅允许消费者消费它在未处于激活状态时发送的消息。 在点对点消息传递域中,目的地被成为队列(queue);在发布/订阅消息传递域中,目的地被成为主题(topic)。

1.5 ActiveMQ可视化工具

在这里插入图片描述

ActiveMQ默认提供后台管理页面,访问地址为http://服务器IP地址:8161/admin/默认用户admin,默认密码admin,用户可通过配置文件修改默认用户名密码,对于有特殊需求的场景,用户可以自行实现后台管理页面。后台管理页面可以查看所有队列、主题、消息订阅者和网络连接等详细信息,并可以往指定队列或主题中发送模拟数据方便开发测试。

1.6 编译ActiveMQ动态库

ActiveMQ-CPP有多种编译方式,其中手动编译的方式比较麻烦,需要自行编译apr, apr-util, apr-iconv和cppunit多个第三方依赖库,比较花时间。通过vcpkg(微软 C++ 团队开发的适用于 C 和 C++ 库的跨平台开源软件包管理器)编译ActiveMQ只需要一条【.\vcpkg install activemq-cpp:x86-windows --debug】命令即可搞定,它会自动编译依赖库,最后使用导出命令【.\vcpkg export activemq-cpp:x86-windows --zip】将编译好的成果物导出即可。
在这里插入图片描述

1.7 ActiveMQ使用示例

1.7.1 ActiveMQ的连接方式

连接ActiveMQ服务端需要确定网络连接协议、连接地址、消息队列类型、用户名、密码和消息确认方式。ActiveMQ支持的应用层协议有openwire、amqp、stomp、mqtt和ws协议,默认支持的协议为openwire协议,也是我们实际开发中用的最多的。

在这里插入图片描述

1.7.2 ActiveMQ编程指南

在这里插入图片描述

1.7.2 生产者发送数据示例

  • 生产者头文件AMQProducer.h
#ifndef _AMQ_PRODUCER_H_
#define _AMQ_PRODUCER_H_

#include <decaf/lang/Thread.h>
#include <decaf/lang/Runnable.h>
#include <decaf/util/concurrent/CountDownLatch.h>
#include <decaf/lang/Long.h>
#include <decaf/util/Date.h>
#include <decaf/util/concurrent/Mutex.h>

#include <activemq/core/ActiveMQConnectionFactory.h>
#include <activemq/util/Config.h>
#include <activemq/library/ActiveMQCPP.h>

#include <cms/Connection.h>
#include <cms/Session.h>
#include <cms/TextMessage.h>
#include <cms/BytesMessage.h>
#include <cms/MapMessage.h>
#include <cms/ExceptionListener.h>
#include <cms/MessageListener.h>

#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <memory>

using namespace activemq;
using namespace activemq::core;
using namespace decaf;
using namespace decaf::lang;
using namespace decaf::util;
using namespace decaf::util::concurrent;
using namespace cms;
using namespace std;

class AMQProducer : public Runnable
{
public:
	AMQProducer(const std::string& brokerURI, unsigned int numMessages, const std::string& destURI, bool useTopic = false, 
		bool clientAck = false, const std::string& username = string(), const std::string& password = string());

	virtual ~AMQProducer();

	// 销毁生产者
	void close();

	// 创建生产者
	void createProducer();

	// 发送数据线程体
	virtual void run();

	// 发送测试数据
	void sendTestData();

private:
	AMQProducer(const AMQProducer&);

	void cleanup();

private:

	Connection*			m_pConnection;	// 与cms连接
	Session*			m_pSession;		// 从cms连接创建的会话
	Destination*		m_pDestination; // 通过会话创建的topic或者queue
	MessageProducer*	m_pProducer;	// 通过topic或者queue创建的消费者
	bool				m_bUseTopic;	// 是否使用topic或者queue的标志
	bool				m_bClientAck;	// 消息接收应答标志(客户端自己确认消息接收还是自动确认消息接收)
	unsigned int		m_nNumMessages;	// 发送测试消息数量
	std::string			m_strBrokerURI;	// 连接cms的地址
	std::string			m_strDestURI;	// 连接的topic或者queue的名称
	std::string			m_strUserName;	// 连接用户名
	std::string			m_strPassword;	// 连接密码
};

#endif
  • 生产者实现文件AMQProducer.cpp
#include "AMQProducer.h"
#include <thread>

AMQProducer::AMQProducer(const std::string& brokerURI, unsigned int numMessages, const std::string& destURI, 
	bool useTopic, bool clientAck, const std::string& username, const std::string& password):
	m_pConnection(nullptr),
	m_pSession(nullptr),
	m_pDestination(nullptr),
	m_pProducer(nullptr),
	m_bUseTopic(useTopic),
	m_bClientAck(clientAck),
	m_nNumMessages(numMessages),
	m_strBrokerURI(brokerURI),
	m_strDestURI(destURI),
	m_strUserName(username),
	m_strPassword(password)
{

}

AMQProducer::~AMQProducer()
{
	if (m_pConnection != nullptr)
	{
		m_pConnection->stop();
	}

	cleanup();
}

void AMQProducer::close()
{
	cleanup();
}

void AMQProducer::createProducer()
{
	try
	{
		// 1.创建一个ActiveMQ连接工厂
		auto_ptr<ActiveMQConnectionFactory> connectionFactory(new ActiveMQConnectionFactory(m_strBrokerURI));

		// 2.从ActiveMQ连接工厂创建一个连接
		try
		{
			if (!m_strUserName.empty() && !m_strUserName.empty())
			{
				m_pConnection = connectionFactory->createConnection(m_strUserName, m_strPassword);
			}
			else
			{
				m_pConnection = connectionFactory->createConnection();
			}
			
			m_pConnection->start();
		}
		catch (CMSException& e)
		{
			e.printStackTrace();
			throw e;
		}

		// 3.从连接创建一个会话
		if (m_bClientAck)
		{
			m_pSession = m_pConnection->createSession(Session::CLIENT_ACKNOWLEDGE);
		}
		else
		{
			m_pSession = m_pConnection->createSession(Session::AUTO_ACKNOWLEDGE);
		}

		// 4.从连接创建一个topic或者queue
		if (m_bUseTopic)
		{
			m_pDestination = m_pSession->createTopic(m_strDestURI);
		}
		else
		{
			m_pDestination = m_pSession->createQueue(m_strDestURI);
		}

		// 5.通过topic或者queue用会话创建生产者
		m_pProducer = m_pSession->createProducer(m_pDestination);
		m_pProducer->setDeliveryMode(DeliveryMode::PERSISTENT);
	}
	catch (CMSException& e)
	{
		e.printStackTrace();
	}
}

void AMQProducer::run()
{
	sendTestData();
}

void AMQProducer::sendTestData()
{
	// 创建线程id字符串
	string threadIdStr = std::to_string(Thread::currentThread()->getId());

	// 创建一个消息
	string text = string("Hello ActiveMQ!");
	while (1)
	{
		for (unsigned int index = 0; index < m_nNumMessages; ++index)
		{
			if (m_pSession != nullptr)
			{
				TextMessage* message = m_pSession->createTextMessage(text);
				message->setIntProperty("Integer", index);

				// 生产者发送数据
				printf("Sent message #%d from thread %s\n", index + 1, threadIdStr.c_str());
				if (m_pProducer != nullptr)
				{
					m_pProducer->send(message);
				}
				
				delete message;
				message = nullptr;
			}

			std::this_thread::sleep_for(std::chrono::milliseconds(20000));
		}

		std::this_thread::sleep_for(std::chrono::seconds(2));
	}
}

void AMQProducer::cleanup()
{
	try 
	{
		if (m_pConnection != nullptr) 
		{
			m_pConnection->close();
		}
	}
	catch (CMSException& e) 
	{
		e.printStackTrace();
	}

	if (m_pDestination != nullptr)
	{
		delete m_pDestination;
		m_pDestination = nullptr;
	}
	
	if (m_pProducer != nullptr)
	{
		delete m_pProducer;
		m_pProducer = nullptr;
	}
	
	if (m_pSession != nullptr)
	{
		delete m_pSession;
		m_pSession = nullptr;
	}
	
	if (m_pConnection != nullptr)
	{
		delete m_pConnection;
		m_pConnection = nullptr;
	}
}
  • main.cpp
#include "AMQProducer.h"

int main(int argc AMQCPP_UNUSED, char* argv[] AMQCPP_UNUSED) {

	std::cout << "=====================================================\n";
	std::cout << "Starting the example:" << std::endl;
	std::cout << "-----------------------------------------------------\n";

	// 初始化ActiveMQ库
	activemq::library::ActiveMQCPP::initializeLibrary();

	// 服务端连接地址
	std::string brokerURI = "failover://(tcp://10.19.222.122:7018)";

	// 测试发送消息的数量
	unsigned int numMessages = 2000;

	// 连接的目标topic或者queue
	std::string destURI = "sgms.ghbms.topic.message";

	// 使用topic还是queue标志
	bool useTopics = true;

	// 消息确认方式(true:客户端接收到消息以后需要发送确认标志, false: 消息接收自动确认)
	bool clientAck = false;

	// 连接用户名
	std::string username = "admin";

	// 连接密码
	std::string password = "7mX26xah";

	// 创建生产者对象
	AMQProducer producer(brokerURI, numMessages, destURI, useTopics, clientAck, username, password);
	producer.createProducer();

	// 运行数据发送线程
	producer.run();

	// 等待程序退出
	std::cout << "Press 'q' to quit" << std::endl;
	while (std::cin.get() != 'q') {}

	// 关闭所有资源
	producer.close();

	// 反初始化ActiveMQ库
	activemq::library::ActiveMQCPP::shutdownLibrary();

	std::cout << "-----------------------------------------------------\n";
	std::cout << "Finished with the example." << std::endl;
	std::cout << "=====================================================\n";

	return 0;
}

1.7.3 消费者消费数据示例

  • AMQConsumer.h
/**
* @file     ActiveMQConsumer.h
* @brief    该类主要负责创建ActiveMQ消费者
* @details
* @mainpage 工程概览
* @author   OSHO
* @email    xiaoxiaolong@hikvision.com.cn
* @version  1.0.0
* @date     2022-02-09
* @license  HangZhou Hikvision System Technology Co., Ltd. All Right Reserved.
*/
#ifndef _AMQ_CONSUMER_H_
#define _AMQ_CONSUMER_H_

#include <decaf/lang/Thread.h>
#include <decaf/lang/Runnable.h>
#include <decaf/util/concurrent/CountDownLatch.h>
#include <activemq/core/ActiveMQConnectionFactory.h>
#include <activemq/core/ActiveMQConnection.h>
#include <activemq/transport/DefaultTransportListener.h>
#include <activemq/library/ActiveMQCPP.h>
#include <decaf/lang/Integer.h>
#include <activemq/util/Config.h>
#include <decaf/util/Date.h>
#include <cms/Connection.h>
#include <cms/Session.h>
#include <cms/TextMessage.h>
#include <cms/BytesMessage.h>
#include <cms/MapMessage.h>
#include <cms/ExceptionListener.h>
#include <cms/MessageListener.h>
#include <stdlib.h>
#include <stdio.h>
#include <iostream>

using namespace activemq;
using namespace activemq::core;
using namespace activemq::transport;
using namespace decaf::lang;
using namespace decaf::util;
using namespace decaf::util::concurrent;
using namespace cms;
using namespace std;

class AMQConsumer : public ExceptionListener, public MessageListener, public DefaultTransportListener
{
public:
	AMQConsumer(const std::string& brokerURI, const std::string& destURI, bool useTopic = false, 
		bool clientAck = false, const std::string& username = string(), const std::string& password = string());

	virtual ~AMQConsumer();

	// 清理系统资源
	void close();

	// 创建消费者
	void runConsumer();

	// 注册消息监听者后的回调函数
	virtual void onMessage(const Message* message);

	// 注册异常监听者后的回调函数
	virtual void onException(const CMSException& ex AMQCPP_UNUSED);

	// 数据传输中断回调函数
	virtual void transportInterrupted();

	// 数据传输恢复回调函数
	virtual void transportResumed();

private:
	AMQConsumer(const AMQConsumer&);

	// 释放程序申请的资源
	void cleanup();

private:
	Connection*				connection;  // 与cms连接
	Session*				session;	 // 从cms连接创建的会话
	Destination*			destination; // 通过会话创建的topic或者queue
	MessageConsumer*		consumer;	 // 通过topic或者queue创建的消费者
	bool					useTopic;	 // 是否使用topic或者queue的标志
	std::string				brokerURI;   // 连接cms的地址
	std::string				destURI;	 // 连接的topic或者queue的名称
	bool					clientAck;   // 消息接收应答标志(客户端自己确认消息接收还是自动确认消息接收)
	std::string				m_strUserName; // 连接用户名
	std::string				m_strPassword; // 连接密码
};

#endif // _CONSUMER_ROLE_H_
  • AMQConsumer.cpp
#include "AMQConsumer.h"

AMQConsumer::AMQConsumer(const std::string& brokerURI, const std::string& destURI, bool useTopic, 
	bool clientAck, const std::string& username, const std::string& password) :
	connection(nullptr),
	session(nullptr),
	destination(nullptr),
	consumer(nullptr),
	useTopic(useTopic),
	brokerURI(brokerURI),
	destURI(destURI),
	clientAck(clientAck),
	m_strUserName(username),
	m_strPassword(password)
{
}

AMQConsumer::~AMQConsumer()
{
	this->cleanup();
}

void AMQConsumer::close()
{
	this->cleanup();
}

void AMQConsumer::runConsumer()
{
	try
	{
		// 1.创建一个连接工厂
		ActiveMQConnectionFactory* connectionFactory = new ActiveMQConnectionFactory(brokerURI);

		// 2.创建一个连接
		if (!m_strUserName.empty() && !m_strPassword.empty())
		{
			connection = connectionFactory->createConnection(m_strUserName, m_strPassword);
		}
		else
		{
			connection = connectionFactory->createConnection();
		}
		
		delete connectionFactory;
		connectionFactory = nullptr;

		ActiveMQConnection* amqConnection = dynamic_cast<ActiveMQConnection*>(connection);
		if (amqConnection != nullptr)
		{
			amqConnection->addTransportListener(this);
		}

		connection->start();

		connection->setExceptionListener(this);

		// 3.创建一个会话
		if (clientAck)
		{
			session = connection->createSession(Session::CLIENT_ACKNOWLEDGE);
		}
		else
		{
			session = connection->createSession(Session::AUTO_ACKNOWLEDGE);
		}

		// 4.创建一个Topic或者Queue
		if (useTopic)
		{
			destination = session->createTopic(destURI);
		}
		else
		{
			destination = session->createQueue(destURI);
		}

		// 5.从会话到Topic或Queue创建消费者
		consumer = session->createConsumer(destination);
		consumer->setMessageListener(this);
	}
	catch (CMSException& e)
	{
		e.printStackTrace();
	}
}

void AMQConsumer::onMessage(const Message* message)
{
	static int count = 0;

	try
	{
		count++;
		const TextMessage* textMessage = dynamic_cast<const TextMessage*>(message);
		string text = "";

		if (textMessage != nullptr)
		{
			text = textMessage->getText();
		}
		else
		{
			text = "Not a textmessage!";
		}

		if (clientAck)
		{
			message->acknowledge();
		}

		printf("Message #%d Received: %s\n", count, text.c_str());
	}
	catch (CMSException& e)
	{
		e.printStackTrace();
	}
}

// If something bad happens you see it here as this class is also been
// registered as an ExceptionListener with the connection.
void AMQConsumer::onException(const CMSException& ex AMQCPP_UNUSED)
{
	printf("CMS exception occurred. Shutting down client.\n");
	exit(1);
}

void AMQConsumer::transportInterrupted() {
	std::cout << "The connection's transport has been interrupted." << std::endl;
}

void AMQConsumer::transportResumed()
{
	std::cout << "The connection's transport has been restored." << std::endl;
}

void AMQConsumer::cleanup()
{
	try
	{
		if (connection != nullptr)
		{
			connection->close();
		}
	}
	catch (CMSException& e)
	{
		e.printStackTrace();
	}

	if (destination != nullptr)
	{
		delete destination;
		destination = nullptr;
	}

	if (consumer != nullptr)
	{
		delete consumer;
		consumer = nullptr;
	}

	if (session != nullptr)
	{
		delete session;
		session = nullptr;
	}

	if (connection != nullptr)
	{
		delete connection;
		connection = nullptr;
	}
}
  • main.cpp
#include "AMQConsumer.h"

int main(int argc AMQCPP_UNUSED, char* argv[] AMQCPP_UNUSED) {

	std::cout << "=====================================================\n";
	std::cout << "Starting the example:" << std::endl;
	std::cout << "-----------------------------------------------------\n";

	// 初始化ActiveMQ库
	activemq::library::ActiveMQCPP::initializeLibrary();

	// 服务端连接地址
	std::string brokerURI = "failover:(tcp://10.19.222.122:7018)";

	// 连接的目标topic或者queue
	std::string destURI = "sgms.ghbms.topic.message"; //?consumer.prefetchSize=1";

	// 使用topic还是queue标志
	bool useTopics = true;

	// 消息确认方式(true:客户端接收到消息以后需要发送确认标志, false: 消息接收自动确认)
	bool clientAck = false;

	// 连接用用户名
	std::string username = "admin";

	// 连接用密码
	std::string password = "7mX26xah";

	// 创建消费者
	AMQConsumer consumer(brokerURI, destURI, useTopics, clientAck, username, password);

	// 消费者启动消息监听
	consumer.runConsumer();

	// 等待程序退出
	std::cout << "Press 'q' to quit" << std::endl;
	while (std::cin.get() != 'q') {}

	// 关闭所有资源
	consumer.close();

	// 反初始化ActiveMQ库
	activemq::library::ActiveMQCPP::shutdownLibrary();

	std::cout << "-----------------------------------------------------\n";
	std::cout << "Finished with the example." << std::endl;
	std::cout << "=====================================================\n";
	
	return 0;
}

1.8 项目应用

ActiveMQ社区对于非java生态的支持比较少,ActiveMQ-CPP库停留在3.9.5版本好几年都没更新过版本,这对于运用在商用软件产品上是极为不利的,软件bug和软件网络安全漏洞都不能及时得到修复,这对于产品本身来讲是有很多风险的,所以在C++端不是很推荐使用。但是对于个别平台服务需要对接硬件的场景,必须使用C++来实现的时候,业务需要无法避免,那也只能将就用了。

2.C++对接Kafka消息队列

2.1 Kafka消息队列简介

由Linkedin公司开发,是一个分布式、支持分区的(partition)、多副本的(replica),基于zookeeper协调的分布式消息系统,最大的特性就是实时的处理大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延迟的实时系统、storm/Spark流式处理引擎,web/nginx日志、访问日志和消息服务等等。使用scala语言编写,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目,支持多种语言客户端: Python、Ruby、Java、C/C++、PHP。

2.2 Kafka特性说明

  • 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。
  • 可扩展性:kafka集群支持热扩展
  • 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
  • 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败)
  • 高并发:支持数千个客户端同时读写

2.3 Kafka应用场景

要想搞清楚什么情况下选用kafka,我们必须先来弄清楚kafka的优缺点,才知道他的应用场景。

优点

  • 性能卓越,单机写入TPS约在百万条/秒,最大的优点,就是吞吐量高。
  • 时效性:ms级
  • 可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
  • 消费者采用Pull方式获取消息, 消息有序, 通过控制能够保证所有消息被消费且仅被消费一次;
  • 有优秀的第三方Kafka Web管理界面Kafka-Manager;
  • 在日志领域比较成熟,被多家公司和多个开源项目使用;
  • 功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用

缺点:

  • Kafka单机超过64个队列/分区,Load会发生明显的飙高现象,队列越多,load越高,发送消息响应时间变长

  • 使用短轮询方式,实时性取决于轮询间隔时间;

  • 消费失败不支持重试;

  • 支持消息顺序,但是一台代理宕机后,就会产生消息乱序;

  • 社区更新较慢;

2.4 Kafka常用概念

在这里插入图片描述

概念说明
BrokerKafka服务器,用来存储消息,Kafka集群中的每一个服务器都是一个Broker,消费者将从broker拉取订阅的消息。Producer向Kafka发送消息,生产者会根据topic分发消息,生产者也负责把消息关联到Topic上的哪一个分区,最简单的方式从分区列表中轮流选择,也可以根据某种算法依照权重选择分区,算法可由开发者定义。
Topic消息的主题、队列,每一个消息都有它的topic,Kafka通过topic对消息进行归类。Kafka中可以将Topic从物理上划分成一个或多个分区(Partition),每个分区在物理上对应一个文件夹,以”topicName_partitionIndex”的命名方式命名,该dir包含了这个分区的所有消息(.log)和索引文件(.index),这使得Kafka的吞吐率可以水平扩展。
Producer消息生产者,负责发布消息到kafka broker
ConsumerConsermer实例可以是独立的进程,负责订阅和消费消息。消费者用Consumer Group来标识自己,同一个消费组可以并发地消费多个分区的消息,同一个Partition也可以由多个Consumer Group并发消费,但是在Consumer Group中一个Partition只能由一个Consumer消费。
Consumer Group消费者分组,每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定Group Name,若不指定Group Name则输入默认的Group),同一个Consumer Group中的Consumers,Kafka将相应Topic中的每个消息只发送给其中一个Consumer。
ZookeeperZooKeeper用于分布式系统的协调,保存着集群broker、topic、partition等meta数据;另外,还负责broker故障发现,partition leader选举,负载均衡等功能
Partition每个分区都是一个顺序的、不可变的消息队列, 并且可以持续的添加,分区中的消息都被分了一个序列号,称之为偏移量(offset),在每个分区中此偏移量都是唯一的。Producer在发布消息的时候,可以为每条消息指定Key,这样消息被发送到broker时,会根据分区算法把消息存储到对应的分区中(一个分区存储多个消息),如果分区规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,这样就实现了负载均衡。
Replica副本,replica副本单元是topic的partition,保障partition的高可用,一个partition的replica数量不能超过broker的数量,因为一个broker最多只会存储这个partition的一个副本
Leaderreplica的虚拟主节点、处理与客户端交互,数据复制等,一个partition一般只有一个Leader
Followerreplica的虚拟从节点,类似选民,完全被动

2.5 kafka可视化工具

kafka官方不提供后台管理页面,主流kafka可视化工具有两种:

  • 安装在本地的服务。通过网络连接kafka拉取数据并展示 比如:Offset Explorer工具

  • 安装在服务器上的应用。通过服务器自行拉取数据对外提供Web,查看kafka的状态。比如:kafdrop应用
    在这里插入图片描述

2.6 编译kafka动态库

​ 目前对接kafka消息中间件的主流开源库是librdkafka,它是基于C语言封装的,版本一直在迭代更新。C语言的接口在C++程序中使用不是那么方便,后面有人基于librdkafka库封装了cppkafka这个C++库,更便于C++程序集成,但是版本更新周期比较长。

​ 本文使用cppkafka库的接口来演示kafka的使用,使用vcpkg软件包管理编译cppkafka动态库(工具会自动编译依赖的librdkafka库)。只需要一句命令【.\vcpkg install cppkafka:x64-windows --debug】即可完成动态库编译。linux版本库在公司环境下可通过windows子系统的方式来编译(解决联网的问题)。
在这里插入图片描述

2.7 Kafka使用示例

2.7.1 kafka使用流程

2.7.1.1 kafka消费者使用步骤

(1)配置消费者客户端参数。
(2)创建相应的消费者实例。
(3)订阅主题。
(4)拉取消息并消费;
(5)提交消息位移;
(6)关闭消费者实例;

2.7.1.2 kafka生产者使用步骤

(1)配置生产者客户端参数。
(2)创建相应的生产者实例。
(3)构建待发送的消息。
(4)发送消息。
(5)关闭生产者实例。

2.7.2 配置kafka连接信息

一般情况下只需要配置brokers、topic和partition三个参数,其他配置可以参照kafka官网介绍的配置项进行个性化配置。

2.7.3 消费者消费数据示例

  • KafkaProducer.h

    #pragma once
    #include <string>
    #include <iostream>
    #include "rdkafkacpp.h"
    
    class KafkaProducer
    {
    public:
    	explicit KafkaProducer(const std::string& brokers, const std::string& topic, int partition);
    
    	void pushMessage(const std::string& str, const std::string& key);
    
    	~KafkaProducer();
    
    private:
    	bool initialize();
    
    private:
    	std::string m_brokers;			// Broker列表,多个使用逗号分隔
    	std::string m_topicStr;			// Topic名称
    	int m_partition;				// 分区
    
    	RdKafka::Conf* m_config;        // Kafka Conf对象
    	RdKafka::Conf* m_topicConfig;   // Topic Conf对象
    	RdKafka::Topic* m_topic;		// Topic对象
    	RdKafka::Producer* m_producer;	// Producer对象
    
    	/*只要看到Cb 结尾的类,要继承它然后实现对应的回调函数*/
    	RdKafka::DeliveryReportCb* m_dr_cb;
    	RdKafka::EventCb* m_event_cb;
    	RdKafka::PartitionerCb* m_partitioner_cb;
    };
    
  • KafkaProducer.cpp

    #include "KafkaProducer.h"
    #include "ProducerDeliveryReportListener.h"
    #include "ProducerEventListener.h"
    #include "HashPartitionerListener.h"
    
    KafkaProducer::KafkaProducer(const std::string& brokers, const std::string& topic, int partition)
    {
    	m_brokers = brokers;
    	m_topicStr = topic;
    	m_partition = partition;
    
    	initialize();
    }
    
    bool KafkaProducer::initialize()
    {
    	/* 创建Kafka Conf对象 */
    	m_config = RdKafka::Conf::create(RdKafka::Conf::CONF_GLOBAL);
    	if (m_config == nullptr)
    	{
    		std::cout << "Create RdKafka CONF_GLOBAL failed." << std::endl;
    		return false;
    	}
    
    	/* 创建Topic Conf对象 */
    	m_topicConfig = RdKafka::Conf::create(RdKafka::Conf::CONF_TOPIC);
    	if (m_topicConfig == nullptr)
    	{
    		std::cout << "Create RdKafka CONF_TOPIC failed." << std::endl;
    		return false;
    	}
    		
    	/* 设置Broker属性 */
    	RdKafka::Conf::ConfResult errCode;
    	std::string errorStr;
    	m_dr_cb = new ProducerDeliveryReportListener();
    	// 设置dr_cb属性值
    	errCode = m_config->set("dr_cb", m_dr_cb, errorStr);
    	if (errCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set dr_cb failed:" << errorStr << std::endl;
    		return false;
    	}
    
    	// 设置event_cb属性值
    	m_event_cb = new ProducerEventListener();
    	errCode = m_config->set("event_cb", m_event_cb, errorStr);
    	if (errCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set failed:" << errorStr << std::endl;
    		return false;
    	}
    
    	// 自定义分区策略
    	m_partitioner_cb = new HashPartitionerListener();
    	errCode = m_topicConfig->set("partitioner_cb", m_partitioner_cb, errorStr);
    	if (errCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set failed:" << errorStr << std::endl;
    		return false;
    	}
    
    	// 设置配置对象的属性值
    	errCode = m_config->set("statistics.interval.ms", "10000", errorStr);
    	if (errCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set failed:" << errorStr << std::endl;
    		return false;
    	}
    
    	errCode = m_config->set("message.max.bytes", "10240000", errorStr);
    	if (errCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set failed:" << errorStr << std::endl;
    		return false;
    	}
    
    	errCode = m_config->set("bootstrap.servers", m_brokers, errorStr);
    	if (errCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set failed:" << errorStr << std::endl;
    		return false;
    	}
    
    	/* 创建Producer */
    	m_producer = RdKafka::Producer::create(m_config, errorStr);
    	if (m_producer == nullptr)
    	{
    		std::cout << "Create Producer failed:" << errorStr << std::endl;
    		return false;
    	}
    
    	/* 创建Topic对象 */
    	m_topic = RdKafka::Topic::create(m_producer, m_topicStr, m_topicConfig, errorStr);
    	if (m_topic == nullptr)
    	{
    		std::cout << "Create Topic failed:" << errorStr << std::endl;
    		return false;
    	}
    
    	return true;
    }
    
    KafkaProducer::~KafkaProducer()
    {
    	while (m_producer->outq_len() > 0)
    	{
    		std::cerr << "Waiting for " << m_producer->outq_len() << std::endl;
    		m_producer->flush(5000);
    	}
    
    	if (m_config != nullptr)
    	{
    		delete m_config;
    		m_config = nullptr;
    	}
    	
    	if (m_topicConfig != nullptr)
    	{
    		delete m_topicConfig;
    		m_topicConfig = nullptr;
    	}
    
    	if (m_topic != nullptr)
    	{
    		delete m_topic;
    		m_topic = nullptr;
    	}
    
    	if (m_producer != nullptr)
    	{
    		delete m_producer;
    		m_producer = nullptr;
    	}
    
    	if (m_dr_cb != nullptr)
    	{
    		delete m_dr_cb;
    		m_dr_cb = nullptr;
    	}
    
    	if (m_event_cb != nullptr)
    	{
    		delete m_event_cb;
    		m_event_cb = nullptr;
    	}
    
    	if (m_partitioner_cb != nullptr)
    	{
    		delete m_partitioner_cb;
    		m_partitioner_cb = nullptr;
    	}
    }
    
    void KafkaProducer::pushMessage(const std::string& str, const std::string& key)
    {
    	int32_t len = str.length();
    	void* payload = const_cast<void*>(static_cast<const void*>(str.data()));
    	RdKafka::ErrorCode errorCode = m_producer->produce(m_topic, RdKafka::Topic::PARTITION_UA,
    		RdKafka::Producer::RK_MSG_COPY, payload, len, &key, nullptr);
    	m_producer->poll(0);
    	if (errorCode != RdKafka::ERR_NO_ERROR)
    	{
    		std::cerr << "Produce failed: " << RdKafka::err2str(errorCode) << std::endl;
    		if (errorCode == RdKafka::ERR__QUEUE_FULL)
    		{
    			m_producer->poll(100);
    		}
    	}
    }
    
  • HashPartitionerListener.h

    /*
    * PartitionerCb用实现自定义分区策略,需要使用RdKafka::Conf::set()设置partitioner_cb属性
    */
    #pragma once
    #include "rdkafkacpp.h"
    class HashPartitionerListener : public RdKafka::PartitionerCb
    {
    public:
    	HashPartitionerListener() = default;
    	~HashPartitionerListener() = default;
    
    public:
    	int32_t partitioner_cb(const RdKafka::Topic *topic, const std::string *key,
    							int32_t partition_cnt, void *msg_opaque);
    private:
    	static inline unsigned int generate_hash(const char *str, size_t len);
    };
    
  • HashPartitionerListener.cpp

    #include "HashPartitionerListener.h"
    #include <iostream>
    
    int32_t HashPartitionerListener::partitioner_cb(const RdKafka::Topic *topic, const std::string *key, int32_t partition_cnt, void *msg_opaque)
    {
    	char msg[128] = { 0 };
    	int32_t partition_id = generate_hash(key->c_str(), key->size()) % partition_cnt;
    	// [topic][key][partition_cnt][partition_id] 
    	// :[test][6419][2][1]
    	sprintf_s(msg, "HashPartitionerCb:topic:[%s], key:[%s]partition_cnt:[%d], partition_id:[%d]", topic->name().c_str(),
    		key->c_str(), partition_cnt, partition_id);
    	std::cout << msg << std::endl;
    	return partition_id;
    }
    
    unsigned int HashPartitionerListener::generate_hash(const char *str, size_t len)
    {
    	unsigned int hash = 5381;
    	for (size_t i = 0; i < len; i++)
    	{
    		hash = ((hash << 5) + hash) + str[i];
    	}
    		
    	return hash;
    }
    
  • ProducerDeliveryReportListener.h

    #pragma once
    #include "rdkafkacpp.h"
    
    class ProducerDeliveryReportListener : public RdKafka::DeliveryReportCb
    {
    public:
    	ProducerDeliveryReportListener() = default;
    	~ProducerDeliveryReportListener() = default;
    
    public:
    	void dr_cb(RdKafka::Message &message);
    };
    
    
  • ProducerDeliveryReportListener.cpp

    /*
    * 每收到一条RdKafka::Producer::produce()函数生产的消息,调用一次投递报告回调函数,RdKafka::Message::err()
    * 将会标识Produce请求的结果。为了使用队列化的投递报告回调函数,必须调用RdKafka::poll()函数。
    */
    #include "ProducerDeliveryReportListener.h"
    #include <iostream>
    
    void ProducerDeliveryReportListener::dr_cb(RdKafka::Message &message) {
    	if (message.err())
    	{
    		std::cerr << "Message delivery failed: " << message.errstr() << std::endl;
    	}
    	else
    	{
    		if (message.key())
    		{
    			std::cout << "Key: " << *(message.key()) << ";" << std::endl;
    		}
    
    		std::cerr << "Message delivered to topic " << message.topic_name()
    			<< " [" << message.partition() << "] at offset "
    			<< message.offset() << std::endl;
    
    		std::cout << "Message delivery for (" << message.len() << " bytes): " << message.errstr() << std::endl;
    	}
    }
    
    
  • ProducerEventListener.h

    /*
    * 事件是从RdKafka传递错误、统计信息、日志等消息到应用程序的通用接口。
    */
    #pragma once
    #include "rdkafkacpp.h"
    #include <iostream>
    
    class ProducerEventListener : public RdKafka::EventCb
    {
    public:
    	ProducerEventListener() = default;
    	~ProducerEventListener() = default;
    
    public:
    	void event_cb(RdKafka::Event &event);
    };
    
    
  • ProducerEventListener.cpp

    #include "ProducerEventListener.h"
    
    
    void ProducerEventListener::event_cb(RdKafka::Event &event)
    {
    	switch (event.type())
    	{
    	case RdKafka::Event::EVENT_ERROR:
    	{
    		std::cerr << "ERROR (" << RdKafka::err2str(event.err()) << "): " << event.str() << std::endl;
    	}
    	break;
    	case RdKafka::Event::EVENT_STATS:
    	{
    		std::cerr << "\"STATS\": " << event.str() << std::endl;
    	}
    	break;
    	case RdKafka::Event::EVENT_LOG:
    	{
    		fprintf(stderr, "LOG-%i-%s: %s\n", event.severity(), event.fac().c_str(), event.str().c_str());
    	}
    	break;
    	case RdKafka::Event::EVENT_THROTTLE:
    	{
    		std::cerr << "THROTTLED: " << event.throttle_time() << "ms by " <<
    			event.broker_name() << " id " << (int)event.broker_id() << std::endl;
    	}
    	break;
    	default:
    		std::cerr << "EVENT " << event.type() << " (" << RdKafka::err2str(event.err()) << "): " << event.str() << std::endl;
    		break;
    	}
    }
    
  • Main.cpp

    #include "ProducerEventListener.h"
    #include "KafkaProducer.h"
    #include <csignal>
    #include <iomanip>
    #include <chrono>
    #include <thread>
    
    static bool run = true;
    void sigterm(int sig)
    {
    	run = false;
    }
    
    int main(int argc, char const *argv[])
    {
    	/*
    	* Kafka Producer使用流程:
    	*	1.创建Kafka配置实例。
    	*	2.创建Topic配置实例。
    	*	3.设置Kafka配置实例Broker属性。
    	*	4.设置Topic配置实例属性。
    	*	5.注册回调函数(分区策略回调函数需要注册到Topic配置实例)。
    	*	6.创建Kafka Producer客户端实例。
    	*	7.创建Topic实例。
    	*	8.生产消息。
    	*	9.阻塞等待Producer生产消息完成。
    	*	10.等待Produce请求完成。
    	*	11.销毁Kafka Producer客户端实例。
    	*/
    	signal(SIGINT, sigterm);
    	signal(SIGTERM, sigterm);
    
    	std::string brokers = "10.19.131.94:29092";
    	std::string topic = "XSINK_PERSON_ALARM";
    	int partition = 0;
    
    	KafkaProducer producer(brokers, topic, partition);
    	for (int i = 0; i < 10000; i++)
    	{
    		char msg[64] = { 0 };
    		sprintf_s(msg, "%s%4d", "Hello Kafka, My name is C++! ", i);
    		// 生产消息
    		char key[8] = { 0 };      // 主要用来做负载均衡
    		sprintf_s(key, "%d", i);
    		producer.pushMessage(msg, key);
    		std::this_thread::sleep_for(std::chrono::microseconds(500));
    	}
    
    	RdKafka::wait_destroyed(5000);
    
    	return 0;
    }
    

2.7.4 消费者消费数据示例

  • KafkaConsumer.h

    #include <string>
    #include <vector>
    #include <memory>
    #include "rdkafkacpp.h"
    
    class KafkaConsumer {
    public:
    	KafkaConsumer(const std::string& brokers, const std::string& groupID,
    		const std::vector<std::string>& topics, int64_t partition);
    	~KafkaConsumer();
    
    	void pullMessage(const int timeout = 1000);
    
    	void stop();
    
    private:
    	bool initialize();
    
    private:
    	std::string m_brokers;
    	std::string m_groupID;
    	std::vector<std::string> m_topicVector;
    	int64_t m_partition;
    	RdKafka::Conf* m_config;
    	RdKafka::Conf* m_topicConfig;
    	RdKafka::KafkaConsumer* m_consumer;
    	RdKafka::EventCb* m_event_cb;
    	RdKafka::RebalanceCb* m_rebalance_cb;
    	RdKafka::DeliveryReportCb* m_deliveryReportCb;
    
    	bool m_bRunFlag;
    };
    
  • KafkaConsumer.cpp

    #include "KafkaConsumer.h"
    #include <algorithm>
    #include <iterator>
    #include <string>
    #include <windows.h>
    #include "ConsumerEventListener.h"
    #include "ConsumerRebalanceListener.h"
    
    int msg_consume(std::string &data, RdKafka::Message* message, void* opaque) {
    	int len = -1;
    	switch (message->err()) {
    	case RdKafka::ERR__TIMED_OUT:
    		std::cout << "RdKafka::ERR__TIMED_OUT" << std::endl;
    		break;
    	case RdKafka::ERR_NO_ERROR:
    	{
    		if (message->payload())
    		{
    			len = static_cast<int>(message->len());
    			//data.resize(len);
    			const char *msg = static_cast<const char *>(message->payload());
    			data = std::string(msg);
    			//memcpy(&data[0], msg, len);
    		}
    	}
    	break;
    	case RdKafka::ERR__PARTITION_EOF:
    	{
    		len = 0;
    	}
    	break;
    	case RdKafka::ERR__UNKNOWN_TOPIC:
    	case RdKafka::ERR__UNKNOWN_PARTITION:
    	default:
    		/* Errors */
    		std::cerr << "Consume failed: " << message->errstr() << std::endl;
    		len = -1;
    	}
    	return len;
    }
    
    KafkaConsumer::KafkaConsumer(const std::string& brokers, const std::string& groupID,
    	const std::vector<std::string>& topics, int64_t partition) :
    	m_config(nullptr)
    	, m_topicConfig(nullptr)
    	, m_consumer(nullptr)
    	, m_rebalance_cb(nullptr)
    	, m_event_cb(nullptr)
    	, m_bRunFlag(false)
    	, m_deliveryReportCb(nullptr)
    {
    	m_brokers = brokers;
    	m_groupID = groupID;
    	m_topicVector = topics;
    	m_partition = partition;
    
    	initialize();
    }
    
    KafkaConsumer::~KafkaConsumer()
    {
    	if (m_consumer != nullptr)
    	{
    		// 取消topic订阅
    		m_consumer->unsubscribe();
    		// 关闭消费者数据连接
    		m_consumer->close();
    
    		delete m_consumer;
    		m_consumer = nullptr;
    	}
    	
    	if (m_config != nullptr) 
    	{
    		delete m_config;
    		m_config = nullptr;
    	}
    	
    	if (m_topicConfig != nullptr)
    	{
    		delete m_topicConfig;
    		m_topicConfig = nullptr;
    	}
    	
    	if (m_event_cb != nullptr)
    	{
    		delete m_event_cb;
    		m_event_cb = nullptr;
    	}
    	
    	if (m_rebalance_cb != nullptr)
    	{
    		delete m_rebalance_cb;
    		m_rebalance_cb = nullptr;
    	}
    
    	if (m_deliveryReportCb != nullptr)
    	{
    		delete m_deliveryReportCb;
    		m_deliveryReportCb = nullptr;
    	}
    }
    
    bool KafkaConsumer::initialize()
    {
    	std::string errorStr;
    	RdKafka::Conf::ConfResult errorCode;
    	m_config = RdKafka::Conf::create(RdKafka::Conf::CONF_GLOBAL);
    	if (m_config == nullptr)
    	{
    		std::cout << "Create CONF_GLOBAL failed!" << std::endl;
    		return false;
    	}
    
    	errorCode = m_config->set("heartbeat.interval.ms", "1000", errorStr);
    	if (errorCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set heartbeat.interval.ms failed: " << errorStr << std::endl;
    		return false;
    	}
    
    	m_event_cb = new ConsumerEventListener();
    	errorCode = m_config->set("event_cb", m_event_cb, errorStr);
    	if (errorCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set event_cb failed: " << errorStr << std::endl;
    		return false;
    	}
    
    	m_rebalance_cb = new ConsumerRebalanceListener();
    	errorCode = m_config->set("rebalance_cb", m_rebalance_cb, errorStr);
    	if (errorCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set rebalance_cb failed: " << errorStr << std::endl;
    		return false;
    	}
    
    	errorCode = m_config->set("enable.partition.eof", "false", errorStr);
    	if (errorCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set enable.partition.eof failed: " << errorStr << std::endl;
    		return false;
    	}
    
    	errorCode = m_config->set("group.id", m_groupID, errorStr);
    	if (errorCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set group.id failed: " << errorStr << std::endl;
    		return false;
    	}
    
    	errorCode = m_config->set("bootstrap.servers", m_brokers, errorStr);
    	if (errorCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set bootstrap.servers failed: " << errorStr << std::endl;
    		return false;
    	}
    
    	errorCode = m_config->set("max.partition.fetch.bytes", "1024000", errorStr);
    	if (errorCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set max.partition.fetch.bytes failed: " << errorStr << std::endl;
    		return false;
    	}
    
    	// partition.assignment.strategy  range,roundrobin
    	m_topicConfig = RdKafka::Conf::create(RdKafka::Conf::CONF_TOPIC);
    	if (m_topicConfig == nullptr)
    	{
    		std::cout << "Create CONF_TOPIC failed!"<< std::endl;
    		return false;
    	}
    
    	// 获取最新的消息数据
    	errorCode = m_topicConfig->set("auto.offset.reset", "latest", errorStr);
    	if (errorCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Topic Conf set auto.offset.reset failed: " << errorStr << std::endl;
    		return false;
    	}
    
    	errorCode = m_config->set("default_topic_conf", m_topicConfig, errorStr);
    	if (errorCode != RdKafka::Conf::CONF_OK)
    	{
    		std::cout << "Conf set default_topic_conf failed: " << errorStr << std::endl;
    		return false;
    	}
    
    	m_consumer = RdKafka::KafkaConsumer::create(m_config, errorStr);
    	if (m_consumer == nullptr)
    	{
    		std::cout << "Create KafkaConsumer failed!"<< std::endl;
    		return false;
    	}
    	std::cout << "Created consumer " << m_consumer->name() << std::endl;
    
    	return true;
    }
    
    void KafkaConsumer::pullMessage(const int timeout)
    {
    	m_bRunFlag = true;
    	// 订阅Topic
    	RdKafka::ErrorCode errorCode = m_consumer->subscribe(m_topicVector);
    
    	// 消费消息
    	while (m_bRunFlag)
    	{
    		std::string data;
    		std::shared_ptr<RdKafka::Message> msg = std::shared_ptr<RdKafka::Message>(m_consumer->consume(timeout));
    		int len = msg_consume(data, msg.get(), nullptr);
    		if (len > 0)
    		{
    			std::cout << data << std::endl;
    		}
    	}
    }
    
    void KafkaConsumer::stop()
    {
    	m_bRunFlag = false;
    }
    
  • ConsumerRebalanceListener.h

    /*
    * 注册rebalance_cb回调函数会关闭rdkafka的自动分区赋值和再分配并替换应用程序的rebalance_cb回调函数。
    * 再平衡回调函数负责对基于RdKafka::ERR_ASSIGN_PARTITIONS和RdKafka::ERR_REVOKE_PARTITIONS事件
    * 更新rdkafka的分区分配,也能处理任意前两者错误除外其它再平衡失败错误。对于RdKafka::ERR_ASSIGN_PARTITIONS和
    * RdKafka::ERR_REVOKE_PARTITIONS事件之外的其它再平衡失败错误,必须调用unassign()同步状态。
    * 没有再平衡回调函数,rdkafka也能自动完成再平衡过程,但注册一个再平衡回调函数可以使应用程序在执行其它
    * 操作时拥有更大的灵活性,例如从指定位置获取位移或手动提交位移。
    */
    #pragma once
    #include "rdkafkacpp.h"
    class ConsumerRebalanceListener : public RdKafka::RebalanceCb
    {
    public:
    	ConsumerRebalanceListener() = default;
    	~ConsumerRebalanceListener() = default;
    
    public:
    	void rebalance_cb(RdKafka::KafkaConsumer *consumer,
    		RdKafka::ErrorCode err,
    		std::vector<RdKafka::TopicPartition*> &partitions);
    
    private:
    	// 打印当前获取的分区
    	static void printTopicPartition(const std::vector<RdKafka::TopicPartition*>&partitions);
    
    private:
    	int partition_count;
    };
    
  • ConsumerRebalanceListener.cpp

    #include "ConsumerRebalanceListener.h"
    #include <iostream>
    
    // 打印当前获取的分区
    void ConsumerRebalanceListener::printTopicPartition(const std::vector<RdKafka::TopicPartition*>&partitions)        
    {
    	for (unsigned int i = 0; i < partitions.size(); i++)
    	{
    		std::cerr << partitions[i]->topic() << "[" << partitions[i]->partition() << "], ";
    	}
    	std::cerr << "\n";
    }
    
    void ConsumerRebalanceListener::rebalance_cb(RdKafka::KafkaConsumer *consumer,
    											RdKafka::ErrorCode err,
    											std::vector<RdKafka::TopicPartition*> &partitions)
    {
    	std::cerr << "RebalanceCb: " << RdKafka::err2str(err) << ": ";
    	printTopicPartition(partitions);
    	if (err == RdKafka::ERR__ASSIGN_PARTITIONS)
    	{
    		consumer->assign(partitions);
    		partition_count = (int)partitions.size();
    	}
    	else
    	{
    		consumer->unassign();
    		partition_count = 0;
    	}
    }
    
  • ConsumerEventListener.h

    /*
    * 事件是从RdKafka传递错误、统计信息、日志等消息到应用程序的通用接口
    */
    #pragma once
    #include "rdkafkacpp.h"
    #include <iostream>
    
    class ConsumerEventListener : public RdKafka::EventCb
    {
    public:
    	ConsumerEventListener() = default;
    	~ConsumerEventListener() = default;
    
    public:
    	void event_cb(RdKafka::Event &event);
    };
    
  • ConsumerEventListener.cpp

    #include "ConsumerEventListener.h"
    
    
    void ConsumerEventListener::event_cb(RdKafka::Event &event)
    {
    	switch (event.type())
    	{
    	case RdKafka::Event::EVENT_ERROR:
    	{
    		std::cerr << "ERROR (" << RdKafka::err2str(event.err()) << "): " << event.str() << std::endl;
    	}
    	break;
    	case RdKafka::Event::EVENT_STATS:
    	{
    		std::cerr << "\"STATS\": " << event.str() << std::endl;
    	}
    	break;
    	case RdKafka::Event::EVENT_LOG:
    	{
    		fprintf(stderr, "LOG-%i-%s: %s\n", event.severity(), event.fac().c_str(), event.str().c_str());
    	}
    	break;
    	case RdKafka::Event::EVENT_THROTTLE:
    	{
    		std::cerr << "THROTTLED: " << event.throttle_time() << "ms by " <<
    			event.broker_name() << " id " << (int)event.broker_id() << std::endl;
    	}
    	break;
    	default:
    		std::cerr << "EVENT " << event.type() << " (" << RdKafka::err2str(event.err()) << "): " << event.str() << std::endl;
    		break;
    	}
    }
    
  • Main.cpp

    #include "ConsumerEventListener.h"
    #include "KafkaConsumer.h"
    #include <csignal>
    #include <iomanip>
    
    static bool run = true;
    void sigterm(int sig)
    {
    	run = false;
    }
    
    int main(int argc, char const *argv[])
    {
    	/*
    	* RdKafka提供了两种消费者API,低级API的Consumer和高级API的KafkaConsumer。
    	* Kafka Consumer使用流程:
    	* 	1.创建Kafka配置实例。
    	* 	2.创建Topic配置实例。
    	* 	3.设置Kafka配置实例Broker属性。
    	* 	4.设置Topic配置实例属性。
    	* 	5.注册回调函数。
    	* 	6.创建Kafka Consumer客户端实例。
    	* 	7.创建Topic实例。
    	* 	8.订阅主题。
    	* 	9.消费消息。
    	* 	10.关闭消费者实例。
    	* 	11.销毁释放RdKafka资源。
    	*/
    
    	signal(SIGINT, sigterm);
    	signal(SIGTERM, sigterm);
    
    	//brokers参数支持多个服务节点,中间用分号隔开
    	std::string brokers = "10.19.131.94:29092";
    	std::string groupId = "kafkaExample-Client";
    	std::vector<std::string> topics;
    	topics.push_back("XSINK_PERSON_ALARM");
    
    	KafkaConsumer consumer(brokers, groupId, topics, RdKafka::Topic::OFFSET_BEGINNING);
    	consumer.pullMessage(1000);
    	if (run == false)
    	{
    		std::cout << "stop consumer" << std::endl;
    		consumer.stop();
    	}
    
    	RdKafka::wait_destroyed(5000);
    	return 0;
    }
    

2.7.5 项目应用

与大多数消息系统比较kafka有更好的吞吐量内置分区、副本和故障转移,这有利于处理大规模的消息。在新架构平台下主要用于人脸、车辆、人流等数据的传输。

3. C++对接RabbitMQ消息队列

3.1 RabbitMQ消息队列简介

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。

3.2 RabbitMQ特性说明

  • 服从AMQP规范
    • AMQP高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计,不受产品、开发语言等条件的限制。AMQP的主要特征是面向消息、队列路由(点对点和发布/订阅)、可靠性、安全。
  • 多语言和多应用协议客户端
    • RabbitMQ是基于erlang语言开发、支持多种客户端:Python、Ruby、.Net、Java、JMS、C、PHP、ActionScript。
    • 支持应用协议XMPP、STOMP。
  • 消息集群和高可用
    • 多个消息服务器可以组成一个集群,形成一个逻辑Broker
    • 队列可以在集群中的机器上进行镜像,是的在部分节点出问题的情况下队列仍然可用
  • 可靠和灵活路由
    • 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
  • 插件机制
    • RabbitMQ提供了许多插件,来从多方面进行扩展,也可以编写自己的插件

3.3 RabbitMQ常用概念

img

概念说明
Broker即 RabbitMQ Server,消息队列服务器本身
Virtual Host虚拟主机。每个 vhost 用于自己的 queue/exchange/binding/权限,默认的 vhost 是 /
Message消息,headers + body,其中消息头由一系列的可选属性组成,包括 routing-key、priority、delivery-mode (消息是否需要持久化)等
Exchange交换器。根据分发规则,匹配 routing key,将消息发给指定队列
Queue队列,用于保存消息,直到消息被发送给消费者。一个消息可以被放到一个或多个队列
Binding绑定,定义了交换器和队列之间的路由规则。binding 中包括 routing key。交换器中有一张 binding 的查询表,是 Message 的分发依据
Connection网络连接,比如一个 TCP 连接
Channel信道,是一个多路复用的双向数据流通道。信道是建立在真实 TCP 连接上的虚拟连接。由于系统建立和销毁 TCP 的开销是很大的,并且很难配置防火墙,所以引入信道来复用一条 TCP 连接。AMQP 的命令都是通过信道发送的,包括发布消息、订阅队列、接收消息等等,所以每个命令会携带一个 channel ID。Channel 作为轻量级的 Connection 极大降低了操作系统建立 TCP 连接的开销。
Publisher消息的生成者,也是一个向交换机发布消息的客户端应用程序
Consumer消息消费者,也就是接受消息的程序
Routing Key路由关键字,Exchange根据这个关键字进行消息投递

3.4 RabbitMQ可视化工具

请添加图片描述

RabbitMQ提供了可视化网页,便于我们开发测试和运维,是非常友好的。关于后台网页的详细使用,在此就不展开了,网上资料很多,可以参照下面的博客:https://blog.csdn.net/u012702547/article/details/121493311

3.5 编译RabbitMQ动态库

RabbitMQ官方没有提供C/C++版本的SDK库,因此只能依赖github这个强大的开源社区了,主要找到了两个AMQP-CPPrabbitmq-c对接库,AMQP-CPP是基于rabbitmq-c进行封住的,版本更新较慢,且配套文档较少,使用不是很友好。rabbitmq-c是提供的C接口,需要自己封装,但是它更新比较活跃,文档比较齐全,对于在实际项目中使用会更加有保障。vcpkg通过命令【vcpkg install librabbitmq:x64-windows --debug】编译rabbitmq-c库
在这里插入图片描述

3.6 RabbitMQ的四种交换机类型

3.6.1 Fanout Exchange广播模式

请添加图片描述

Fanout Exchange模式不处理路由键,它的路由规则非常简单,只需要简单的将队列绑定到交换机上。一个发送到交换的消息都会本转发到与该Exchange绑定的所有Queue上。Fanout Exchange广播模式特点:

  • Fanout Exchange模式可以理解为路由表的模式,这种模式不需要RouteKey
  • Fanout Exchange模式需要提交将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定
  • 如果接收到的消息的Exchanhe没有与任何Queue绑定,则消息会被抛弃
  • 任何发送到Fanout Exchange的消息都会被转发到与该Exchange绑定的所有Queue上

3.6.2 Direct Exchange直通模式

在这里插入图片描述

Direct Exchange模式需要处理路由键,需要将一个队列绑定到交换机上,如果消息中的路由键(routing key)和Binding中的binding key一致,交换机(Exchange)就将消息发到对应的队列(Queue)中,这是一个完整的匹配。一般情况下可以使用RabbitMQ自带的default Exchange。该Exchange的名字为空字符串,当你手动创建一个队列时,后台会自动将这个队列绑定到一个名称为空的Direct Exchange上,绑定RoutingKey与队列名称相同。有了这个默认的交换机和绑定,使我们只关心队列这一层即可,这种比较适合做一些简单的应用。Direct Exchange直通模式特点:

  • 在进行消息传递时需要一个RouteKey,可以简单的裂解为要发送到的队列的名称
  • 如果vhost中不存在RouteKey中指定的队列名,则该消息会被抛弃
  • 任何发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue

3.6.3 Topic Exchange主题模式

在这里插入图片描述
Topic Exchange主题模式将路由键和某个模式进行匹配,此时队列需要绑定在一个模式上。符号"#"匹配一个或多个词,符号匹配一个词。因此"ais.#“能够匹配"ais.alarm.queue”,但是"ais.“只会匹配到"ais.alarm”。Topic Exchange主题模式特点:

  • 主题模式较为复杂,就是每个队列都要其关心的主题,所有的消息都带有一个RouteKey,Exchange会将消息转发到所有关注主题能与RouteKey模糊匹配的队列
  • 在进行绑定是,要提供一个该队列关心的主题,如"#.log.#"表示该队列关心所有涉及log的消息
  • 如果Exchange没有发现足够与RouteKey匹配的Queue,则会抛弃此消息
  • 任何发送到Topic Exchange的消息都会被转到到所有关心RouteKey中指定话题的Queue上

3.6.4 Headers Exchange消息头参数匹配模式

Headers Exchange会忽略RouteKey而根据消息中的Headers和创建绑定关系时指定Arguments来匹配决定路由到那些Queue。在绑定Queue与Exchange时指定一组键值对,当消息发送到Exchange时,RabbitMQ会去到该消息的Headers,对比其中的键值对是否完全匹配Queue与Exchange绑定是指定的键值对,如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。Headers Exchange的性能比较差,而且Direct Exchange完全可以替代它,所以不建议使用。

3.7 RabbitMQ使用示例

因为使用示例是按照项目实战的标准进行了代码封装,代码较多,本文中只贴生产者端和消费者端的核心代码,完成的代码示例可通过附件查看。

3.7.1 Fanout Exchange广播模式代码示例

  • 生产者代码示例

    int RabbitMqProducerTest::fanoutModeTest()
    {
    	std::string exchangeName = "rabbitmq.exchange.fanout.test";
    	std::string queueName = "rabbitmq.queue.fanout.test";
    	std::string routingKeys = "rabbitmq.fanout.routingKeys.test";
    	std::string err;
    	RabbitMqClient rabbitMqClient(connectHost, connectPort, connectUserName, connectPassword);
    	if (0 != rabbitMqClient.connect(err))
    	{
    		return -1;
    	}
    
    	RabbitMqExchange exchange(exchangeName, "fanout", true, false);
    	if (0 != rabbitMqClient.declareExchange(exchange, err))
    	{
    		return -1;
    	}
    
    	amqp_basic_properties_t properties;
    	properties._flags = AMQP_BASIC_CONTENT_TYPE_FLAG | AMQP_BASIC_DELIVERY_MODE_FLAG;
    	properties.content_type = amqp_cstring_bytes("text/plain");
    	properties.delivery_mode = 2;
    
    	for (int i = 0; i < 5000; i++)
    	{
    		std::string szBuf = "Hello RabbitMQ, My name is C++, fanout";
    		RabbitMqMessage message(szBuf, properties, routingKeys);
    		if (0 != rabbitMqClient.publishMessage(message, exchangeName, routingKeys, err))
    		{
    			return -1;
    		}
    		std::cout << "Send message success: " << szBuf << std::endl;
    		std::this_thread::sleep_for(std::chrono::milliseconds(500));
    	}
    
    	return 0;
    }
    
  • 消费者代码示例

    int RabbitMqConsumerTest::fanoutModeTest()
    {
    	std::string exchangeName = "rabbitmq.exchange.fanout.test";
    	std::string queueName = "rabbitmq.queue.fanout.test";
    	std::string routingKeys = "rabbitmq.fanout.routingKeys.test";
    	std::string err;
    	//mq连接
    	RabbitMqClient rabbitMqClient(connectHost, connectPort, connectUserName, connectPassword);
    	if (0 != rabbitMqClient.connect(err))
    	{
    		return -1;
    	}
    
    	//声明交换机
    	RabbitMqExchange exchange(exchangeName, "fanout", true, false);
    	if (0 != rabbitMqClient.declareExchange(exchange, err))
    	{
    		return -1;
    	}
    
    	//声明队列
    	RabbitMqQueue queue(queueName);
    	if (0 != rabbitMqClient.declareQueue(queue, err))
    	{
    		return -1;
    	}
    
    	//队列绑定到交换机上
    	if (0 != rabbitMqClient.bindQueueToExchange(queue, exchange, routingKeys, err))
    	{
    		return -1;
    	}
    
    	do
    	{
    		std::vector<std::string> messageVec;
    		struct timeval tv;
    		tv.tv_sec = 5;
    		tv.tv_usec = 0;
    		if (rabbitMqClient.consumerMessage(messageVec, queueName, 2, tv, err)  != 0)
    		{
    			continue;
    		}
    		std::cout << "Message received successfully" << std::endl;
    		for (int i = 0; i < messageVec.size(); i++)
    		{
    			char str[64];
    			time_t now = time(nullptr);
    			tm time;
    			localtime_s(&time, &now);
    			strftime(str, 64, "%Y-%m-%d %X", &time);
    			std::cout << "["<< str << "]" << "[Message]: " << messageVec[i].c_str() << std::endl;
    		}
    	} while (1);
    
    	return 0;
    }
    

3.7.2 Direct Exchange直通模式代码示例

  • 生产者代码示例

    int RabbitMqProducerTest::directModeTest()
    {
    	std::string exchangeName = "rabbitmq.exchange.direct.test";
    	std::string queueName = "rabbitmq.queue.direct.test";
    	std::string routingKeys = "rabbitmq.direct.routingKeys.test";
    	std::string err;
    	RabbitMqClient rabbitMqClient(connectHost, connectPort, connectUserName, connectPassword);
    	if (0 != rabbitMqClient.connect(err))
    	{
    		return -1;
    	}
    
    	RabbitMqExchange exchange(exchangeName, "direct", true, false);
    	if (0 != rabbitMqClient.declareExchange(exchange, err))
    	{
    		return -1;
    	}
    
    	amqp_basic_properties_t properties;
    	properties._flags = AMQP_BASIC_CONTENT_TYPE_FLAG | AMQP_BASIC_DELIVERY_MODE_FLAG;
    	properties.content_type = amqp_cstring_bytes("text/plain");
    	properties.delivery_mode = 2;
    
    	for (int i = 0; i < 50000; i++)
    	{
    		std::string szBuf = "Hello RabbitMQ, My name is C++, direct!";
    		RabbitMqMessage message(szBuf, properties, routingKeys);
    		if (0 != rabbitMqClient.publishMessage(message, exchangeName, routingKeys, err))
    		{
    			return -1;
    		}
    		std::cout << "Send message success: " << szBuf << std::endl;
    		std::this_thread::sleep_for(std::chrono::milliseconds(50));
    	}
    
    	return 0;
    }
    
  • 消费者代码示例

    int RabbitMqConsumerTest::directModeTest()
    {
    	std::string exchangeName = "rabbitmq.exchange.direct.test";
    	std::string queueName = "rabbitmq.queue.direct.test";
    	std::string routingKeys = "rabbitmq.direct.routingKeys.test";
    	std::string err;
    	//mq连接
    	RabbitMqClient rabbitMqClient(connectHost, connectPort, connectUserName, connectPassword);
    	if (0 != rabbitMqClient.connect(err))
    	{
    		return -1;
    	}
    
    	//声明交换机
    	RabbitMqExchange exchange(exchangeName, "direct", true, false);
    	if (0 != rabbitMqClient.declareExchange(exchange, err))
    	{
    		return -1;
    	}
    
    	//声明队列
    	RabbitMqQueue queue(queueName);
    	if (0 != rabbitMqClient.declareQueue(queue, err))
    	{
    		return -1;
    	}
    
    	//队列绑定到交换机上
    	if (0 != rabbitMqClient.bindQueueToExchange(queue, exchange, routingKeys, err))
    	{
    		return -1;
    	}
    
    	do
    	{
    		std::vector<std::string> messageVec;
    		struct timeval tv;
    		tv.tv_sec = 5;
    		tv.tv_usec = 0;
    		if (rabbitMqClient.consumerMessage(messageVec, queueName, 2, tv, err) != 0)
    		{
    			continue;
    		}
    		std::cout << "Message received successfully" << std::endl;
    		for (int i = 0; i < messageVec.size(); i++)
    		{
    			char str[64];
    			time_t now = time(nullptr);
    			tm time;
    			localtime_s(&time, &now);
    			strftime(str, 64, "%Y-%m-%d %X", &time);
    			std::cout << "[" << str << "]" << "[Message]: " << messageVec[i].c_str() << std::endl;
    		}
    	} while (1);
    
    	return 0;
    }
    

3.7.3 Topic Exchange主题模式代码示例

  • 生产者代码示例

    int RabbitMqProducerTest::topicModeTest()
    {
    	std::string exchangeName = "rabbitmq.exchange.topic.test";
    	std::string queueName = "rabbitmq.queue.topic.test";
    	std::string routingKey1 = "hik.topic.test1";
    	std::string routingKey2 = "hik.topic.test2";
    	std::string err;
    	RabbitMqClient rabbitMqClient(connectHost, connectPort, connectUserName, connectPassword);
    	if (0 != rabbitMqClient.connect(err))
    	{
    		return -1;
    	}
    
    	RabbitMqExchange exchange(exchangeName, "topic", true, false);
    	if (0 != rabbitMqClient.declareExchange(exchange, err))
    	{
    		return -1;
    	}
    
    	amqp_basic_properties_t properties;
    	properties._flags = AMQP_BASIC_CONTENT_TYPE_FLAG | AMQP_BASIC_DELIVERY_MODE_FLAG;
    	properties.content_type = amqp_cstring_bytes("text/plain");
    	properties.delivery_mode = 2;
    
    	for (int i = 0; i < 50000; i++)
    	{
    		std::string strBuffer1 = "Hello RabbitMQ, My name is C++, topic1!";
    		RabbitMqMessage message1(strBuffer1, properties, routingKey1);
    		if (0 != rabbitMqClient.publishMessage(message1, exchangeName, routingKey1, err))
    		{
    			return -1;
    		}
    		std::cout << "Send tpoic1 message success: " << strBuffer1 << std::endl;
    
    		std::string strBuffer2 = "Hello RabbitMQ, My name is C++, topic2!";
    		RabbitMqMessage message2(strBuffer2, properties, routingKey2);
    		if (0 != rabbitMqClient.publishMessage(message2, exchangeName, routingKey2, err))
    		{
    			return -1;
    		}
    		std::cout << "Send tpoic2 message success: " << strBuffer2 << std::endl;
    		std::this_thread::sleep_for(std::chrono::milliseconds(50));
    	}
    
    	return 0;
    }
    
    
  • 消费者代码示例

    int RabbitMqConsumerTest::topicModeTest()
    {
    	std::string exchangeName = "rabbitmq.exchange.topic.test";
    	std::string queueName = "rabbitmq.queue.topic.test";
    	std::string routingKeys = "hik.#";
    	std::string err;
    	//mq连接
    	RabbitMqClient rabbitMqClient(connectHost, connectPort, connectUserName, connectPassword);
    	if (0 != rabbitMqClient.connect(err))
    	{
    		return -1;
    	}
    
    	//声明交换机
    	RabbitMqExchange exchange(exchangeName, "topic", true, false);
    	if (0 != rabbitMqClient.declareExchange(exchange, err))
    	{
    		return -1;
    	}
    
    	//声明队列
    	RabbitMqQueue queue(queueName);
    	if (0 != rabbitMqClient.declareQueue(queue, err))
    	{
    		return -1;
    	}
    
    	//队列绑定到交换机上
    	if (0 != rabbitMqClient.bindQueueToExchange(queue, exchange, routingKeys, err))
    	{
    		return -1;
    	}
    
    	do
    	{
    		std::vector<std::string> messageVec;
    		struct timeval tv;
    		tv.tv_sec = 5;
    		tv.tv_usec = 0;
    		if (rabbitMqClient.consumerMessage(messageVec, queueName, 2, tv, err) != 0)
    		{
    			continue;
    		}
    		std::cout << "Message received successfully" << std::endl;
    		for (int i = 0; i < messageVec.size(); i++)
    		{
    			char str[64];
    			time_t now = time(nullptr);
    			tm time;
    			localtime_s(&time, &now);
    			strftime(str, 64, "%Y-%m-%d %X", &time);
    			std::cout << "[" << str << "]" << "[Message]: " << messageVec[i].c_str() << std::endl;
    		}
    	} while (1);
    
    	return 0;
    }
    

3.7 项目应用

RabbitMQ因为灵活多样的对接模式,很适合平台报警数据相关的订阅与发布。

4.C++对接MQTT消息队列

在这里插入图片描述

4.1 MQTT消息队列简介

MQTT 是一种基于客户端服务端架构的发布/订阅模式的消息传输协议。它的设计思想是轻巧、开放、 简单、规范,易于实现。这些特点使得它对很多场景来说都是很好的选择,特别是对于受限的环境如机器与机器的通信(M2M)以及物联网环境(IoT)。

4.2 MQTT特性说明

MQTT 协议是为工作在低带宽、不可靠网络的远程传感器和控制设备之间的通讯而设计的协议,它具 有以下主要的几项特性:

  1. 使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合。
  2. 基于 TCP/IP 提供网络连接。主流的 MQTT 是基于 TCP 连接进行数据推送的,但是同样也有基于 UDP 的版本,叫做 MQTT-SN。这两种版本由于基于不同的连接方式,优缺点自然也就各有不同了。
  3. 支持 QoS 服务质量等级。根据消息的重要性不同设置不同的服务质量等级。
  4. 小型传输,开销很小,协议交换最小化,以降低网络流量。这就是为什么在介绍里说它非常适合"在物联网领域,传感器与服务器的通信,信息的收集",要知道嵌入式设备的运算能力和带宽都相对薄弱,使用这种协议来传递消息再适合不过了,在手机移动应用方面,MQTT 是一种不错的 Android 消息推送方案。
  5. 使用 will 遗嘱机制来通知客户端异常断线。
  6. 基于主题发布/订阅消息,对负载内容屏蔽的消息传输。
  7. 支持心跳机制。

4.3 MQTT协议版本

目前 MQTT 主流版本有两个,分别是 MQTT3.1.1 和 MQTT5。MQTT3.1.1 是在 2014 年 10 月发布的,而 MQTT5 是在 2019 年 3 月发布的。虽然 MQTT3.1.1 与 MQTT5 在时间相差了将近五年,但是 MQTT3.1.1作为一个经典的版本,目前仍然是主流版本,能够满足大部分实际需求。MQTT5 是在 MQTT3.1.1 的基础上进行了升级,因此 MQTT5 是完全兼容 MQTT3.1.1 的。而 MQTT5 是 在 MQTT3.1.1 的基础上添加了更多的功能、补充完善 MQTT 协议。

4.4 MQTT应用场景

MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。

4.5 MQTT常用概念

概念说明
服务 端MQTT 服务端通常是一台服务器(broker),它是 MQTT 信息传输的枢纽,负责将 MQTT 客户端发送来的信息传递给 MQTT 客户端;MQTT 服务端还负责管理 MQTT 客户端,以确保客户端之间的通讯顺畅,保证 MQTT 信息得以正确接收和准确投递。
客户端MQTT 客户端可以向服务端发布信息,也可以从服务端收取信息;我们把客户端发送信息的行为称为 “发布”信息。而客户端要想从服务端收取信息,则首先要向服务端“订阅”信息。“订阅”信息这一操作 很像我们在使用微信时“关注”了某个公众号,当公众号的作者发布新的文章时,微信官方会向关注了该公众号的所有用户发送信息,告诉他们有新文章更新了,以便用户查看。
MQTT主题客户端发布消息时需要为消息指定一个“主题”,表示将消息发布到该主题;而对于订阅消息的客户端 来说,可通过订阅“主题”来订阅消息,这样当其它客户端或自己(当前客户端)向该主题发布消息时,MQTT 服务端就会将该主题的信息发送给该主题的订阅者(客户端)

4.6 MQTT客户端工具

MQTT协议本身并没有定义后台管理相关协议,但是开源的MQTT客户端非常之多,这使得监测数据非常容易。这里推荐八款常用的MQTT客户端工具:

  • MQTT X: https://github.com/emqx/MQTTX/releases
  • Mosquitto CLI:https://mosquitto.org/
  • MQTT.fx:https://mqttfx.jensd.de/index.php/download
  • MQTT Explorerhttps://mqtt-explorer.com
  • MQTT Box:https://workswithweb.com/mqttbox.html
  • mqtt-spy:https://github.com/eclipse/paho.mqtt-spy/releases
  • MQTT Lens:https://chrome.google.com/webstore/detail/mqttlens/
  • MQTT WebSocket Toolkit:https://github.com/emqx/MQTT-Web-Toolkit

4.7 编译MQTT动态库

通过vcpkg命令【vcpkg search mqtt】搜索mqtt库发现mqtt的库还是比较多的,我们代码示例使用的是paho-mqtttpp3这个库,主要是这个库版本维护较为活跃,接口文档、示例代码都比较齐全。
在这里插入图片描述
通过vcpkg编译paho-mqtttpp3库,使用命令【vcpkg install paho-mqttpp3[ssl]:x64-windows --debug】
在这里插入图片描述

4.8 MQTT服务器

MQTT协议是透明的,服务端落地的实现有很多,ActiveMQ和RabbitMQ同样实现了MQTT协议的支持,常见的有下面几种:

4.9 MQTT使用示例

本章节的MQTT服务端使用的RabbitMQ消息中间件,主要是有现成的环境懒得搭专门的MQTT服务器,毕竟MQTT协议是一样的,至于以何种方式落地这个不是那么重要。

4.9.1 MQTT生产者代码示例

  • SampleMemPersistence.h

    #pragma once
    #include "mqtt/iclient_persistence.h"
    
    class SampleMemPersistence : public virtual mqtt::iclient_persistence
    {
    public:
    	SampleMemPersistence();
    	~SampleMemPersistence() = default;
    
    	// "Open" the store
    	void open(const std::string& clientId, const std::string& serverURI) override;
    	// Close the persistent store that was previously opened.
    	void close() override;
    	// Clears persistence, so that it no longer contains any persisted data.
    	void clear() override;
    	// Returns whether or not data is persisted using the specified key.
    	bool contains_key(const std::string &key) override;
    	// Returns the keys in this persistent data store.
    	mqtt::string_collection keys() const override;
    	// Puts the specified data into the persistent store.
    	void put(const std::string& key, const std::vector<mqtt::string_view>& bufs) override;
    	// Gets the specified data out of the persistent store.
    	std::string get(const std::string& key) const override;
    	// Remove the data for the specified key.
    	void remove(const std::string &key) override;
    	
    private:
    	// Whether the store is open
    	bool open_;
    
    	// Use an STL map to store shared persistence pointers
    	// against string keys.
    	std::map<std::string, std::string> store_;
    };
    
  • SampleMemPersistence.cpp

    #include "SampleMemPersistence.h"
    #include "mqtt\exception.h"
    #include "mqtt\buffer_view.h"
    
    SampleMemPersistence::SampleMemPersistence() : 
    	open_(false)
    {
    }
    
    void SampleMemPersistence::open(const std::string& clientId, const std::string& serverURI) 
    {
    	std::cout << "[Opening persistence store for '" << clientId << "' at '" << serverURI << "']" << std::endl;
    	open_ = true;
    }
    
    void SampleMemPersistence::close() 
    {
    	std::cout << "[Closing persistence store.]" << std::endl;
    	open_ = false;
    }
    
    void SampleMemPersistence::clear()  
    {
    	//std::cout << "[Clearing persistence store.]" << std::endl;
    	store_.clear();
    }
    
    bool SampleMemPersistence::contains_key(const std::string &key)  
    {
    	return store_.find(key) != store_.end();
    }
    
    mqtt::string_collection SampleMemPersistence::keys() const  
    {
    	static mqtt::string_collection ks;
    	ks.clear();
    	for (const auto& k : store_)
    	{
    		ks.push_back(k.first);
    	}
    
    	return ks;
    }
    
    void SampleMemPersistence::put(const std::string& key, const std::vector<mqtt::string_view>& bufs) 
    {
    	//std::cout << "[Persisting data with key '" << key << "']" << std::endl;
    	std::string str;
    	for (const auto& b : bufs)
    	{
    		str += b.str();
    	}
    
    	store_[key] = std::move(str);
    }
    
    std::string SampleMemPersistence::get(const std::string& key) const
    {
    	//std::cout << "[Searching persistence for key '" << key << "']" << std::endl;
    	auto p = store_.find(key);
    	if (p == store_.end())
    	{
    		throw mqtt::persistence_exception();
    	}
    		
    	//std::cout << "[Found persistence data for key '" << key << "']" << std::endl;
    
    	return p->second;
    }
    
    void SampleMemPersistence::remove(const std::string &key) 
    {
    	//std::cout << "[Persistence removing key '" << key << "']" << std::endl;
    	auto p = store_.find(key);
    	if (p == store_.end())
    	{
    		throw mqtt::persistence_exception();
    	}
    
    	store_.erase(p);
    	//std::cout << "[Persistence key removed '" << key << "']" << std::endl;
    }
    
  • MqttEventListener.h

    #pragma once
    #include "mqtt/callback.h"
    
    using namespace mqtt;
    
    class MqttEventListener : public virtual mqtt::callback
    {
    public:
    	MqttEventListener() = default;
    	~MqttEventListener() = default;
    
    public:
    	//客户端与服务端连接成功的时候调用此方法
    	virtual void connected(const string& cause);
    
    	//客户端与服务端连接丢失时调用此方法
    	virtual void connection_lost(const string& cause);
    
    	//当消息从服务器到达时调用此方法
    	virtual void message_arrived(const_message_ptr msg);
    
    	//当消息传递完成并且已确认收到是调用
    	virtual void delivery_complete(delivery_token_ptr tok);
    };
    
  • MqttEventListener.cpp

    #include "MqttEventListener.h"
    
    
    void MqttEventListener::connected(const string& cause)
    {
    	std::cout << "Connection success" << std::endl;
    	if (!cause.empty())
    	{
    		std::cout << "\tcause: " << cause << std::endl;
    	}
    }
    
    void MqttEventListener::message_arrived(const_message_ptr msg)
    {
    	std::cout << "Message arrived: " << msg->get_payload_str() << std::endl;
    }
    
    void MqttEventListener::connection_lost(const std::string& cause)
    {
    	std::cout << "Connection lost" << std::endl;
    	if (!cause.empty())
    	{
    		std::cout << "\tcause: " << cause << std::endl;
    	}
    }
    
    void MqttEventListener::delivery_complete(mqtt::delivery_token_ptr tok)
    {
    	//std::cout << "[Delivery complete for token: " << (tok ? tok->get_message_id() : -1) << "]" << std::endl;
    }
    
  • Main.cpp

    #include <iostream>
    #include "MQTTClientPersistence.h"
    #include "MQTTClient.h"
    #include "MQTTProperties.h"
    #include "mqtt/client.h"
    #include "MqttEventListener.h"
    #include "SampleMemPersistence.h"
    #include <sstream>
    
    using namespace mqtt;
    using namespace std;
    
    const std::string SERVER_ADDRESS = "tcp://10.19.131.94:1883";
    const std::string USER_NAME = "root";
    const std::string PASSWORD = "GOivuGph";
    const std::string CLIENT_ID = "sync_publish_cpp";
    const std::string PAYLOAD1 = "Hello MQTT, My name is C++ MQTT-Client!";
    const char* PAYLOAD2 = "Hello MQTT,nice to meet you!";
    const char* PAYLOAD3 = "Hello MQTT,let's be good friends!";
    const std::string TOPIC = "mqtt/cpp/test";
    
    const int QOS = 1;
    
    int main(int argc, char* argv[])
    {
    	std::cout << "Initialzing..." << std::endl;
    	SampleMemPersistence persist;
    	mqtt::client client(SERVER_ADDRESS, CLIENT_ID, &persist);
    
    	MqttEventListener cb;
    	client.set_callback(cb);
    
    	mqtt::connect_options connOpts;
    	connOpts.set_mqtt_version(MQTTVERSION_3_1_1);
    	connOpts.set_keep_alive_interval(20);
    	connOpts.set_clean_session(true);
    	connOpts.set_user_name(USER_NAME);
    	connOpts.set_password(PASSWORD);
    
    	try {
    		//连接到MQTT服务器
    		std::cout << "Connecting..." << std::endl;
    		mqtt::connect_response response = client.connect(connOpts);
    		std::cout << "...OK" << std::endl;
    
    		//使用消息指针发布消息
    		std::cout << "Sending message..." << std::endl;
    		auto pubmsg = mqtt::make_message(TOPIC, PAYLOAD1);
    		pubmsg->set_qos(QOS);
    		client.publish(pubmsg);
    		std::cout << "...OK" << std::endl;
    
    		//使用直接发布消息的方式
    		std::cout << "Sending next message..." << std::endl;
    		client.publish(TOPIC, PAYLOAD2, strlen(PAYLOAD2) + 1);
    		std::cout << "...OK" << std::endl;
    
    		//使用侦听器、无令牌和非堆消息方式
    		std::cout << "Sending final message..." << std::endl;
    		client.publish(mqtt::message(TOPIC, PAYLOAD3, QOS, false));
    		std::cout << "OK" << std::endl;
    
    		for (int i = 0; i < 1000; i++)
    		{
    			std::stringstream ss;
    			ss << PAYLOAD3 << " count = " << i;
    			std::cout << "sending"<< "[" << TOPIC << "]" << "..." << ss.str() << std::endl;
    			auto cntmsg = mqtt::make_message(TOPIC, ss.str());
    			cntmsg->set_qos(QOS);
    			client.publish(cntmsg);
    			std::this_thread::sleep_for(std::chrono::milliseconds(100));
    		}
    
    		//断开连接
    		std::cout << "Disconnecting..." << std::endl;
    		client.disconnect();
    		std::cout << "...OK" << std::endl;
    	}
    	catch (const mqtt::persistence_exception& exc) 
    	{
    		std::cerr << "Persistence Error: " << exc.what() << " [" << exc.get_reason_code() << "]" << std::endl;
    		return 1;
    	}
    	catch (const mqtt::exception& exc) 
    	{
    		std::cerr << exc.what() << std::endl;
    		return 1;
    	}
    
    	std::cout << "MQTT goodbye!" << std::endl;
    
    	return 0;
    }
    

4.9.2 MQTT消费者代码示例

  • MqttEventListener.h

    #pragma once
    #include "mqtt/callback.h"
    
    using namespace mqtt;
    
    class MqttEventListener : public virtual mqtt::callback
    {
    public:
    	MqttEventListener() = default;
    	~MqttEventListener() = default;
    
    public:
    	//客户端与服务端连接成功的时候调用此方法
    	virtual void connected(const string& cause);
    
    	//客户端与服务端连接丢失时调用此方法
    	virtual void connection_lost(const string& cause);
    
    	//当消息从服务器到达时调用此方法
    	virtual void message_arrived(const_message_ptr msg);
    
    	//当消息传递完成并且已确认收到是调用
    	virtual void delivery_complete(delivery_token_ptr tok);
    };
    
  • MqttEventListener.cpp

    #include "MqttEventListener.h"
    
    
    void MqttEventListener::connected(const string& cause)
    {
    	std::cout << "Connection success" << std::endl;
    	if (!cause.empty())
    	{
    		std::cout << "\tcause: " << cause << std::endl;
    	}
    }
    
    void MqttEventListener::message_arrived(const_message_ptr msg)
    {
    	std::cout << "Message arrived: " << msg->get_payload_str() << std::endl;
    }
    
    void MqttEventListener::connection_lost(const std::string& cause)
    {
    	std::cout << "Connection lost" << std::endl;
    	if (!cause.empty())
    	{
    		std::cout << "\tcause: " << cause << std::endl;
    	}
    }
    
    void MqttEventListener::delivery_complete(mqtt::delivery_token_ptr tok)
    {
    	std::cout << "[Delivery complete for token: " << (tok ? tok->get_message_id() : -1) << "]" << std::endl;
    }
    
  • Main.cpp

    #include <iostream>
    #include <cstdlib>
    #include <string>
    #include <cstring>
    #include <cctype>
    #include <thread>
    #include <chrono>
    #include <map>
    #include <functional>
    #include "mqtt/client.h"
    
    using namespace std;
    using namespace std::chrono;
    
    const string SERVER_ADDRESS{ "tcp://10.19.131.94:1883" };
    const string CLIENT_ID{ "paho_cpp_sync_consume5" };
    const string USER_NAME{ "root" };
    const string PASSWORD{ "GOivuGph" };
    const string TOPIC{ "mqtt/cpp/test" };
    
    constexpr int QOS_0 = 0;
    constexpr int QOS_1 = 1;
    
    // Message table function signature
    using handler_t = std::function<bool(const mqtt::message&)>;
    
    // Handler for data messages (i.e. topic "mqtt/cpp/test")
    bool data_handler(const mqtt::message& msg)
    {
    	cout << msg.get_topic() << ": " << msg.to_string() << endl;
    	return true;
    }
    
    // Handler for command messages (i.e. topic "command")
    // Return false to exit the application
    bool command_handler(const mqtt::message& msg)
    {
    	if (msg.to_string() == "exit") 
    	{
    		cout << "Exit command received" << endl;
    		return false;
    	}
    
    	return true;
    }
    
    int main(int argc, char* argv[])
    {
    	mqtt::client cli(SERVER_ADDRESS, CLIENT_ID, mqtt::create_options(MQTTVERSION_3_1_1));
    	auto connOpts = mqtt::connect_options_builder()
    		.mqtt_version(MQTTVERSION_3_1_1)
    		.automatic_reconnect(seconds(2), seconds(30))
    		.clean_session(true)
    		.user_name(USER_NAME)
    		.password(PASSWORD)
    		.finalize();
    
    	//根据订阅ID处理传入消息的调度表
    	std::map<std::string, handler_t> handler;
    	handler["mqtt/cpp/test"] = data_handler;
    	handler["command"] = command_handler;
    	try {
    		cout << "Connecting to the MQTT server..." << flush;
    		mqtt::connect_response rsp = cli.connect(connOpts);
    		cout << "OK\n" << endl;
    
    		if (!rsp.is_session_present()) {
    			std::cout << "Subscribing to topics..." << std::flush;
    
    			mqtt::subscribe_options subOpts;
    			mqtt::properties props1{
    				{ mqtt::property::SUBSCRIPTION_IDENTIFIER, 1 },
    			};
    			cli.subscribe(TOPIC, QOS_0, subOpts, props1);
    
    			//mqtt::properties props2{
    			//	{ mqtt::property::SUBSCRIPTION_IDENTIFIER, 2 },
    			//};
    			//cli.subscribe("command", QOS_1, subOpts, props2);
    
    			std::cout << "OK" << std::endl;
    		}
    		else 
    		{
    			cout << "Session already present. Skipping subscribe." << std::endl;
    		}
    
    		// Consume messages
    		while (true) 
    		{
    			auto msg = cli.consume_message();
    			//在一个真正的应用程序中,你需要做更多的错误和边界检查
    			if (msg) 
    			{
    				//从传入消息中获取topic
    				std::string topic = msg->get_topic();
    
    				//根据订阅ID分派到处理程序函数
    				if (!(handler[topic])(*msg))
    					break;
    			}
    			else if (!cli.is_connected()) 
    			{
    				cout << "Lost connection" << endl;
    				while (!cli.is_connected()) 
    				{
    					this_thread::sleep_for(milliseconds(250));
    				}
    				cout << "Re-established connection" << endl;
    			}
    		}
    
    		// 和服务器断开连接
    		cout << "Disconnecting from the MQTT server..." << flush;
    		cli.disconnect();
    		cout << "OK" << endl;
    	}
    	catch (const mqtt::exception& exc) 
    	{
    		cerr << exc.what() << endl;
    		return 1;
    	}
    
    	return 0;
    }
    

5.消息中间件对比

消息中间件说明
ActiveMQ历史悠久的开源项目,已经在很多产品中得到应用,实现了JMS1.1规范,可以和spring-jms轻松融合,实现了多种协议,不够轻巧(源代码比RocketMQ多),支持持久化到数据库,对队列数较多的情况支持不好。
Kafkakafka设计的初衷就是处理日志的,不支持AMQP事务处理,可以看做是一个日志系统,针对性很强,所以它并没有具备一个成熟MQ应该具有的特性,kafka的性能(吞吐量、tps)比RabbitMQ要强,如果用来做大数据量的快速处理是比RabbitMQ有优势的。
RabbitMQ支持多种协议,对路由、负载均衡、数据持久化都有比较好的支持。它比kafka成熟,支持AMQP事务处理,在可靠性、稳定性上,RabbitMQ超过kafka,在性能方面超过ActiveMQ
MQTTMQTT 协议是为工作在低带宽、不可靠网络的远程传感器和控制设备之间的通讯而设计的协议。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

奥修的灵魂

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

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

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

打赏作者

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

抵扣说明:

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

余额充值