目录
一、概述
二、编译RabbitMQ-c
三、核心原理
3.1生产者与交换机关系
3.2交换机与队列关系
3.3队列与消费者关系
3.4交换机与交换机的关系
四、开发者
4.1接口文件
4.2交换机
声明交换机
删除交换机
向交换机绑定路由键
从交换机解绑路由键
交换机之间绑定路由键
交换机之间解绑路由键
4.3队列
声明队列
清空队列
删除队列
4.4生产者
4.5消费者
4.5.1订阅
4.5.2拉取
RabbitMQ-C使用开发详解(Windows环境)
讨论的是windows环境下的使用RabbitMQ-c与RabbitMQ服务端的交互。
RabbitMq的C/C++客户端有很多,我们选用RabbitMq-c。windows环境下的MFC开发,需要把RabbitMq-c客户端编译成dll。
1.下载和安装
下载rabbitmq-c最新代码包:https://github.com/alanxz/rabbitmq-c
下载cmake最新安装包:https://cmake.org/download/
2.使用cmake编译生成适合自己编译环境的工程
第一步:填写源代码路径
第二步:填写建立后的路径,build的文件夹一般建立在源代码路径里,也可以放在其他位置
第三步:点击配置按钮,在配置里面选择属于自己编译环境的名字
第四步:点击生成按钮,不出现运行失败就说明已经编译成功了
特别说明:在编译rabbitmq-c是如果出现如图的错误,可以去掉ENABLE_SSL_SUPPORT括号里的对勾。
在以上生成的工程目录下的librabbitmq\Debug路径下会生成librabbitmq.4.lib、librabbitmq.4.dll两个文件,对应的动态库的导出文件在rabbitmq-c-master\librabbitmq目录下。
生产者与交换机的关系是多对多的有关系,多个生产者可以给同一个交换机生产消息,同时一个生产者也可以能多个交换机生产消息。
交换机与队列的关系是多对多的关系,一个交换机可以给多个队列提供消息,同时多个交换机也可以同时给一个队列提供消息。
队列与消费者的关系是多对多的关系,一个队列可以同时被多个消费者消费,同时一个消费者可以同时消费多个队列的消息。
交换机与交换机的关系,与交换机与队列的关系是一样的。部份交换机充当队列的角色,从其绑定的交换机上分流数据,然后再把自己角色转换成交换机,然后给绑定在自身上的队列分派消息。
所以交换机之间的关系是多对多,一个上级交换机可以绑定多个下级交换机,一个下级交换机可同时绑定多个上级交换机。
总共有4个导出文件:
amqp.h:主要的rabbitmq-c客户端接口都在此文件
amqp_tcp_socket.h:与socket相关的接口
mqp_framing.h:不常用的一些接口
amqp_ssl_socket.h:用户ssl方式加密访问rabbitmq-server
生产者生产消息过程:
(1)客户端连接到消息队列服务器,打开一个channel。
(2)客户端声明一个exchange,并设置相关属性。
(3)客户端声明一个queue,并设置相关属性。
(4)客户端使用routing key,在exchange和queue之间建立好绑定关系。
(5)客户端投递消息到exchange。
RabbitMQ支持消息的持久化:
也就是数据写在磁盘上,为了数据安全考虑,我想大多数用户都会选择持久化。如果exchange和queue都是持久化的,那么它们之间的binding也是持久化的。如果exchange和queue两者之间有一个持久化,一个非持久化,就不允许建立绑定。消息队列持久化包括3个部分:
(1)exchange持久化,在声明时指定durable => 1
(2)queue持久化,在声明时指定durable => 1
(3)消息持久化,在投递时指定delivery_mode=> 2(1是非持久化)
AMQP_PUBLIC_FUNCTION amqp_exchange_declare_ok_t *AMQP_CALL amqp_exchange_declare(amqp_connection_state_t state, amqp_channel_t channel,amqp_bytes_t exchange, amqp_bytes_t type, amqp_boolean_t passive,amqp_boolean_t durable, amqp_boolean_t auto_delete, amqp_boolean_t internal,amqp_table_t arguments);
/**
* amqp_exchange_declare
*
* @param [in] connect连接 amqp_new_connection获取
* @param [in] channel the channel to do the RPC on,程序自己设置一个通道号,一个连接可以多个通道号。
* @param [in] exchange 指定交换机名称 eg:amqp_cstring_bytes("exchange_cat")
* @param [in] type 指定交换机类型,amqp_cstring_bytes("direct")
* "fanout" 广播的方式,发送到该exchange的所有队列上。
* "direct" 通过路由键发送到指定的队列上。
* "topic" 通过匹配路由键的方式获取,使用通配符*,#
* @param [in] passive 检测exchange是否存在,设为true,若exchange存在则命令成功返回(调用其他参数不会影响exchange属性),若不存在不会创建exchange,返回错误。设为false,如果exchange不存在则创建exchange,调用成功返回。如果exchange已经存在,并且匹配现在exchange的话则成功返回,如果不匹配则exchange声明失败。
* @param [in] durable 队列是否持久化
* @param [in] auto_delete 连接断开的时候,exchange是否自动删除
* @param [in] internal internal
* @param [in] arguments arguments
* @returns amqp_exchange_declare_ok_t
*/
Demo示例:
#include
#include
void die_on_amqp_error2(amqp_rpc_reply_t x, char const *context) {
char sLog[1024] = {0};
switch (x.reply_type) {
case AMQP_RESPONSE_NORMAL:
return;
case AMQP_RESPONSE_NONE:
printf(sLog, "%s: missing RPC reply type!\n", context);
break;
case AMQP_RESPONSE_LIBRARY_EXCEPTION:
printf(sLog, "%s: %s\n", context, amqp_error_string2(x.library_error));
break;
case AMQP_RESPONSE_SERVER_EXCEPTION:
switch (x.reply.id) {
case AMQP_CONNECTION_CLOSE_METHOD: {
amqp_connection_close_t *m =
(amqp_connection_close_t *)x.reply.decoded;
printf(sLog, "%s: server connection error %uh, message: %.*s\n",
context, m->reply_code, (int)m->reply_text.len,
(char *)m->reply_text.bytes);
break;
}
case AMQP_CHANNEL_CLOSE_METHOD: {
amqp_channel_close_t *m = (amqp_channel_close_t *)x.reply.decoded;
printf(sLog, "%s: server channel error %uh, message: %.*s\n",
context, m->reply_code, (int)m->reply_text.len,
(char *)m->reply_text.bytes);
break;
}
default:
printf(sLog, "%s: unknown server error, method id 0x%08X\n",
context, x.reply.id);
break;
}
break;
}
AfxMessageBox(sLog);
}
void die_on_error2(int x, char const *context) {
if (x < 0) {
char sLog[1024] = {0};
printf(sLog, "%s: %s\n", context, amqp_error_string2(x));
AfxMessageBox(sLog);
}
}
void Crabbitmq_demoDlg::OnBnClickedButton1()
{
char const *hostname;
int port, status;
char const *exchange;
char const *exchangetype;
amqp_socket_t *socket = NULL;
amqp_connection_state_t conn;
hostname ="localhost";
port = 5672;
exchange = "test.fanout";
exchangetype = "fanout"; //fanout/direct/topic
conn = amqp_new_connection();
socket = amqp_tcp_socket_new(conn);
if (!socket) {
AfxMessageBox("creating TCP socket");
}
status = amqp_socket_open(socket, hostname, port);
if (status) {
AfxMessageBox("opening TCP socket");
}
die_on_amqp_error2(amqp_login(conn, "/", 0, 131072, 0, AMQP_SASL_METHOD_PLAIN,
"guest", "guest"),
"Logging in");
amqp_channel_open(conn, 1);
die_on_amqp_error2(amqp_get_rpc_reply(conn), "Opening channel");
amqp_exchange_declare(conn, 1, amqp_cstring_bytes(exchange),
amqp_cstring_bytes(exchangetype), 0, 0, 0, 0,
amqp_empty_table);
die_on_amqp_error2(amqp_get_rpc_reply(conn), "Declaring exchange");
die_on_amqp_error2(amqp_channel_close(conn, 1, AMQP_REPLY_SUCCESS),
"Closing channel");
die_on_amqp_error2(amqp_connection_close(conn, AMQP_REPLY_SUCCESS),
"Closing connection");
die_on_error2(amqp_destroy_connection(conn), "Ending connection");
int end = 0;
}
/**
* amqp_exchange_delete
*
* @param [in] state connection state
* @param [in] channel the channel to do the RPC on
* @param [in] exchange exchange
* @param [in] if_unused if_unused
* @returns amqp_exchange_delete_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_exchange_delete_ok_t *AMQP_CALL
amqp_exchange_delete(amqp_connection_state_t state, amqp_channel_t channel,
amqp_bytes_t exchange, amqp_boolean_t if_unused);
队列的消息来源于交换机,所以队列需要通过路由键与交换机建立联系,然后报备自己需要的消息。
/**
* amqp_queue_bind
*
* @param [in] state connection state:连接对象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] queue queue:待绑定的队列名称
* @param [in] exchange exchange:与队列绑定的交换机名称
* @param [in] routing_key routing_key:路由键
* @param [in] arguments arguments
* @returns amqp_queue_bind_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_queue_bind_ok_t *AMQP_CALL amqp_queue_bind(
amqp_connection_state_t state, amqp_channel_t channel, amqp_bytes_t queue,
amqp_bytes_t exchange, amqp_bytes_t routing_key, amqp_table_t arguments);
取消队列与交换机之间的关联。
/**
* amqp_queue_unbind
*
* @param [in] state connection state:对象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] queue queue:队列名称
* @param [in] exchange exchange:交换机名称
* @param [in] routing_key routing_key:路由键
* @param [in] arguments arguments
* @returns amqp_queue_unbind_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_queue_unbind_ok_t *AMQP_CALL amqp_queue_unbind(
amqp_connection_state_t state, amqp_channel_t channel, amqp_bytes_t queue,
amqp_bytes_t exchange, amqp_bytes_t routing_key, amqp_table_t arguments);
把队列与交换机,通过路由键建立交联,当交换机收到指定路由键的消息时,交会路由到之前绑定的队列中去。
/**
* amqp_exchange_bind
*
* @param [in] state connection state:连接对象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] destination destination:接收消息的交换机
* @param [in] source source:发出消息的交换机
* @param [in] routing_key routing_key:路由键
* @param [in] arguments arguments
* @returns amqp_exchange_bind_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_exchange_bind_ok_t *AMQP_CALL
amqp_exchange_bind(amqp_connection_state_t state, amqp_channel_t channel,
amqp_bytes_t destination, amqp_bytes_t source,
amqp_bytes_t routing_key, amqp_table_t arguments);
把队列与交换机通过路由键建立的交联进行解除,让队列与交换机解除关系。
/**
* amqp_exchange_unbind
*
* @param [in] state connection state:连接对象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] destination destination:
* @param [in] source source
* @param [in] routing_key routing_key
* @param [in] arguments arguments
* @returns amqp_exchange_unbind_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_exchange_unbind_ok_t *AMQP_CALL
amqp_exchange_unbind(amqp_connection_state_t state, amqp_channel_t channel,
amqp_bytes_t destination, amqp_bytes_t source,
amqp_bytes_t routing_key, amqp_table_t arguments);
/**
* amqp_queue_declare
*
* @param [in] state connection state:连接对象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] queue queue:需要绑定的队列名称
* @param [in] passive passive:检测queue是否存在,设为true,若queue存在则命令成功返回(调用其他参数不会影响queue属性),若不存在不会创建queue,返回错误。设为false,如果queue不存在则创建queue,调用成功返回。如果queue已经存在,并且匹配现在queue的话则成功返回,如果不匹配则queue声明失败。
* @param [in] durable durable:是否持久化(写入到硬盘)
* @param [in] exclusive exclusive:当前连接断开时,队列是否自动删除
* @param [in] auto_delete auto_delete:没有消费者时,是否自动删除
* @param [in] arguments arguments
* @returns amqp_queue_declare_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_queue_declare_ok_t *AMQP_CALL amqp_queue_declare(
amqp_connection_state_t state, amqp_channel_t channel, amqp_bytes_t queue,
amqp_boolean_t passive, amqp_boolean_t durable, amqp_boolean_t exclusive,
amqp_boolean_t auto_delete, amqp_table_t arguments);
清空队列里的数据
/**
* amqp_queue_purge
*
* @param [in] state connection state:连接对象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] queue queue:队列名称
* @returns amqp_queue_purge_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_queue_purge_ok_t *AMQP_CALL amqp_queue_purge(amqp_connection_state_t state,
amqp_channel_t channel,
amqp_bytes_t queue);
/**
* amqp_queue_delete
*
* @param [in] state connection state:连接对象
* @param [in] channel the channel to do the RPC on:通道号
* @param [in] queue queue:队列名称
* @param [in] if_unused if_unused:当为真时,仅当队列不使用时删除
* @param [in] if_empty if_empty:当为真时,仅当队列为空时删除
* @returns amqp_queue_delete_ok_t:返回值
*/
AMQP_PUBLIC_FUNCTION
amqp_queue_delete_ok_t *AMQP_CALL amqp_queue_delete(
amqp_connection_state_t state, amqp_channel_t channel, amqp_bytes_t queue,
amqp_boolean_t if_unused, amqp_boolean_t if_empty);
/**
* 发布一条消息到broker
*
* 使用路由密钥在exchange上发布消息。
*
* 请注意,在AMQ协议级别basic.publish是一个异步方法:
* 这意味着broker发生的错误情况(例如发布到不存在的exchange)将不会反映在此函数的返回值中。
*
* \param [in] state 连接对象
* \param [in] channel 通道标识符
* \param [in] exchange broker需要发布到的exchange
* \param [in] routing_key 发布消息使用的路由秘钥
* \param [in] mandatory 向broker表明该消息必须路由到一个队列。如果broker不能这样做,
* 它应该用一个basic.return方法来回应。
* \param [in] immediate 向broker表明该消息必须立即传递给消费者,如果broker不能这样做,
* 它应该用一个basic.return方法来回应。
* \param [in] properties 与消息相关的属性
* \param [in] body 消息体
* \return 成功返回AMQP_STATUS_OK,失败返回amqp_status_enum值
* 注意:请注意,basic.publish是一个异步方法,此函数的返回值仅指示消息数据已
* 成功传输到代理.它并不表示broker发生的故障,例如发布到不存在的exchange.
* 可能的错误值:
* - AMQP_STATUS_TIMER_FAILURE:系统计时器设施返回错误,消息未被发送。
* - AMQP_STATUS_HEARTBEAT_TIMEOUT: 等待broker的心跳连接超时,消息未被发送
* - AMQP_STATUS_NO_MEMORY:分配内存失败,消息未被发送
* - AMQP_STATUS_TABLE_TOO_BIG:属性中的table太大而不适合单个框架,消息未被发送
* - AMQP_STATUS_CONNECTION_CLOSED:连接被关闭。
* - AMQP_STATUS_SSL_ERROR:发生SSL错误。
* - AMQP_STATUS_TCP_ERROR:发生TCP错误,errno或WSAGetLastError()可能提供更多的信息
*
*/
AMQP_PUBLIC_FUNCTION
int AMQP_CALL amqp_basic_publish(
amqp_connection_state_t state, amqp_channel_t channel,
amqp_bytes_t exchange, amqp_bytes_t routing_key, amqp_boolean_t mandatory,
amqp_boolean_t immediate, struct amqp_basic_properties_t_ const *properties,
amqp_bytes_t body);
消费方式分push与pull。RabbitMQ-C的example和网上的博客都是通过consume方式获取数据,但consume是通过推送的方式被动消费数据,当调用amqp_basic_consume开始一个消费者后,服务器就开始推送数据,而调用amqp_consume_message只是从本地的缓冲区中读取数据,每次服务器都推送了300条数据;
而我想一条一条的主动取,example中并没有关于主动get数据的方式。后来经过多次读源码,写例子,才发现amqp_basic_get就是一条一条的取数据,当然这个函数不能像amqp_basic_consume一样仅仅调用一次;而是在每次amqp_read_message之前都要调用amqp_basic_get方法;
如果只调用一次amqp_basic_get那么当我们第二次调用amqp_read_message的时候就会阻塞,所以它们必须成对出现。
服务端主动给消费者推送消息,通过两个函数配合使用,先调用amqp_basic_consume开始一个消费者后,服务器就开始推送数据,再调用amqp_consume_message从本地的缓冲区中读取数据,每次服务器都推送了300条数据。
/**
* amqp_basic_consume:开始一个队列消费者
*
* @param [in] state connection state:连接对象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] queue queue:队列名称
* @param [in] consumer_tag consumer_tag:消费者标签
* @param [in] no_local no_local:填false,属于amqp标准,rabbitmq没有实现
* @param [in] no_ack no_ack:如果为true,消费消息后直接从队列清除,为false,需要
* 人工调用amqp_basic_ack函数后,消息才会从队列清除。
* @param [in] exclusive exclusive:排他消费者,即这个队列只能由一个消费者消费.适用于
* 任务不允许进行并发处理的情况下.比如系统对接
* @param [in] arguments arguments
* @returns amqp_basic_consume_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_basic_consume_ok_t *AMQP_CALL amqp_basic_consume(
amqp_connection_state_t state, amqp_channel_t channel, amqp_bytes_t queue,
amqp_bytes_t consumer_tag, amqp_boolean_t no_local, amqp_boolean_t no_ack,
amqp_boolean_t exclusive, amqp_table_t arguments);
/**
* Wait for and consume a message:等待并消费消息
*
* Waits for a basic.deliver method on any channel, upon receipt of
* basic.deliver it reads that message, and returns. If any other method is
* received before basic.deliver, this function will return an amqp_rpc_reply_t
* with ret.reply_type == AMQP_RESPONSE_LIBRARY_EXCEPTION, and
* ret.library_error == AMQP_STATUS_UNEXPECTED_STATE. The caller should then
* call amqp_simple_wait_frame() to read this frame and take appropriate action.
*
* This function should be used after starting a consumer with the
* amqp_basic_consume() function
*
* \param [in,out] state the connection object:连接对象
* \param [in,out] envelope a pointer to a amqp_envelope_t object. Caller
* should call #amqp_destroy_envelope() when it is done using
* the fields in the envelope object. The caller is responsible
* for allocating/destroying the amqp_envelope_t object itself.
*返回一个指针,指向一个对象,这个对象使用后,需要人工调用amqp_destroy_envelope
*释放。
* \param [in] timeout a timeout to wait for a message delivery. Passing in
* NULL will result in blocking behavior.:等待时间,为NULL将阻塞
* \param [in] flags pass in 0. Currently unused.:无效
* \returns a amqp_rpc_reply_t object. ret.reply_type == AMQP_RESPONSE_NORMAL
* on success. If ret.reply_type == AMQP_RESPONSE_LIBRARY_EXCEPTION,
* and ret.library_error == AMQP_STATUS_UNEXPECTED_STATE, a frame other
* than AMQP_BASIC_DELIVER_METHOD was received, the caller should call
* amqp_simple_wait_frame() to read this frame and take appropriate
* action.
*
* \since v0.4.0
*/
AMQP_PUBLIC_FUNCTION
amqp_rpc_reply_t AMQP_CALL amqp_consume_message(amqp_connection_state_t state,
amqp_envelope_t *envelope,
struct timeval *timeout,
int flags);
amqp_basic_get与amqp_read_message配合,一条一条主动提取消息。如果只调用一次amqp_basic_get那么当我们第二次调用amqp_read_message的时候就会阻塞,所以它们必须成对出现。
/**
* Do a basic.get:单条提取消息
*
* Synchonously polls the broker for a message in a queue, and
* retrieves the message if a message is in the queue.:从队列中提取消息,一次提取一
*条,这是主动提取,适合某些主动处理消息的场景
* \param [in] state the connection object:连接对象
* \param [in] channel the channel identifier to use:通道
* \param [in] queue the queue name to retrieve from:队列
* \param [in] no_ack if true the message is automatically ack'ed
* if false amqp_basic_ack should be called once the message
* retrieved has been processed:是否要人工回馈
* \return amqp_rpc_reply indicating success or failure
*
* \since v0.1
*/
AMQP_PUBLIC_FUNCTION
amqp_rpc_reply_t AMQP_CALL amqp_basic_get(amqp_connection_state_t state,
amqp_channel_t channel,
amqp_bytes_t queue,
amqp_boolean_t no_ack);
/**
* Reads the next message on a channel:读取消息
*读取指定通道上的队列上的消息,需要与amqp_basic_get()配合使用
* Reads a complete message (header + body) on a specified channel. This
* function is intended to be used with amqp_basic_get() or when an
* AMQP_BASIC_DELIVERY_METHOD method is received.
*
* \param [in,out] state the connection object:连接对象
* \param [in] channel the channel on which to read the message from:通道
* \param [in,out] message a pointer to a amqp_message_t object. Caller should
* call amqp_message_destroy() when it is done using the
* fields in the message object. The caller is responsible for
* allocating/destroying the amqp_message_t object itself.
* 指向返回消息的指针,需要人工调用amqp_message_destroy() 接口释放
* \param [in] flags pass in 0. Currently unused.:废弃
* \returns a amqp_rpc_reply_t object. ret.reply_type == AMQP_RESPONSE_NORMAL on
* success.
*
* \since v0.4.0
*/
AMQP_PUBLIC_FUNCTION
amqp_rpc_reply_t AMQP_CALL amqp_read_message(amqp_connection_state_t state,
amqp_channel_t channel,
amqp_message_t *message,
int flags);
/**
* Acknowledges a message:回馈指定消息
*
* Does a basic.ack on a received message:对消费后的消息进行回馈
*
* \param [in] state the connection object:连接对象
* \param [in] channel the channel identifier:通道
* \param [in] delivery_tag the delivery tag of the message to be ack'd:传输标签
* \param [in] multiple if true, ack all messages up to this delivery tag, if
* false ack only this delivery tag:传输标签
* \return 0 on success, 0 > on failing to send the ack to the broker.
* this will not indicate failure if something goes wrong on the
* broker
*
* \since v0.1
*/
AMQP_PUBLIC_FUNCTION
int AMQP_CALL amqp_basic_ack(amqp_connection_state_t state,
amqp_channel_t channel, uint64_t delivery_tag,
amqp_boolean_t multiple);
delivery_tag如何获得?以下是伪代码:
amqp_rpc_reply_t rpc_reply;
do {
rpc_reply = amqp_basic_get(conn, 1,queuename, 0);
} while (rpc_reply.reply_type == AMQP_RESPONSE_NORMAL &&
rpc_reply.reply.id == AMQP_BASIC_GET_EMPTY_METHOD
/*&& amqp_time_has_past(deadline) == AMQP_STATUS_OK*/);
amqp_message_t message;
amqp_rpc_reply_t rpc_reply2 = amqp_read_message(conn, 1, &message, 0);
amqp_method_t method = rpc_reply.reply;
if (AMQP_BASIC_GET_OK_METHOD == method.id)
{
amqp_basic_ack_t *s;
s = (amqp_basic_ack_t *) method.decoded;
int nRe = amqp_basic_ack(conn, 1, s->delivery_tag, 0);
int i = 0;
}
而通过amqp_consume_message接口消费的消息,如果要回馈的话,delivery_tag在返回的amqp_envelope_t变量里。