【视频点播系统】AMQP-SDK 介绍及使用

AMQP-SDK 介绍及使用

  • [一. AMQP 协议介绍](#一. AMQP 协议介绍)
    • [1. 消息队列核心要素](#1. 消息队列核心要素)
    • [2. 消息队列工作流程](#2. 消息队列工作流程)
  • [二. RabbitMQ AMQP 安装](#二. RabbitMQ AMQP 安装)
  • [三. AMQP-CPP SDK 介绍](#三. AMQP-CPP SDK 介绍)
  • [四. AMQP-CPP SDK 安装](#四. AMQP-CPP SDK 安装)
  • [五. AMQP-CPP 类与接口](#五. AMQP-CPP 类与接口)
    • [1. 默认 TCP 模式](#1. 默认 TCP 模式)
    • [2. 扩展模式](#2. 扩展模式)
      • [2.1 Connection、Channel](#2.1 Connection、Channel)
      • [2.2 Reliable、Message](#2.2 Reliable、Message)
      • [2.3 Deferred 派生系列](#2.3 Deferred 派生系列)
    • [3. 扩展网络库 libev](#3. 扩展网络库 libev)
      • [3.1 LibEvHandler](#3.1 LibEvHandler)
      • [3.2 ev_loop](#3.2 ev_loop)
  • [六. AMQP-CPP 使用样例](#六. AMQP-CPP 使用样例)
    • [1. 目录结构](#1. 目录结构)
    • [2. 项目构建](#2. 项目构建)
    • [3. 代码实现](#3. 代码实现)
      • [1.1 简单队列消息订阅与发布](#1.1 简单队列消息订阅与发布)
      • [1.2 死信队列](#1.2 死信队列)
  • [七. AMQP-CPP 封装](#七. AMQP-CPP 封装)
    • [1. 设计与实现](#1. 设计与实现)
      • [1.1 目录结构](#1.1 目录结构)
      • [1.2 代码实现](#1.2 代码实现)
    • [2. 使用样例](#2. 使用样例)
      • [1.1 目录结构](#1.1 目录结构)
      • [1.2 项目构建](#1.2 项目构建)
      • [1.3 代码实现](#1.3 代码实现)
        • [1.3.1 简单队列消息的订阅与发布](#1.3.1 简单队列消息的订阅与发布)
        • [1.3.1 延时队列消息的订阅与发布](#1.3.1 延时队列消息的订阅与发布)

一. AMQP 协议介绍

  • AMQP 即 Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang 中的实现有 RabbitMQ 等。
  • AMQP 规定了消息系统中三大组件:消息服务器/代理节点 (server/broker)、生产者/发布者 (producer/publisher)、消费者/订阅者 (consumer/subscriber) 之间的通信规范,以及代理节点的设计规范等。

1. 消息队列核心要素

  • Broker:消息代理服务器 (比如 RabbitMQ)
  • 虚拟机 (Virtual Host):虚拟主机,隔离环境。
  • 交换机 (Exchange) :交换机是消息的分发中心,它接收生产者发送的消息并根据一定的规则将消息路由到一个或多个队列中。比较典型的交换机类型,包括:
    • 直连交换机 (Direct Exchange):根据消息的路由键将消息发送到特定队列。
    • 主题交换机 (Topic Exchange):根据消息的路由键和通配符匹配将消息发送到多个队列。
    • 广播交换机 (Fanout Exchange):将消息广播到与交换机绑定的所有队列。
    • 头交换机 (Headers Exchange):根据消息的自定义头部属性进行匹配路由。
  • 队列 (Queue):队列是消息的容器,它存储消息直到消费者准备好接收和处理它们。消息通过交换机路由到队列,消费者可以从队列中读取消息。每个队列都有一个名称,它们可以绑定到一个或多个交换机,并指定了消息的路由规则。
  • Binding-Key:Binding-Key是交换机和队列之间的绑定键,用于定义消息的路由规则。在直连交换机中,Binding-Key 通常与队列的路由键一致,在主题交换机中,Binding-Key 可以使用通配符进行匹配。
  • 生产者 (Producer):生产者是消息的发送方。它们创建消息并将其发布到 RabbitMQ 的交换机上。生产者通常将消息发送到一个或多个队列,以便消费者可以订阅并处理这些消息。
  • 消费者 (Consumer):消费者是消息的接收方,它们订阅队列并从中获取消息。一旦消费者接收到消息,它们可以对消息进行处理,例如执行某些任务或将数据存储到数据库中。

2. 消息队列工作流程

消息的传输流程

  1. 生产者连接 Broker 进行发布消息,并指定交换机和路由键。
  2. 交换机根据路由键将消息路由到一个或多个队列。
  3. 消费者从队列中获取消息并处理它们。

作用

  • 解耦:消息队列可以将消息的生产者和消费者解耦,它们不需要同时在线或直接交互,只需要通过队列进行通信。
  • 异步处理:消息队列允许异步处理任务,这意味着生产者可以发送消息而不必等待消费者处理消息的结果,从而提高性能和吞吐量。
  • 削峰填谷:将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。
  • 负载均衡:在多个消费者的情况下,消息队列可以自动分配消息到不同的消费者,实现负载均衡。
  • 灵活性和可扩展性:消息队列支持水平扩展,可以根据需要添加更多的消费者来处理消息。
  • 增强系统的可维护性:通过将消息传递机制与业务逻辑分离,系统的不同部分可以独立开发和维护。
  • 容错性:消费者处理消息失败时,消息队列可以重新路由消息,或者将其放入死信队列以便后续处理。
  • 支持复杂的业务流程:消息队列可以支持复杂的业务流程,如工作流和任务调度。
  • 数据分发:消息队列可以用于数据分发,将数据从一个地方传输到另一个地方,特别是在分布式系统中。
  • 日志记录:消息队列可以用于日志记录,收集来自不同源的日志信息,然后进行集中处理。
  • 跨语言和平台:消息队列通常支持多种编程语言和平台,使得不同技术栈的系统可以轻松集成。

二. RabbitMQ AMQP 安装

bash 复制代码
# 更新软件源
sudo apt-get update
# 安装 RabbitMQ 依赖 Erlang
sudo apt-get install -y erlang
# 安装 RabbitMQ AMQP 协议服务器
sudo apt-get install -y rabbitmq-server
# 启动 RabbitMQ 服务
sudo systemctl start rabbitmq-server
# 设置 RabbitMQ 开机自启
sudo systemctl enable rabbitmq-server

由于在环境搭建中已经安装了 RabbitMQ AMQP 服务器,并且启动了,但是需要开启网页访问,也就是 进入 RabbitMQ 容器启用管理插件


密码位于 docker-compose-yml 配置文件中,如下:


网页访问:


三. AMQP-CPP SDK 介绍

  • AMQP-CPP 是一个用 现代 C++ 编写的 AMQP 协议客户端库,主要用于与 RabbitMQ 等消息中间件进行通信。它遵循 AMQP 0-9-1 和部分 AMQP 1.0 规范,提供了高性能、轻量级的消息发布与订阅能力,适合对性能和资源占用要求较高的服务端程序。
  • AMQP-CPP 采用 事件驱动、非阻塞 I/O 的设计,不直接处理网络连接,而是将 socket 读写交给用户或第三方网络库(如 libevent、libev、asio 等),从而具备良好的可移植性和灵活性。开发者可以根据自身项目选择合适的事件循环模型。
  • 该库支持交换机、队列、绑定、确认机制、事务、消息确认(ack/nack)等 RabbitMQ 核心功能,并且接口设计简洁,易于与 C++ 项目集成。AMQP-CPP 常被用于 微服务通信、异步任务处理、日志收集和解耦系统架构 等场景,是 C++ 生态中使用 RabbitMQ 的主流解决方案之一。

四. AMQP-CPP SDK 安装

AMQP-CPP 的项目地址为:https://github.com/CopernicaMarketingSoftware/AMQP-CPP

bash 复制代码
# 安装依赖
sudo apt-get update
sudo apt-get install -y build-essential cmake libev-dev libssl-dev
# 下载 AMQP-CPP
git clone https://github.com/CopernicaMarketingSoftware/AMQP-CPP.git
cd AMQP-CPP
# 创建并进入 build 目录
mkdir build && cd build
# 生成 Makefile
cmake ..
# 编译
make -j$(nproc)
# 安装到系统(默认 /usr/local)
sudo make install

由于在环境搭建中已经安装了 AMQP-CPP,这里不需要再次安装了。

五. AMQP-CPP 类与接口

AMQP-CPP 的使用有两种模式:

  • 使用默认的 TCP 模块进网络通信。
  • 使用扩展的 libevent、libev、libuv、asio 异步网络通信组件进行通信。

1. 默认 TCP 模式

  • 实现一个类继承 AMQP::TcpHandler 类,它负责网络层的 TCP 连接。
  • 重写相关函数,其中必须重写 monitor 函数。
  • 在 monitor 函数中需要实现的是将 fd 放入 eventloop(select, epoll) 中监控,当 fd 可写可读就绪之后,调用 AMQP-CPP 的 connection->process(fd, flags) 方法。
cpp 复制代码
// 自定义TCP处理类:用于处理AMQP连接的TCP层事件
class MyTcpHandler : public AMQP::TcpHandler {
    // 连接成功回调:当TCP连接成功建立时调用
    virtual void onAttached(AMQP::TcpConnection *connection) override; 
    // 连接完成回调:当TCP连接完成握手时调用
    virtual void onConnected(AMQP::TcpConnection *connection) override; 
    // 连接安全回调:当连接升级为安全连接时调用
    virtual bool onSecured(AMQP::TcpConnection *connection, const SSL *ssl) override; 
    // 连接就绪回调:当AMQP连接完成初始化时调用
    virtual void onReady(AMQP::TcpConnection *connection) override; 
    // 协商回调:用于协商TCP连接的AMQP协议版本
    virtual uint16_t onNegotiate(AMQP::TcpConnection *connection, uint16_t interval) override; 
    // 错误回调:当连接发生错误时调用
    virtual void onError(AMQP::TcpConnection *connection, const char *message) override; 
    // 连接关闭回调:当连接被正常关闭时调用
    virtual void onClosed(AMQP::TcpConnection *connection) override; 
    // 连接丢失回调:当连接丢失时调用
    virtual void onLost(AMQP::TcpConnection *connection) override; 
    // 连接分离回调:当连接被分离时调用
    virtual void onDetached(AMQP::TcpConnection *connection) override; 
    // 监控回调:用于监控TCP连接的事件
    virtual void monitor(AMQP::TcpConnection *connection, int fd, int flags) override; 
};

2. 扩展模式

以 libev 为例,我们不必要自己实现 monitor 函数,可以直接使用 AMQP::LibEvHandler

2.1 Connection、Channel

Connection 用于设定 rabbitmq 服务器地址后,创建一个通信连接对象。

cpp 复制代码
namespace AMQP {
    class LibEvHandler : public TcpHandler;
    class ConnectionHandler;
    // 地址类:用于表示 AMQP 服务器的地址
    class Address {
        Address(const std::string &address);
    }
    // TCP 连接类:用于建立与 AMQP 服务器的 TCP 连接
    class TcpConnection : private ConnectionHandler {
        TcpConnection(TcpHandler *handler, const Address &address);
    };
    // 登录类:用于登录 AMQP 服务器
    class Login {
        Login(std::string user, std::string password);
    };
    // 连接类:用于建立与 AMQP 服务器的逻辑连接
    class Connection {
        Connection(ConnectionHandler *handler, const Login &login, const std::string &vhost);
        bool close(); // 关闭连接
    };
}

这些回调函数使得 AMQP-CPP 库能够在消息发布后提供反馈,帮助应用程序实现更可靠的消息传递机制。通过实现这些回调,开发者可以对消息的发布结果进行适当的处理,确保消息传递的可靠性或进行错误恢复。

cpp 复制代码
extern const int durable; // 消息是否持久化
extern const int autodelete; // 队列是否自动删除
extern const int active; // 队列是否激活
extern const int passive; // 队列是否被动创建
extern const int ifunused;  // 队列是否未被使用
extern const int ifempty;  // 队列是否为空
extern const int global;  // 队列是否全局唯一
extern const int nolocal;  // 队列是否不允许本地消费
extern const int noack;  // 消息是否不确认
extern const int exclusive;  // 队列是否排他性
extern const int nowait;  // 是否不等待服务器响应
extern const int mandatory;  // 是否强制要求消息被路由到队列
extern const int immediate;  // 是否立即路由消息
extern const int redelivered;  // 是否重新投递消息
extern const int multiple;  // 是否批量确认消息
extern const int requeue;  // 是否重新入队消息
extern const int readable;  // 队列是否可读
extern const int writable;  // 队列是否可写
extern const int internal;  // 队列是否内部队列

namespace AMQP {
    // 成功回调:在连接或通道操作成功时调用
    using SuccessCallback = std::function<void()>; 
    // 错误回调:在连接或通道操作失败时调用
    using ErrorCallback = std::function<void(const char *message)>; 
    // 最终回调:在连接关闭时调用
    using FinalizeCallback = std::function<void()>; 
    // 队列回调:在队列声明成功时调用
    using QueueCallback = std::function<void(const std::string &name, uint32_t messagecount, uint32_t consumercount)>;
    // 删除回调:在队列删除成功时调用
    using DeleteCallback = std::function<void(uint32_t deletedmessages)>;
    // 消息回调:在消息接收时调用
    using MessageCallback = std::function<void(const Message &message, uint64_t deliveryTag, bool redelivered)>;
    // 确认回调:在消息确认时调用
    using AckCallback = std::function<void(uint64_t deliveryTag, bool multiple)>;
    // 发布确认回调:在消息发布确认时调用
    using PublishAckCallback = std::function<void()>;
    // 发布拒绝回调:在消息发布拒绝时调用
    using PublishNackCallback = std::function<void()>;
    // 发布丢失回调:在消息发布丢失时调用
    using PublishLostCallback = std::function<void()>;

    // 交换机类型
    enum ExchangeType {
        fanout, // 广播交换机:将消息发送到所有绑定的队列
        direct, // 直接交换机:根据路由键将消息发送到指定队列
        topic, // 主题交换机:根据路由键和模式将消息发送到指定队列
        headers, // 头交换机:根据消息头将消息发送到指定队列
        consistent_hash, // 一致性哈希交换机:根据路由键和哈希算法将消息发送到指定队列
        message_deduplication // 消息去重交换机:根据消息属性将重复消息丢弃
    };
}

Channel 是一个虚拟连接,一个连接上可以建立多个通道。并且所有的 RabbitMQ 指令都是通过 Channel 传输,所以连接建立后的第一步,就是建立 Channel。因为所有操作是异步的,所以在 Channel 上执行指令的返回值并不能作为操作执行结果,实际上它返回的是 Deferred 类,可以使用它安装处理函数。

cpp 复制代码
namespace AMQP {
    // 通道类,用于与AMQP服务器进行通信
    class Channel {
        Channel(Connection *connection);
        bool connected(); // 是否连接可用
        Deferred &close(); // 关闭当前channel 
        DeferredConfirm &confirmSelect(); // 确认模式选择
        Deferred &startTransaction(); // 开始事务
        Deferred &commitTransaction(); // 提交事务
        Deferred &rollbackTransaction(); // 回滚事务
        // 声明交换机:创建一个交换机,用于路由消息到队列
        Deferred &declareExchange(const std::string_view &name, ExchangeType type, int flags); 
        // 声明队列:创建一个队列,用于存储消息
        DeferredQueue &declareQueue(const std::string_view &name, int flags); 
        // 将队列绑定到交换机:将队列绑定到交换机,通过绑定键指定路由规则
        Deferred &bindQueue(const std::string_view &exchange, const std::string_view &queue, const std::string_view &bindingkey); 
        // 解除队列绑定:通过绑定键指定路由规则解除队列绑定
        Deferred &unbindQueue(const std::string_view &exchange, const std::string_view &queue, const std::string_view &bindingkey); 
        // 删除队列:删除队列,队列中的消息将被丢弃
        DeferredDelete &removeQueue(const std::string_view &name, int flags = 0); 
        // 发布消息:将消息发布到交换机,通过路由键指定队列
        bool publish(const std::string_view &exchange, const std::string_view &routingKey, const std::string &message, int flags = 0);
        // 重新投递消息:重新投递消息到交换机,通过路由键指定队列
        DeferredRecall &recall(uint64_t deliveryTag, int flags = 0); 
        // 消费消息:从队列中获取消息,通过标签指定消费者
        DeferredConsumer &consume(const std::string_view &queue, const std::string_view &tag, int flags = 0); 
        // 取消消费:取消指定标签的消费者,使服务器停止发送消息给该消费者
        DeferredCancel &cancel(const std::string_view &tag); 
        // 获取消息:从队列中获取一条消息
        DeferredGet &get(const std::string_view &queue, int flags = 0); 
        // 确认消息:确认已处理的消息,使服务器将其从队列中删除
        bool ack(uint64_t deliveryTag, int flags=0); 
        // 拒绝消息:拒绝已处理的消息,使服务器将其重新投递到队列或发送到死信队列
        bool reject(uint64_t deliveryTag, int flags=0);
        // 恢复未确认消息:消费者重新发送未确认的消息给服务器
        Deferred &recover(int flags = 0);
    };
}

2.2 Reliable、Message

Reliable 用于可靠的消息发布及错误处理。Message 用于订阅者收到消息后的处理。

cpp 复制代码
namespace AMQP {
    template <typename BASE=Tagger>
    // 可靠发布类:确保消息可靠发布
    class Reliable : public BASE {
        template <typename ...Args>
        Reliable(Args &&...args);
        // 发布消息到指定的交换机和路由键
        DeferredPublish &publish(const std::string_view &exchange, const std::string_view &routingKey, const std::string_view &message, int flags = 0)
    };

    // 信封类:封装消息的元数据和体
    class Envelope : public MetaData  {
        const char *body(); // 消息体指针
        uint64_t bodySize(); // 消息体大小
    };
    // 消息类:继承自信封类,添加消息的具体实现
    class Message : public Envelope  {
        const std::string &exchange(); // 交换机名称
        const std::string &routingkey(); // 路由键
    };
}

// 示例:发布可靠消息
AMQP::TcpChannel mychannel(connection);
AMQP::Reliable reliable(mychannel);
reliable.publish("my-exchange", "my-key", "my first message")
    .onAck()
    .onNack()
    .onLost()
    .onError();

2.3 Deferred 派生系列

Deferred 类 (包括其派生类,如 DeferredQueue、DeferredConsumer 等) 是处理异步操作的核心机制。它用于表示一个尚未完成的操作,用于设置异步调用的回调函数,进行接口的连续调用。

cpp 复制代码
namespace AMQP {
    // 延迟对象:用于处理异步操作的回调
    class Deferred {
        Deferred &onSuccess(const SuccessCallback& callback); // 当操作成功完成时调用
        Deferred &onError(const ErrorCallback& callback); // 当操作遇到错误时调用
        Deferred &onFinalize(const FinalizeCallback& callback); // 无论操作成功还是失败,都会调用
    };
    // 队列操作相关的延迟对象:用于处理队列操作的异步回调
    class DeferredQueue : public Deferred {
        using QueueCallback = std::function<void(const std::string &name, uint32_t messageCount, uint32_t consumerCount)>;
        DeferredQueue &onSuccess(const QueueCallback& callback); // 当队列操作成功完成时调用
        DeferredQueue &onSuccess(const SuccessCallback& callback); // 当队列操作成功完成时调用
    };
    // 消费者操作相关的延迟对象:用于处理消费者操作的异步回调
    class DeferredConsumer : public Deferred {
        using ConsumeCallback = std::function<void(const std::string_view &tag)>;
        using MessageCallback = std::function<void (const AMQP::Message &message, uint64_t deliveryTag, bool redelivered)>;
        DeferredConsumer &onSuccess(const ConsumeCallback& callback); // 当消费者操作成功完成时调用
        DeferredConsumer &onReceived(const MessageCallback& callback); // 当消费者接收到消息时调用
        DeferredConsumer &onMessage(const MessageCallback& callback); // 当消费者处理消息时调用
        DeferredConsumer &onCancelled(const CancelCallback& callback); // 当消费者被取消时调用
    };
}

3. 扩展网络库 libev

3.1 LibEvHandler

LibEvHandler 是 amqpcpp 库与 libev 网络库的对接类。

cpp 复制代码
// 基于libev的TCP处理句柄
class LibEvHandler : public TcpHandler {
    LibEvHandler(struct ev_loop* loop, int priority = 0)
};

3.2 ev_loop

ev_loop 是 libev 网络通信库的核心使用接口。

cpp 复制代码
// 异步任务结构体
typedef struct ev_async {
    EV_WATCHER(ev_async)
    EV_ATOMIC_T sent;
} ev_async;

// 异步任务中断类型
enum {
    EVBREAK_CANCEL = 0, // 取消所有异步任务
    EVBREAK_ONE = 1, // 取消当前异步任务
    EVBREAK_ALL = 2  // 取消所有异步任务
};

// 创建libev操作句柄
struct ev_loop* ev_default_loop(unsigned int flags EV_CPP (= 0))
# define EV_DEFAULT ev_default_loop(0)

void ev_loop_destroy(struct ev_loop* loop); // 销毁事件循环:销毁事件循环中的所有资源,包括异步任务、定时器等
int ev_run(struct ev_loop* loop); // 运行事件循环:阻塞直到事件循环结束
void ev_break(struct ev_loop* loop, int32_t break_type); // 退出事件循环:根据参数,退出事件循环的不同方式

// 异步任务回调函数,非阻塞接口
// 也就是每个外部针对ev的操作都必须放在ev_loop所在的线程内执行
// 这里相当于打包了一个任务,加入到ev_loop的任务队列中执行
void (*callback)(struct ev_loop* loop, ev_async* watcher, int32_t revents); // 异步任务回调函数
void ev_async_init(ev_async *w, callback cb); // 初始化异步任务:异步任务回调函数
void ev_async_start(struct ev_loop *loop, ev_async *w); // 启动异步任务:将异步任务添加到事件循环中
void ev_async_send(struct ev_loop *loop, ev_async *w); // 发送异步任务:触发异步任务的回调函数

// 示例:异步退出 
void mycb(struct ev_loop *loop, ev_async *watcher, int32_t revents) {
    ev_break(loop, EVBREAK_ALL);
};
ev_async ea;
ev_async_init(&ea, mycb);
ev_async_start(loop, &ea);
ev_async_send(loop, &ea);

六. AMQP-CPP 使用样例

1. 目录结构

bash 复制代码
example/
|-- amqpcpp
|   |-- dlx_publish.cc
|   |-- dlx_subscribe.cc
|   |-- makefile
|   |-- simple_publish.cc
|   |-- simple_subscribe.cc

2. 项目构建

bash 复制代码
# makefile
all: simple_publish simple_subscribe dlx_publish dlx_subscribe
simple_publish: simple_publish.cc
	g++ -o $@ $^ -std=c++17 -lamqpcpp -lev -lpthread
simple_subscribe: simple_subscribe.cc
	g++ -o $@ $^ -std=c++17 -lamqpcpp -lev -lpthread
dlx_publish: dlx_publish.cc
	g++ -o $@ $^ -std=c++17 -lamqpcpp -lev -lpthread
dlx_subscribe: dlx_subscribe.cc
	g++ -o $@ $^ -std=c++17 -lamqpcpp -lev -lpthread

.PHONY: clean
clean:
	rm -f simple_publish simple_subscribe dlx_publish dlx_subscribe

编译生产可执行程序:

3. 代码实现

1.1 简单队列消息订阅与发布

cpp 复制代码
// simple_subscribe.cc
#include <iostream>
#include <ev.h>
#include <amqpcpp.h>
#include <amqpcpp/libev.h>

int main()
{
    // 1.定义消息队列rabbitmq访问url、绑定键交换机、队列、绑定键
    std::string url = "amqp://admin:123456@192.168.174.128:5672/";
    std::string exchange = "my-exchange";
    std::string queue = "my-queue";
    std::string binding_key = "my-binding-key";
    // 2.实例化libev网络通信模块句柄:用于处理libev事件循环
    struct ev_loop* ev_loop = EV_DEFAULT; // 事件循环句柄:用于管理libev事件循环
    AMQP::LibEvHandler handler(ev_loop); // 网络通信模块句柄:用于处理libev事件循环
    // 3.实例化rabbitmq连接对象TcpConnection:用于与rabbitmq服务器进行通信
    AMQP::TcpConnection connection(&handler, AMQP::Address(url));
    // 4.实例化TcpChannel对象:用于与rabbitmq服务器进行通信
    AMQP::TcpChannel channel(&connection);
    // 5.声明交换机:创建一个直接交换机,用于将消息路由到队列中
    // 异步操作,需要一层套一层回调函数,处理成功或失败情况
    channel.declareExchange(exchange, AMQP::ExchangeType::direct)
        .onSuccess([&](){
            std::cout << "声明交换机成功:" << exchange << std::endl;
            // 6.声明队列:创建一个持久化队列,用于存储消息
            channel.declareQueue(queue)
                .onSuccess([&](const std::string& name, uint32_t messageCount, uint32_t consumerCount){
                    std::cout << "声明队列成功:" << queue << std::endl;
                    std::cout << "队列消息数:" << messageCount << std::endl;
                    std::cout << "队列消费者数:" << consumerCount << std::endl;
                    // 7.声明绑定队列:将交换机和队列通过绑定键绑定起来
                    channel.bindQueue(exchange, queue, binding_key)
                        .onSuccess([&](){
                            std::cout << "绑定交换机和队列成功:" << exchange << " -> " << queue << " with " << binding_key << std::endl;
                            // 8.订阅队列消息:开始从队列中接收消息
                            channel.consume(queue)
                                .onMessage([&](const AMQP::Message& message, uint64_t deliveryTag, bool redelivered){
                                    std::string body(message.body(), message.bodySize());
                                    std::cout << "收到队列消息:" << body << std::endl;
                                    // 9.确认消息已被处理:手动确认消息已被处理,否则消息会被重新投递
                                    channel.ack(deliveryTag);
                                })
                                .onSuccess([&](){
                                    std::cout << "订阅队列消息成功" << std::endl;
                                })
                                .onError([&](const char* message){
                                    std::cout << "订阅队列消息失败:" << message << std::endl;
                                });
                        })
                        .onError([&](const char* message){
                            std::cout << "绑定交换机和队列失败:" << message << std::endl;
                            abort(); // 终止程序
                        });
                })
                .onError([&](const char* message){
                    std::cout << "声明队列失败:" << message << std::endl;
                    abort(); // 终止程序
                });
        })
        .onError([&](const char* message){
            std::cout << "声明交换机失败:" << message << std::endl;
            abort(); // 终止程序
        });
    // 9.启动事件循环:开始处理网络事件,阻塞等待事件发生
    ev_run(ev_loop);

    return 0;
}
cpp 复制代码
// simple_publish.cc
#include <iostream>
#include <ev.h>
#include <amqpcpp.h>
#include <amqpcpp/libev.h>

int main()
{
    // 1.定义消息队列rabbitmq访问url、绑定键交换机、队列、绑定键
    std::string url = "amqp://admin:123456@192.168.174.128:5672/";
    std::string exchange = "my-exchange";
    std::string queue = "my-queue";
    std::string binding_key = "my-binding-key";
    // 2.实例化libev网络通信模块句柄:用于处理libev事件循环
    struct ev_loop* ev_loop = EV_DEFAULT; // 事件循环句柄:用于管理libev事件循环
    AMQP::LibEvHandler handler(ev_loop); // 网络通信模块句柄:用于处理libev事件循环
    // 3.实例化rabbitmq连接对象TcpConnection:用于与rabbitmq服务器进行通信
    AMQP::TcpConnection connection(&handler, AMQP::Address(url));
    // 4.实例化TcpChannel对象:用于与rabbitmq服务器进行通信
    AMQP::TcpChannel channel(&connection);
    // 5.声明交换机:创建一个直接交换机,用于将消息路由到队列中
    // 异步操作,需要一层套一层回调函数,处理成功或失败情况
    channel.declareExchange(exchange, AMQP::ExchangeType::direct)
        .onSuccess([&](){
            std::cout << "声明交换机成功:" << exchange << std::endl;
            // 6.声明队列:创建一个持久化队列,用于存储消息
            channel.declareQueue(queue)
                .onSuccess([&](const std::string& name, uint32_t messageCount, uint32_t consumerCount){
                    std::cout << "声明队列成功:" << queue << std::endl;
                    std::cout << "队列消息数:" << messageCount << std::endl;
                    std::cout << "队列消费者数:" << consumerCount << std::endl;
                    // 7.声明绑定队列:将交换机和队列通过绑定键绑定起来
                    channel.bindQueue(exchange, queue, binding_key)
                        .onSuccess([&](){
                            std::cout << "绑定交换机和队列成功:" << exchange << " -> " << queue << " with " << binding_key << std::endl;
                            // 8.发布消息:通过交换机结合绑定键发布消息到队列中
                            std::string message = "Hello World";
                            bool ret = channel.publish(exchange, binding_key, message);
                            if(ret == true) {
                                std::cout << "发布消息成功" << std::endl;
                            } else {
                                std::cout << "发布消息失败" << std::endl;
                            }
                        })
                        .onError([&](const char* message){
                            std::cout << "绑定交换机和队列失败:" << message << std::endl;
                            abort(); // 终止程序
                        });
                })
                .onError([&](const char* message){
                    std::cout << "声明队列失败:" << message << std::endl;
                    abort(); // 终止程序
                });
        })
        .onError([&](const char* message){
            std::cout << "声明交换机失败:" << message << std::endl;
            abort(); // 终止程序
        });
    // 9.启动事件循环:开始处理网络事件,阻塞等待事件发生
    ev_run(ev_loop);

    return 0;
}

先执行 subscribe,再执行 publish:

登录网页访问:



1.2 死信队列

  • 死信消息 (Dead Letter Message):当一个消息满足某些特定条件时 (消息拒绝,消息过期,队列满了),它就会变成 "死信消息"
  • 死信交换机 (Dead Letter Exchange):当一个消息变成死信后,RabbitMQ 会将其重新发布 (publish) 到这个特定的交换机,也就是 "死信交换机"。
  • 死信队列 (Dead Letter Queue):死信交换机将死信消息路由 (通过绑定的 Routing Key) 到这个队列进行存储,也就是 "死信队列"。应用程序可以消费这个队列中的消息来处理这些 "死信"。

使用流程:

  1. 创建普通交换机 A 和普通队列 A 并进行绑定 (作为死信交换机、死信队列存在)
  2. 创建普通交换机 B 和普通队列 B 并进行绑定 (作为常规交换机、常规队列存在),创建队列 B 的时候设置关联的死信交换机相关信息。
cpp 复制代码
// dlx_subscribe.cc
#include <iostream>
#include <ev.h>
#include <amqpcpp.h>
#include <amqpcpp/libev.h>

// 声明订阅组件:声明交换机、队列、绑定键、订阅消息
void declaredSubscribeComponent(AMQP::TcpChannel& channel, const std::string& exchange, const std::string& queue, const std::string& binding_key) {
    // 1.声明交换机:创建一个直接交换机,用于将消息路由到队列中
    channel.declareExchange(exchange, AMQP::ExchangeType::direct)
        .onSuccess([&](){
            std::cout << "声明交换机成功:" << exchange << std::endl;
            // 2.声明队列:创建一个持久化队列,用于存储消息
            channel.declareQueue(queue)
                .onSuccess([&](const std::string& name, uint32_t messageCount, uint32_t consumerCount){
                    std::cout << "声明队列成功:" << queue << std::endl;
                    std::cout << "队列消息数:" << messageCount << std::endl;
                    std::cout << "队列消费者数:" << consumerCount << std::endl;
                    // 3.声明绑定队列:将交换机和队列通过绑定键绑定起来
                    channel.bindQueue(exchange, queue, binding_key)
                        .onSuccess([&](){
                            std::cout << "绑定交换机和队列成功:" << exchange << " -> " << queue << " with " << binding_key << std::endl;
                            // 4.订阅队列消息:开始从队列中接收消息
                            channel.consume(queue)
                                .onMessage([&](const AMQP::Message& message, uint64_t deliveryTag, bool redelivered){
                                    std::string body(message.body(), message.bodySize());
                                    std::cout << "收到队列消息:" << body << std::endl;
                                    channel.ack(deliveryTag); // 确认消息已被处理:手动确认消息已被处理,否则消息会被重新投递
                                })
                                .onSuccess([&](){
                                    std::cout << "订阅队列消息成功" << std::endl;
                                })
                                .onError([&](const char* message){
                                    std::cout << "订阅队列消息失败:" << message << std::endl;
                                });
                        })
                        .onError([&](const char* message){
                            std::cout << "绑定交换机和队列失败:" << message << std::endl;
                            abort(); // 终止程序
                        });
                })
                .onError([&](const char* message){
                    std::cout << "声明队列失败:" << message << std::endl;
                    abort(); // 终止程序
                });
        })
        .onError([&](const char* message){
            std::cout << "声明交换机失败:" << message << std::endl;
            abort(); // 终止程序
        });
}

int main()
{
    // 1.定义消息队列rabbitmq访问url、连接对象、信道对象
    std::string url = "amqp://admin:123456@192.168.174.128:5672/";
    struct ev_loop* ev_loop = EV_DEFAULT;
    AMQP::LibEvHandler handler(ev_loop);
    AMQP::TcpConnection connection(&handler, AMQP::Address(url));
    AMQP::TcpChannel channel(&connection);
    // 2.创建普通交换机和队列进行绑定,作为死信交换机、队列存在
    std::string dlx_exchange = "dlx-exchange";
    std::string dlx_queue = "dlx-queue";
    std::string dlx_binding_key = "dlx-binding-key";
    declaredSubscribeComponent(channel, dlx_exchange, dlx_queue, dlx_binding_key);
    // 3.启动事件循环:开始处理网络事件,阻塞等待事件发生
    ev_run(ev_loop);

    return 0;
}
cpp 复制代码
// dlx_publish.cc
#include <iostream>
#include <ev.h>
#include <amqpcpp.h>
#include <amqpcpp/libev.h>

// 声明组件:声明交换机、队列、绑定键
void declaredComponent(AMQP::TcpChannel& channel, const std::string& exchange, const std::string& queue, const std::string& binding_key) {
    // 1.声明交换机:创建一个直接交换机,用于将消息路由到队列中
    channel.declareExchange(exchange, AMQP::ExchangeType::direct)
        .onSuccess([&](){
            std::cout << "声明交换机成功:" << exchange << std::endl;
            // 2.声明队列:创建一个持久化队列,用于存储消息
            channel.declareQueue(queue)
                .onSuccess([&](const std::string& name, uint32_t messageCount, uint32_t consumerCount){
                    std::cout << "声明队列成功:" << queue << std::endl;
                    std::cout << "队列消息数:" << messageCount << std::endl;
                    std::cout << "队列消费者数:" << consumerCount << std::endl;
                    // 3.声明绑定队列:将交换机和队列通过绑定键绑定起来
                    channel.bindQueue(exchange, queue, binding_key)
                        .onSuccess([&](){
                            std::cout << "绑定交换机和队列成功:" << exchange << " -> " << queue << " with " << binding_key << std::endl;
                        })
                        .onError([&](const char* message){
                            std::cout << "绑定交换机和队列失败:" << message << std::endl;
                            abort(); // 终止程序
                        });
                })
                .onError([&](const char* message){
                    std::cout << "声明队列失败:" << message << std::endl;
                    abort(); // 终止程序
                });
        })
        .onError([&](const char* message){
            std::cout << "声明交换机失败:" << message << std::endl;
            abort(); // 终止程序
        });
}
// 声明发布组件:声明交换机、队列、绑定键、超时时间、发布消息
void declaredPublishComponent(AMQP::TcpChannel& channel, const std::string& exchange, const std::string& queue, const std::string& binding_key, int timeout) {
    // 1.声明交换机:创建一个直接交换机,用于将消息路由到队列中
    channel.declareExchange(exchange, AMQP::ExchangeType::direct)
        .onSuccess([&](){
            std::cout << "声明交换机成功:" << exchange << std::endl;
            // 2.声明队列:创建一个持久化队列,用于存储消息,如果设置了超时时间,绑定死信交换机、路由键、消息过期时间
            AMQP::Table args;
            args["x-dead-letter-exchange"] = "dlx-exchange";
            args["x-dead-letter-routing-key"] = "dlx-binding-key";
            args["x-message-ttl"] = timeout; // 如果消息在队列中超过这个时间还没有被消费,就会被路由到死信交换机
            channel.declareQueue(queue, args)
                .onSuccess([&](const std::string& name, uint32_t messageCount, uint32_t consumerCount){
                    std::cout << "声明队列成功:" << queue << std::endl;
                    std::cout << "队列消息数:" << messageCount << std::endl;
                    std::cout << "队列消费者数:" << consumerCount << std::endl;
                    // 3.声明绑定队列:将交换机和队列通过绑定键绑定起来
                    channel.bindQueue(exchange, queue, binding_key)
                        .onSuccess([&](){
                            std::cout << "绑定交换机和队列成功:" << exchange << " -> " << queue << " with " << binding_key << std::endl;
                            // 4.发布消息:将消息发布到交换机中,路由键为绑定键
                            std::string message = "Hello World";
                            bool ret = channel.publish(exchange, binding_key, message);
                            if(ret == true) {
                                std::cout << "发布消息成功" << std::endl;
                            } else {
                                std::cout << "发布消息失败" << std::endl;
                            }
                        })
                        .onError([&](const char* message){
                            std::cout << "绑定交换机和队列失败:" << message << std::endl;
                            abort(); // 终止程序
                        });
                })
                .onError([&](const char* message){
                    std::cout << "声明队列失败:" << message << std::endl;
                    abort(); // 终止程序
                });
        })
        .onError([&](const char* message){
            std::cout << "声明交换机失败:" << message << std::endl;
            abort(); // 终止程序
        });
}

int main()
{
    // 1.定义消息队列rabbitmq访问url、连接对象、信道对象
    std::string url = "amqp://admin:123456@192.168.174.128:5672/";
    struct ev_loop* ev_loop = EV_DEFAULT;
    AMQP::LibEvHandler handler(ev_loop);
    AMQP::TcpConnection connection(&handler, AMQP::Address(url));
    AMQP::TcpChannel channel(&connection);
    // 2.创建普通交换机和队列进行绑定,作为死信交换机、队列存在
    std::string dlx_exchange = "dlx-exchange";
    std::string dlx_queue = "dlx-queue";
    std::string dlx_binding_key = "dlx-binding-key";
    declaredComponent(channel, dlx_exchange, dlx_queue, dlx_binding_key);
    // 3.创建普通交换机和队列进行绑定,同时设置参数-消息过期时间,以及关联死信队列,作为延时队列
    std::string delayed_exchange = "delayed-exchange";
    std::string delayed_queue = "delayed-queue";
    std::string delayed_binding_key = "delayed-binding-key";
    declaredPublishComponent(channel, delayed_exchange, delayed_queue, delayed_binding_key, 5000);
    // 4.启动事件循环:开始处理网络事件,阻塞等待事件发生
    ev_run(ev_loop);

    return 0;
}

先执行 subscribe,再执行 publish,五秒后消息未被消费者处理,导致消息从延迟队列移动至死信队列中,被消费者所消费:

七. AMQP-CPP 封装

1. 设计与实现

消息队列的操作基本就五个操作:声明交换机,声明队列,绑定交换机-队列,发布消息,订阅队列消息。而基于这五个操作,封装一些类出来进行简化:

  • 套件配置声明结构:便于交换机与队列的声明与绑定,以及队列的订阅和消息的发布。
  • 消息队列客户端类:将客户端的各个请求操作封装起来,简化外部操作。
  • 发布者类:基于消息队列客户端类进行上层封装,用于发布端进行消息发布。
  • 订阅者类:基于消息队列客户端类进行上层封装,用于订阅端进行消息订阅。

1.1 目录结构

cpp 复制代码
source/
|-- mq.cc
|-- mq.h

1.2 代码实现

cpp 复制代码
// mq.h
#pragma once
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <ev.h>
#include <amqpcpp.h>
#include <amqpcpp/libev.h>

namespace xzymq {
    // 定义默认的交换机类型
    const std::string DIRECT = "direct";
    const std::string FANOUT = "fanout";
    const std::string TOPIC = "topic";
    const std::string HEADERS = "headers";
    const std::string DELAYED = "delayed";
    const std::string DLX_PREFIX = "dlx_";
    const std::string DEAD_LETTER_EXCHANGE = "x-dead-letter-exchange";
    const std::string DEAD_LETTER_BINDING_KEY = "x-dead-letter-routing-key";
    const std::string MESSAGE_TTL = "x-message-ttl";
    // 定义交换机、队列、绑定关键字、延时时间的结构体
    struct declare_settings {
        std::string exchange;
        std::string exchange_type;
        std::string queue;
        std::string binding_key;
        size_t delayed_ttl = 0;
        // 如果是延时队列,则需要在名称前面额外添加 dlx 前缀
        std::string dlx_exchange() const;
        std::string dlx_queue() const;
        std::string dlx_binding_key() const;
    };
    // 将字符串类型的交换机类型转换为AMQP::ExchangeType枚举值
    extern AMQP::ExchangeType exchange_type(const std::string& type);

    // 消息订阅成功的回调函数类型
    using MessageCallback = std::function<void(const char*, size_t)>;
    // 消息客户端类:用于连接消息队列服务器并进行消息的发布和订阅
    class MQClient {
    public:
        using ptr = std::shared_ptr<MQClient>;
        // 构造函数:初始化事件循环、消息处理程序、连接、通道、异步线程处理消息队列中的消息
        MQClient(const std::string& url);
        // 析构函数:关闭连接、等待异步线程结束、释放事件循环
        ~MQClient();
        // 声明交换机、队列、并绑定二者关系,如果是延时队列,则还需要创建配套的死信交换机和队列
        void declare(const declare_settings& settings);
        // 发布消息:将消息发布到指定的交换机和路由键
        bool publish(const std::string& exchange, const std::string& routing_key, const std::string& body);
        // 订阅消息:将指定队列的消息传递给回调函数
        void consume(const std::string& queue, const MessageCallback& callback);
        // 等待异步线程处理完成所有消息之后,退出事件循环
        void wait();
    private:
        // 异步回调函数:用于退出事件循环
        static void callback(struct ev_loop* loop, ev_async* watcher, int32_t revents);
        // 声明交换机、队列、绑定交换机和队列
        void _declared(const declare_settings& settings, AMQP::Table &args, bool is_dlx = false);
    private:
        std::mutex _declared_mtx; // 用于多线程同时调用声明交换机、队列、绑定操作和订阅操作的互斥锁
        std::mutex _mtx; // 用于声明交换机、队列、绑定操作->订阅操作的同步操作
        std::condition_variable _cv; // 条件变量与_mtx完成同步
        struct ev_loop* _ev_loop; // 事件循环,用于处理异步事件
        struct ev_async _ev_async; // 异步任务,用于通知事件循环退出
        AMQP::LibEvHandler _handler; // 消息处理程序,用于处理AMQP协议消息
        AMQP::TcpConnection _connection; // TCP连接,用于与消息队列服务器建立连接
        AMQP::TcpChannel _channel; // TCP信道,用于在连接上进行消息通信
        std::thread _async_thread; // 异步线程,用于处理消息队列中的消息
    };
    // 消息发布类:用于发布消息到消息队列
    class Publisher {
    public:
        using ptr = std::shared_ptr<Publisher>;
        // 构造函数:对成员进行初始化,并声明套件内的交换机和队列
        Publisher(const MQClient::ptr& mq_client, const declare_settings& settings);
        ~Publisher();
        // 发布消息:将消息发布到消息队列中
        bool publish(const std::string& body);
    private:
        MQClient::ptr _mq_client; // 消息客户端对象,用于发布消息
        declare_settings _settings; // 声明交换机、队列、绑定关键字、延时时间的结构体
    };
    // 消息订阅类:用于订阅消息队列中的消息
    class Subscriber {
    public:
        using ptr = std::shared_ptr<Subscriber>;
        // 构造函数:对成员进行初始化,并声明套件内的交换机和队列
        Subscriber(const MQClient::ptr& mq_client, const declare_settings& settings);
        ~Subscriber();
        // 订阅消息:如果是延时队列,则实际订阅的是配套的死信队列
        void consume(MessageCallback&& cb);
    private:
        MQClient::ptr _mq_client; // 消息客户端对象,用于订阅消息
        declare_settings _settings; // 声明交换机、队列、绑定关键字、延时时间的结构体
        MessageCallback _callback; // 消息回调函数,用于处理订阅到的消息
    };
    // 消息工厂类:用于创建不同类型的消息客户端对象
    class MQFactory {
    public:
        template<typename R, typename... Args>
        static std::shared_ptr<R> create(Args&&... args) {
            return std::make_shared<R>(std::forward<Args>(args)...);
        }
    };
}
cpp 复制代码
// mq.cc
#include "mq.h"
#include "log.h"

namespace xzymq {
    // 如果是延时队列,则需要在名称前面额外添加 dlx 前缀
    std::string declare_settings::dlx_exchange() const { return DLX_PREFIX + exchange; }
    std::string declare_settings::dlx_queue() const { return DLX_PREFIX + queue; }
    std::string declare_settings::dlx_binding_key() const { return DLX_PREFIX + binding_key; }
    // 将字符串类型的交换机类型转换为AMQP::ExchangeType枚举值
    AMQP::ExchangeType exchange_type(const std::string& type) {
        if (type == DIRECT) { // 直接交换机
            return AMQP::ExchangeType::direct;
        } else if (type == FANOUT) { // 广播交换机
            return AMQP::ExchangeType::fanout;
        } else if (type == TOPIC) { // 主题交换机
            return AMQP::ExchangeType::topic;
        } else if (type == HEADERS) { // 头交换机
            return AMQP::ExchangeType::headers;
        } else if (type == DELAYED) { // 延时交换机-直接交换机
            return AMQP::ExchangeType::direct;
        } else {
            ERR("交换机类型错误:{}", type);
            abort(); // 程序异常退出
        }
    }
    // 构造函数:初始化事件循环、消息处理程序、连接、通道、异步线程处理消息队列中的消息
    MQClient::MQClient(const std::string& url)
        : _ev_loop(EV_DEFAULT), _handler(_ev_loop), _connection(&_handler, AMQP::Address(url))
        , _channel(&_connection), _async_thread(std::thread([this](){ ev_run(_ev_loop); })) {}
    // 析构函数:关闭连接、等待异步线程结束、释放事件循环
    MQClient::~MQClient() {
        ev_async_init(&_ev_async, MQClient::callback); // 初始化异步任务
        ev_async_start(_ev_loop, &_ev_async); // 启动异步任务检测
        ev_async_send(_ev_loop, &_ev_async); // 发送异步任务到异步线程中执行
        _async_thread.join(); // 等待异步线程结束
    }
    // 异步回调函数:用于退出事件循环
    void MQClient::callback(struct ev_loop* loop, ev_async* watcher, int32_t revents) {
        ev_break(loop, EVBREAK_ALL); // 由于所有的ev操作都需要在同一个线程中执行,也就是需要在异步线程中执行
    }
    // 声明交换机、队列、绑定交换机和队列
    void MQClient::_declared(const declare_settings& settings, AMQP::Table &args, bool is_dlx) {
        // 1.加锁:确保线程安全
        std::unique_lock<std::mutex> declared_lock(_declared_mtx); // 互斥:确保多线程时只存在一个线程声明交换机、队列、绑定操作和订阅操作
        std::unique_lock<std::mutex> lock(_mtx); // 同步:确保在声明交换机、队列、绑定操作完成后再订阅消息
        // 2.根据参数 is_dlx 判断是否是死信队列,取出要使用的交换机、队列、绑定关键字
        std::string exchange, queue, binding_key;
        if (is_dlx == true) {
            exchange = settings.dlx_exchange();
            queue = settings.dlx_queue();
            binding_key = settings.dlx_binding_key();
        } else {
            exchange = settings.exchange;
            queue = settings.queue;
            binding_key = settings.binding_key;
        }
        // 3.获取交换机类型
        AMQP::ExchangeType type = exchange_type(settings.exchange_type);
        // 4.声明交换机、队列、并绑定交换机和队列
        _channel.declareExchange(exchange, type)
            .onSuccess([=, this](){
                _channel.declareQueue(queue, args)
                    .onSuccess([=, this](const std::string& name, uint32_t messagecount, uint32_t consumercount){
                        _channel.bindQueue(exchange, queue, binding_key)
                            .onSuccess([=, this]{
                                _cv.notify_all(); // 通知等待线程:绑定操作成功
                            })
                            .onError([=, this](const char* message){
                                ERR("绑定{}交换机和{}队列失败:{}", exchange, queue, message);
                                _cv.notify_all(); // 通知等待线程:绑定操作失败
                                abort(); // 程序异常退出
                            });
                    })
                    .onError([=, this](const char* message){
                        ERR("声明{}队列失败:{}", queue, message);
                        _cv.notify_all(); // 通知等待线程:声明队列失败
                        abort();
                    });
            })
            .onError([=, this](const char* message){
                ERR("声明{}交换机失败:{}", exchange, message);
                _cv.notify_all(); // 通知等待线程:声明交换机失败
                abort();
            });
        // 5.等待绑定操作完成后退出
        _cv.wait(lock);
    }
    // 声明交换机、队列、并绑定二者关系,如果是延时队列,则还需要创建配套的死信交换机和队列
    void MQClient::declare(const declare_settings& settings) {
        // 1.判断当前是否是延时队列
        AMQP::Table args;
        if (settings.exchange_type == DELAYED) {
            // 2.声明配套的死信交换机、队列、并进行绑定
            _declared(settings, args, true);
            // 3.设置参数:使常规交换机配套死信交换机
            args[DEAD_LETTER_EXCHANGE] = settings.dlx_exchange();
            args[DEAD_LETTER_BINDING_KEY] = settings.dlx_binding_key();
            args[MESSAGE_TTL] = settings.delayed_ttl; // 如果规定时间内未被消费,则将其丢进死信队列中
        }
        // 4.声明常规交换机、队列、并进行绑定,如果是延时队列,则通过参数配套死信交换机
        _declared(settings, args, false);
    }
    // 发布消息:将消息发布到指定的交换机和路由键
    bool MQClient::publish(const std::string& exchange, const std::string& routing_key, const std::string& body) {
        return _channel.publish(exchange, routing_key, body);
    }
    // 订阅消息:从指定的队列中接收消息
    void MQClient::consume(const std::string& queue, const MessageCallback& callback) {
        // 1.加锁:确保线程安全
        std::unique_lock<std::mutex> declared_lock(_declared_mtx); // 互斥:确保多线程时只存在一个线程声明交换机、队列、绑定操作和订阅操作
        std::unique_lock<std::mutex> lock(_mtx); // 同步:确保在声明交换机、队列、绑定操作完成后再订阅消息
        // 2.订阅队列消息:将队列中的消息传递给回调函数
        _channel.consume(queue)
            .onMessage([=, this](const AMQP::Message& message, uint64_t deliveryTag, bool redelivered){
                callback(message.body(), message.bodySize());
                _channel.ack(deliveryTag); // 手动确认消息:确保消息被正确处理后才被删除
            })
            .onSuccess([&](){
                DBG("订阅队列消息成功:{}", queue);
                _cv.notify_all(); // 通知等待线程:订阅操作完成
            })
            .onError([&](const char* message){
                ERR("订阅队列消息失败:{}", message);
                _cv.notify_all(); // 通知等待线程:订阅操作完成
                abort();
            });
        // 3.等待订阅操作完成后退出
        _cv.wait(lock);
    }
    // 等待异步线程处理完成所有消息之后,退出事件循环
    void MQClient::wait() {
        _async_thread.join();
    }

    // 发布者类:用于发布消息到消息队列中
    Publisher::Publisher(const MQClient::ptr& mq_client, const declare_settings& settings)
        : _mq_client(mq_client), _settings(settings) {
        _mq_client->declare(_settings);
    }
    Publisher::~Publisher(){}
    // 发布消息:将消息发布到消息队列中
    bool Publisher::publish(const std::string& body) {
        return _mq_client->publish(_settings.exchange, _settings.binding_key, body);
    }

    // 订阅类:初始化消息客户端对象和配置信息
    Subscriber::Subscriber(const MQClient::ptr& mq_client, const declare_settings& settings)
        : _mq_client(mq_client), _settings(settings) {
        _mq_client->declare(_settings);
    }
    Subscriber::~Subscriber(){}
    // 订阅消息:如果是延时队列,则实际订阅的是配套的死信队列,否则订阅的是常规队列
    void Subscriber::consume(MessageCallback&& cb) {
        _callback = std::move(cb);
        if (_settings.exchange_type == DELAYED) {
            _mq_client->consume(_settings.dlx_queue(), _callback); //  订阅配套的死信队列
        } else {
            _mq_client->consume(_settings.queue, _callback); // 订阅常规队列
        }
    }
}

2. 使用样例

1.1 目录结构

bash 复制代码
test/
|-- mq
    |-- delayed_publish.cc
    |-- delayed_subscribe.cc
    |-- makefile
    |-- simple_publish.cc
    |-- simple_subscribe.cc

1.2 项目构建

bash 复制代码
# makefile
all: simple_publish simple_subscribe delayed_publish delayed_subscribe
simple_publish: simple_publish.cc ../../source/mq.cc ../../source/log.cc
	g++ -o $@ $^ -std=c++17 -lamqpcpp -lev -lspdlog -lfmt -lpthread
simple_subscribe: simple_subscribe.cc ../../source/mq.cc ../../source/log.cc
	g++ -o $@ $^ -std=c++17 -lamqpcpp -lev -lspdlog -lfmt -lpthread
delayed_publish: delayed_publish.cc ../../source/mq.cc ../../source/log.cc
	g++ -o $@ $^ -std=c++17 -lamqpcpp -lev -lspdlog -lfmt -lpthread
delayed_subscribe: delayed_subscribe.cc ../../source/mq.cc ../../source/log.cc
	g++ -o $@ $^ -std=c++17 -lamqpcpp -lev -lspdlog -lfmt -lpthread

.PHONY: clean
clean:
	rm -f simple_publish simple_subscribe delayed_publish delayed_subscribe

编译生产可执行程序:

1.3 代码实现

1.3.1 简单队列消息的订阅与发布
cpp 复制代码
// simple_subscribe.cc
#include "../../source/mq.h"
#include "../../source/log.h"

int main()
{
    // 1.初始化日志模块,并定义代理rabbitmq服务器url
    xzylog::log_init();
    const std::string url = "amqp://admin:123456@192.168.174.128:5672/";
    // 2.定义配置信息
    xzymq::declare_settings settings = {
        .exchange = "test-exchange",
        .exchange_type = "direct",
        .queue = "test-queue",
        .binding_key = "test-binding-key"
    };
    // 3.定义消息队列客户端对象
    std::shared_ptr<xzymq::MQClient> mq_client = xzymq::MQFactory::create<xzymq::MQClient>(url);
    // 4.定义消息订阅者对象
    std::shared_ptr<xzymq::Subscriber> subscriber = xzymq::MQFactory::create<xzymq::Subscriber>(mq_client, settings);
    // 5.发布信息
    subscriber->consume([](const char* message, size_t len){
        std::cout << "收到队列消息:" << std::string(message, len) << std::endl;
    });
    // 6.阻塞等待消息
    mq_client->wait();

    return 0;
}
cpp 复制代码
// simple_publish.cc
#include "../../source/mq.h"
#include "../../source/log.h"

int main()
{
    // 1.初始化日志模块,并定义代理rabbitmq服务器url
    xzylog::log_init();
    const std::string url = "amqp://admin:123456@192.168.174.128:5672/";
    // 2.定义配置信息
    xzymq::declare_settings settings = {
        .exchange = "test-exchange",
        .exchange_type = "direct",
        .queue = "test-queue",
        .binding_key = "test-binding-key"
    };
    // 3.定义消息队列客户端对象
    std::shared_ptr<xzymq::MQClient> mq_client = xzymq::MQFactory::create<xzymq::MQClient>(url);
    // 4.定义消息发布者对象
    std::shared_ptr<xzymq::Publisher> publisher = xzymq::MQFactory::create<xzymq::Publisher>(mq_client, settings);
    // 5.发布信息
    publisher->publish("Hello World");

    return 0;
}

先执行 subscribe,再执行 publish:

1.3.1 延时队列消息的订阅与发布
cpp 复制代码
// delayed_subscribe.cc
#include "../../source/mq.h"
#include "../../source/log.h"

int main()
{
    // 1.初始化日志模块,并定义代理rabbitmq服务器url
    xzylog::log_init();
    const std::string url = "amqp://admin:123456@192.168.174.128:5672/";
    // 2.定义配置信息
    xzymq::declare_settings settings = {
        .exchange = "delayed-exchange",
        .exchange_type = "delayed",
        .queue = "delayed-queue",
        .binding_key = "delayed-binding-key",
        .delayed_ttl = 3000 // 延迟时间
    };
    // 3.定义消息队列客户端对象
    std::shared_ptr<xzymq::MQClient> mq_client = xzymq::MQFactory::create<xzymq::MQClient>(url);
    // 4.定义消息订阅者对象
    std::shared_ptr<xzymq::Subscriber> subscriber = xzymq::MQFactory::create<xzymq::Subscriber>(mq_client, settings);
    // 5.发布信息
    subscriber->consume([](const char* message, size_t len){
        std::cout << "收到队列消息:" << std::string(message, len) << std::endl;
    });
    // 6.阻塞等待消息
    mq_client->wait();

    return 0;
}
cpp 复制代码
// delayed_publish.cc
#include "../../source/mq.h"
#include "../../source/log.h"

int main()
{
    // 1.初始化日志模块,并定义代理rabbitmq服务器url
    xzylog::log_init();
    const std::string url = "amqp://admin:123456@192.168.174.128:5672/";
    // 2.定义配置信息
    xzymq::declare_settings settings = {
        .exchange = "delayed-exchange",
        .exchange_type = "delayed",
        .queue = "delayed-queue",
        .binding_key = "delayed-binding-key",
        .delayed_ttl = 3000 // 延迟时间
    };
    // 3.定义消息队列客户端对象
    std::shared_ptr<xzymq::MQClient> mq_client = xzymq::MQFactory::create<xzymq::MQClient>(url);
    // 4.定义消息发布者对象
    std::shared_ptr<xzymq::Publisher> publisher = xzymq::MQFactory::create<xzymq::Publisher>(mq_client, settings);
    // 5.发布信息
    publisher->publish("Hello World");

    return 0;
}

先执行 subscribe,再执行 publish,三秒后消息未被消费者处理,导致消息从延迟队列移动至死信队列中,被消费者所消费:

相关推荐
进击的小头2 小时前
设计模式落地的避坑指南(C语言版)
c语言·开发语言·设计模式
凤年徐2 小时前
容器适配器深度解析:从STL的stack、queue到优先队列的底层实现
开发语言·c++·算法
ujainu2 小时前
Flutter + OpenHarmony 游戏开发进阶:虚拟摄像机系统——平滑跟随与坐标偏移
开发语言·flutter·游戏·swift·openharmony
金书世界2 小时前
使用PHP+html+MySQL实现用户的注册和登录(源码)
开发语言·mysql·php
Dxy12393102162 小时前
MySQL如何避免隐式转换
开发语言·mysql
历程里程碑2 小时前
Linux 18 进程控制
linux·运维·服务器·开发语言·数据结构·c++·笔记
froginwe112 小时前
C# 预处理器指令
开发语言
爱装代码的小瓶子2 小时前
【c++与Linux基础】文件篇(5)- 文件管理系统:
linux·开发语言·c++
马猴烧酒.2 小时前
【团队空间|第十一天】基础功能实现,RBAC权限控制,ShardingSphere详解
java·开发语言·数据库