RabbitMQ C#操作示例
发布第二版,对RabbitMQ的知识重新做了梳理,优化了.NET的客户端操作代码(MarkDown贴图麻烦就不搞图了)
推荐:https://geewu.gitbooks.io/rabbitmq-quick/content/RabbitMQ介绍.html
前世今生
要说RabbitMQ就不得不扯一扯AMQP,消息队列这个概念其实并不新鲜,早在80年代的时候就被应用在金融交易中,如:TIB、在到之后IBM开发了MQSeries、微软搞了MSMQ,这些MQ有一个共同点就是要钱,虽然Java搞了JMQ,但舅舅不疼姥姥不爱,凄凉哉。人们依旧生活在水深火热之中,业界也亟需一款开源、免费、统一的消息队列项目的出现。
转机出现在2004年,大财主摩根大通和iMatrix开始着手AMQP标准的制定,并于2006年发布规范,2007年基于AMQP标准、Erlang语言开发的RabbitMQ应运而生,这位新生儿一出生就得到业界的广泛关注、好评如潮、blabla……
基本概念
接下来对于AMQP中的一些基本概念进行说明,对于操作上的理解有一定帮助。
- Broker:用于接收和分发消息,RabbitMQ总的Broker就是RabbitMQ的Server端;
- Virtual Host:是出于安全和多用户角度考虑的,在RabbitMQ中可以为多个用户划分不同的Virtual Host;
- Connection:客户端和服务端的TCP连接;
- Channel:通信管道、毕竟不停的创建连接的消耗比较大,可以通过Channel来进行通讯,不过拎不清的是为啥.NET的客户端里叫CreateModel;
- Exchange:Message到达Broker后,会由Exchange根据规则查询Routing Key进行分发,将Message分发到Queue中去,Exchange常用的类型有:
- Direct:这个是默认的Exchange类型,在使用这种类型的Exchange时,可以不指定Routing Key,这个RoutingKey和Queue的名字相同;
- Fanout:这种类型会忽略RoutingKey的存在,直接将Message广播到其上绑定的所有的的Queue中去;
- Topic:这种类型会在Queue绑定的时候可疑使用通配符,如*表示一个关键字、#表示多个关键字,你在发布的时候就可疑根据RoutingKey和绑定关系找到对应的Queue,示例:
static void Main(string[] args)
{
Console.WriteLine("开始执行");
var factory = new ConnectionFactory()
{
UserName = "admin",
Password = "admin",
HostName = "192.168.253.131"
};
var conn = factory.CreateConnection();
var channel = conn.CreateModel();
//声明转发器类型为Topic
channel.ExchangeDeclare("Korea", ExchangeType.Topic, true, false, null);
//声明Q1,RoutingKey的匹配规则为log.*,可以匹配到诸如log.error,log.warn等,但是无法匹配到log.error.self
channel.QueueDeclare("Q1", true);
channel.QueueBind("Q1", "Korea", "log.*", null);
channel.QueueDeclare("Q2", true);
channel.QueueBind("Q2", "Korea", "log.*", null);
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2;
channel.BasicPublish("Korea", "log.warn", properties, Encoding.UTF8.GetBytes("你好"));
var bgr = channel.BasicGet("Q2", true);
Console.WriteLine(Encoding.UTF8.GetString(bgr.Body));
Console.WriteLine("执行完毕");
Console.ReadLine();
}
* Headers:Headers会忽略RoutingKey的存在,在Publish时,可以在Headers中加入相关信息,依据Headers中的规则来匹配,示例:
static void Main(string[] args)
{
Console.WriteLine("开始执行");
var factory = new ConnectionFactory()
{
UserName = "admin",
Password = "admin",
HostName = "192.168.253.131"
};
var conn = factory.CreateConnection();
var channel = conn.CreateModel();
//声明转发器类型为Topic
channel.ExchangeDeclare("school", ExchangeType.Headers, true, false, null);
//声明Q1,RoutingKey的匹配规则为log.*,可以匹配到诸如log.error,log.warn等,但是无法匹配到log.error.self
Dictionary<string, object> headers = new Dictionary<string, object>();
headers.Add("x-match", "any"); //all/ any(只要有一个键值对匹配即可用)
headers.Add("name", "lilei");
headers.Add("age", 13);
channel.QueueDeclare("students", true);
channel.QueueBind("students", "school", "", headers);
channel.QueueDeclare("teachers", true);
channel.QueueBind("teachers", "school", "", null);
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2;
//添加Header存储的键值对
properties.Headers=new Dictionary<string,object>();
properties.Headers.Add("name", "hanmeimei");
properties.Headers.Add("age", 13);
channel.BasicPublish("school", "", properties, Encoding.UTF8.GetBytes("你好"));
var bgr = channel.BasicGet("students", true); //但是注意,如果Queue中不绑定headers,如teachers也可以拿到值
Console.WriteLine(Encoding.UTF8.GetString(bgr.Body));
Console.WriteLine("执行完毕");
Console.ReadLine();
}
- Queue:消息最终会被缓存在Queue中等待消费;
- Binding:Exchange和Queue中的绑定关系,Binding信息会存储在Exchange的查询表中,用于分发Message数据。
我们再梳理一下流程(这里不画图了,自己YY一下),建立Connection之后,消息通过Channel到达Exchange,Exchange根据绑定的RoutingKey规则将Mesage拷贝到各个Queue中去,等待消费。
生产/消费代码
这里写了一个基础操作类,用于展示如何使用RabbitMQ
*RabbitMQ C#操作示例*
======================
_发布第二版,对RabbitMQ的知识重新做了梳理,优化了.NET的客户端操作代码(MarkDown贴图麻烦就不搞图了)_
# 前世今生
要说RabbitMQ就不得不扯一扯AMQP,消息队列这个概念其实并不新鲜,早在80年代的时候就被应用在金融交易中,如:TIB、在到之后IBM开发了MQSeries、微软搞了MSMQ,这些MQ有一个共同点就是要钱,虽然Java搞了JMQ,但舅舅不疼姥姥不爱,凄凉哉。人们依旧生活在水深火热之中,业界也亟需一款开源、免费、统一的消息队列项目的出现。<br/>
转机出现在2004年,大财主摩根大通和iMatrix开始着手AMQP标准的制定,并于2006年发布规范,2007年基于AMQP标准、Erlang语言开发的RabbitMQ应运而生,这位新生儿一出生就得到业界的广泛关注、好评如潮、blabla……
# 基本概念
接下来对于AMQP中的一些基本概念进行说明,对于操作上的理解有一定帮助。
* Broker:用于接收和分发消息,RabbitMQ总的Broker就是RabbitMQ的Server端;
* Virtual Host:是出于安全和多用户角度考虑的,在RabbitMQ中可以为多个用户划分不同的Virtual Host;
* Connection:客户端和服务端的TCP连接;
* Channel:通信管道、毕竟不停的创建连接的消耗比较大,可以通过Channel来进行通讯,不过拎不清的是为啥.NET的客户端里叫CreateModel;
* Exchange:Message到达Broker后,会由Exchange根据规则查询Routing Key进行分发,将Message分发到Queue中去,Exchange常用的类型有:
* Direct:这个是默认的Exchange类型,在使用这种类型的Exchange时,可以不指定Routing Key,这个RoutingKey和Queue的名字相同;
* Fanout:这种类型会忽略RoutingKey的存在,直接将Message广播到其上绑定的所有的的Queue中去;
* Topic:这种类型会在Queue绑定的时候可疑使用通配符,如*表示一个关键字、#表示多个关键字,你在发布的时候就可疑根据RoutingKey和绑定关系找到对应的Queue,示例:
````csharp
static void Main(string[] args)
{
Console.WriteLine("开始执行");
var factory = new ConnectionFactory()
{
UserName = "admin",
Password = "admin",
HostName = "192.168.253.131"
};
var conn = factory.CreateConnection();
var channel = conn.CreateModel();
//声明转发器类型为Topic
channel.ExchangeDeclare("Korea", ExchangeType.Topic, true, false, null);
//声明Q1,RoutingKey的匹配规则为log.*,可以匹配到诸如log.error,log.warn等,但是无法匹配到log.error.self
channel.QueueDeclare("Q1", true);
channel.QueueBind("Q1", "Korea", "log.*", null);
channel.QueueDeclare("Q2", true);
channel.QueueBind("Q2", "Korea", "log.*", null);
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2;
channel.BasicPublish("Korea", "log.warn", properties, Encoding.UTF8.GetBytes("你好"));
var bgr = channel.BasicGet("Q2", true);
Console.WriteLine(Encoding.UTF8.GetString(bgr.Body));
Console.WriteLine("执行完毕");
Console.ReadLine();
}
* Headers:Headers会忽略RoutingKey的存在,在Publish时,可以在Headers中加入相关信息,依据Headers中的规则来匹配,示例:
static void Main(string[] args)
{
Console.WriteLine("开始执行");
var factory = new ConnectionFactory()
{
UserName = "admin",
Password = "admin",
HostName = "192.168.253.131"
};
var conn = factory.CreateConnection();
var channel = conn.CreateModel();
//声明转发器类型为Topic
channel.ExchangeDeclare("school", ExchangeType.Headers, true, false, null);
//声明Q1,RoutingKey的匹配规则为log.*,可以匹配到诸如log.error,log.warn等,但是无法匹配到log.error.self
Dictionary<string, object> headers = new Dictionary<string, object>();
headers.Add("x-match", "any"); //all/ any(只要有一个键值对匹配即可用)
headers.Add("name", "lilei");
headers.Add("age", 13);
channel.QueueDeclare("students", true);
channel.QueueBind("students", "school", "", headers);
channel.QueueDeclare("teachers", true);
channel.QueueBind("teachers", "school", "", null);
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2;
//添加Header存储的键值对
properties.Headers=new Dictionary<string,object>();
properties.Headers.Add("name", "hanmeimei");
properties.Headers.Add("age", 13);
channel.BasicPublish("school", "", properties, Encoding.UTF8.GetBytes("你好"));
var bgr = channel.BasicGet("students", true); //但是注意,如果Queue中不绑定headers,如teachers也可以拿到值
Console.WriteLine(Encoding.UTF8.GetString(bgr.Body));
Console.WriteLine("执行完毕");
Console.ReadLine();
}
- Queue:消息最终会被缓存在Queue中等待消费;
- Binding:Exchange和Queue中的绑定关系,Binding信息会存储在Exchange的查询表中,用于分发Message数据。
我们再梳理一下流程(这里不画图了,自己YY一下),建立Connection之后,消息通过Channel到达Exchange,Exchange根据绑定的RoutingKey规则将Mesage拷贝到各个Queue中去,等待消费。
生产/消费代码
这里写了一个基础操作类,用于展示如何使用RabbitMQ
/// <summary>
/// RabbitMQ的操作类。
/// </summary>
public class RabbitMQClient : IDisposable
{
/// <summary>
/// 连接,链接的开启和关闭比较耗费系统资源,建议缓存当前连接,使用完毕后,使用Close()方法释放链接。
/// </summary>
protected IConnection RabbitMQConnection { get; set; }
/// <summary>
/// 通道,在多线程中使用时,建议将isDefaultChannel设置为false。
/// </summary>
protected IModel RabbitMQChannel { get; set; }
/// <summary>
/// 声明队列。
/// </summary>
/// <param name="exchange">转发器。</param>
/// <param name="queue">队列名称。</param>
public void DeclareQueue(string exchange, string queue)
{
IModel channel = BuildChannel(true);
//这里将durable声明为true,表示会持久化到erlang自带的mnesia数据库中,当rabbitmq宕机时可以恢复
channel.QueueDeclare(queue, true, false, false, null);
if (!string.IsNullOrEmpty(exchange))
{
//在不声明routingkey的情况下,routingkey默认为queue的名称
channel.ExchangeDeclare(exchange, ExchangeType.Direct, true);
channel.QueueBind(queue, exchange, queue, null);
}
}
/// <summary>
/// 构造函数。
/// </summary>
/// <param name="connStr">链接字符串,多个链接用逗号隔开,如:“192.168.253.129,192.168.253.131”。</param>
/// <param name="uName">RabbitMQ的服务端用户名。</param>
/// <param name="uPwd">>RabbitMQ的服务端密码。</param>
public RabbitMQClient(string connStr, string uName, string uPwd)
{
RabbitMQConnection = CreateRabbitConnection(connStr, uName, uPwd);
RabbitMQChannel = RabbitMQConnection.CreateModel();
}
/// <summary>
/// 发布消息。
/// </summary>
/// <param name="msg">消息。</param>
/// <param name="exchange">转发器名称(若为空,则默认使用AMQP default)。</param>
/// <param name="queue">队列名称。</param>
/// <param name="isDefaultChannel">是否启用新的Channel。</param>
/// <returns>操作结果。</returns>
public bool PubMsg(string msg, string exchange, string queue, bool isDefaultChannel = true, bool isQueueDeclared = true)
{
if (!isQueueDeclared)
{
DeclareQueue(exchange, queue);
}
IModel channel = BuildChannel(isDefaultChannel);
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2;
channel.BasicPublish(exchange, queue, properties, Encoding.UTF8.GetBytes(msg));
return true;
}
/// <summary>
/// 订阅信息(每次取出一条),获取成功后该消息会被自动删除。
/// </summary>
/// <param name="queue">队列名称。</param>
/// <param name="isDefaultChannel">是否启用新的Channel。</param>
/// <returns>取出的消息。</returns>
public string SubMsg(string queue, bool isDefaultChannel = true, ushort qos = 0)
{
IModel channel = BuildChannel(isDefaultChannel);
if (qos != 0)
{
channel.BasicQos(0, qos, false);
}
var gr = channel.BasicGet(queue, true);
if (gr == null || gr.Body == null)
return null;
return Encoding.UTF8.GetString(gr.Body);
}
/// <summary>
/// 从队列中取出消息,消息不会自动ACK,需要在Handler手动剔除,该方法会阻塞当前进程。
/// </summary>
/// <param name="queue">队列名称。</param>
/// <param name="handler">监听到消息时的处理函数。</param>
/// <param name="isDefaultChannel">是否启用新的Channel。</param>
public void ListenMessages(string queue, EventHandler<BasicDeliverEventArgs> handler)
{
//多线程消费时不建议使用同一个Channel
IModel channel = BuildChannel(false);
List<string> bgrs = new List<string>();
var consumer = new EventingBasicConsumer(channel);
consumer.Received += handler;
do
{
var msg = channel.BasicConsume(queue, true, consumer);
if (channel.MessageCount(queue) == 0)
{
Console.WriteLine("跳出线程");
break;
}
} while (true);
}
/// <summary>
/// 订阅信息(每次取出一条),获取成功后该消息会被自动删除。
/// </summary>
/// <param name="queue">队列名称。</param>
/// <param name="autoAck">是否自动删除队列。</param>
/// <param name="isDefaultChannel">是否启用新的Channel。</param>
/// <returns>取出的消息。</returns>
public BasicGetResult SubResult(string queue, bool autoAck, bool isDefaultChannel = true)
{
IModel channel = BuildChannel(isDefaultChannel);
var bgr = channel.BasicGet(queue, autoAck);
return bgr;
}
/// <summary>
/// 消费确认。
/// </summary>
/// <param name="deliveryTag">信息传输标识。</param>
/// <param name="isDefaultChannel">是否启用新的Channel。</param>
public void SubAck(ulong deliveryTag, bool isDefaultChannel = true)
{
IModel channel = BuildChannel(isDefaultChannel);
channel.BasicAck(deliveryTag, false); //如果将multipe设置为true,则会一次性nack所有小于deliveryTag的值
}
/// <summary>
/// 消费失败处理。
/// </summary>
/// <param name="deliveryTag">信息传输标识。</param>
/// <param name="requeue">true表示重新发送,false表示丢弃。</param>
/// <param name="isDefaultChannel">是否启用新的Channel。</param>
public void SubNAck(ulong deliveryTag, bool requeue, bool isDefaultChannel = true)
{
IModel channel = BuildChannel(isDefaultChannel);
channel.BasicNack(deliveryTag, false, requeue); //如果将multipe设置为true,则会一次性ack所有小于deliveryTag的值
}
/// <summary>
/// 清除所有存储在Queue中的内容。
/// </summary>
/// <param name="queue">队列名。</param>
/// <param name="isDefaultChannel">是否启用新的Channel。</param>
public void PurgeQueue(string queue, bool isDefaultChannel = true)
{
IModel channel = BuildChannel(isDefaultChannel);
channel.QueuePurge(queue);
}
/// <summary>
/// 快速推送消息(单条)。
/// </summary>
/// <param name="msg">待推送消息主体。</param>
/// <param name="connStr">链接字符串,多个链接用逗号隔开,如:“192.168.253.129,192.168.253.131”。</param>
/// <param name="uName">RabbitMQ的服务端用户名。</param>
/// <param name="uPwd">>RabbitMQ的服务端密码。</param>
/// <param name="queue">推送的队列名称。</param>
/// <param name="exchange">转发器的名称。</param>
public static void QuickPubMsg(string msg, string connStr, string uName, string uPwd, string queue, string exchange = "")
{
using (var conn = CreateRabbitConnection(connStr, uName, uPwd))
{
var channel = conn.CreateModel();
if (!string.IsNullOrEmpty(exchange))
{
//在不声明routingkey的情况下,routingkey默认为queue的名称
channel.ExchangeDeclare(exchange, ExchangeType.Direct, true);
channel.QueueBind(queue, exchange, queue, null);
}
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2;
channel.QueueDeclare(queue, true, false, false, null);
channel.BasicPublish(exchange, queue, properties, Encoding.UTF8.GetBytes(msg));
}
}
/// <summary>
/// 快速消费消息(单条)。
/// </summary>
/// <param name="connStr">链接字符串,多个链接用逗号隔开,如:“192.168.253.129,192.168.253.131”。</param>
/// <param name="uName">RabbitMQ的服务端用户名。</param>
/// <param name="uPwd">RabbitMQ的服务端密码。</param>
/// <param name="queue">推送的队列名称。</param>
/// <returns>得到的消息。</returns>
public static string QuickSubMsg(string connStr, string uName, string uPwd, string queue)
{
using (var conn = CreateRabbitConnection(connStr, uName, uPwd))
{
var channel = conn.CreateModel();
var gr = channel.BasicGet(queue, true);
if (gr == null || gr.Body == null)
return null;
return Encoding.UTF8.GetString(gr.Body);
}
}
/// <summary>
/// 获取RabbitMQ的链接。
/// </summary>
protected static IConnection CreateRabbitConnection(string connStr, string uName, string uPwd)
{
if (string.IsNullOrEmpty(connStr))
throw new ArgumentNullException("未将对象的引用设置到对象的实例");
var factory = new ConnectionFactory()
{
UserName = uName,
Password = uPwd
};
var hosts = connStr.Trim().Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
return factory.CreateConnection(hosts);
}
protected IModel BuildChannel(bool isDefaultChannel)
{
if (!RabbitMQConnection.IsOpen)
throw new Exception("The connection is closed.");
IModel channel;
if (isDefaultChannel)
channel = RabbitMQChannel;
else
channel = RabbitMQConnection.CreateModel();
return channel;
}
/// <summary>
/// 关闭客户端。
/// </summary>
public void Close()
{
if (RabbitMQConnection != null)
RabbitMQConnection.Close();
}
/// <summary>
/// 释放资源。
/// </summary>
public void Dispose()
{
if (RabbitMQConnection != null)
{
RabbitMQConnection.Close();
RabbitMQConnection = null;
}
}
}
使用示例(不看也罢):
static void Main(string[] args)
{
Console.WriteLine("开始执行");
RabbitMQClient client = new RabbitMQClient("192.168.253.131", "admin", "admin");
client.PubMsg("Hello World", "MyExchange", "MyQueue");
var msg = client.SubMsg("MyQueue");
client.Close();
Console.WriteLine(msg);
Console.WriteLine("执行完毕");
Console.ReadLine();
}