使用RabbitMQ创建AMQP示例应用程序
RabbitMQ提供了一个名为RabbitMQ.Client的.NET客户机库。在这里可以找到关于客户端的文档。
与所有新技术一样,文档是空白的,API是原始的和无组织的,几乎所有的东西都可以从一个对象——IModel中访问。
首先要做的事情之一是:
通过围绕最常见的用例组织功能,使其更容易使用。
- 发布消息
- 接受消息
将声明与功能分开
- 创建exchange(交换机),queue(队列)和绑定过程是独立的。
- 发布和接受消息的过程。
使用app.config文件进行配置。
在我们设计上面描述的简化api之前,让我们简要地介绍一下RabbitMQ
的功能,
客户提供下面是一些示例代码,以突出RabbitMQ
的方式。
客户端API允许我们与RabbitMQ
服务器进行交互。
//Creating Connections
RabbitMQ.Client.ConnectionFactory factory = new RabbitMQ.Client.ConnectionFactory();
factory.Endpoint = new AmqpTcpEndpoint(server);
factory.UserName = userName;
factory.Password = password;
IConnection Connection = factory.CreateConnection();
要连接到RabbitMQ
服务器,必须在客户机和服务器之间建立连接,
如上所示。第7行创造了我们将在下面的例子中使用的连接。
连接并不是我们要用来与服务器通信的东西,
相反,我们使用的是连接对象为我们创建的专用通信通道IModel
对象。
IModel
是客户端和代理之间的通信通道,可以建立多个通道。
RabbitMQ
的一个奇怪之处是IModel
无法在线程之间共享对象,
我们需要确保在设计应用程序时考虑到这一点。
下一节将介绍通道对象提供的一些最常用的功能。
using (IModel channel = Connection.CreateModel())
{
... // Object declaration //
channel.ExchangeDeclare( /* parameters */);
channel.QueueDeclare( /* parameters */);
channel.QueueBind( /* parameters */);
// Publishing
channel.BasicPublish( /* object parameters */);
// Synchronous receiving
channel.BasicGet( /* parameters */);
// Acknowledging a message
channel.BasicAck( /* parameters */);
// Reject a message
channel.BasicReject( /* parameters */);
// Requeue a message
channel.BasicReject( /* parameters - make sure to set requeue = true */);
}
奇怪的是,Requeueing消息使用了该BasicReject(…)方法。
BasicReject有一个requeue参数,通过设置为true,消息被重新排队。
创建专用AMQP App配置部分
当我们使用RabbitMQ时,我们必须配置服务器以适当地接收和分发消息给客户端,
以及配置我们的应用程序以从适当的交换或队列发送或接收消息。
最简单的方法是使用配置文件来执行此操作,
这也允许我们在应用程序部署到各种环境时轻松更改这些值。
我选择为RabbitMQ创建一个专用配置部分,因为需要配置很多,
我想将所有RabbitMQ配置元素保存在一起。结果如下所示
(代码可以在CodePlex项目中找到:codeplex_link)
<configuration>
<configsections>
<sectiongroup name="AMQPConnection">
<section name="ConnectionSettings"
type="Sample.Configuration.AMQP.Config.ConnectionSection,
Sample.Configuration.AMQP" />
</sectiongroup>
<sectiongroup name="AMQPAdmin">
<section name="AMQPObjectsDeclaration"
type="Sample.Configuration.AMQP.Config.AMQPObjectsDeclarationSection,
Sample.Configuration.AMQP" allowlocation="true"
allowdefinition="Everywhere" />
</sectiongroup>
</configsections>
<amqpadmin>
<amqpobjectsdeclaration>
<exchangelist>
<add name="orders" type="topic"
durable="true" autodelete="false" />
</exchangelist>
<queuelist>
<add name="uk_orders" durable="true"
autodelete="false" />
</queuelist>
<bindinglist>
<add subscriptionkey="order.uk.#"
queue="uk_orders" exchange="orders" />
</bindinglist>
</amqpobjectsdeclaration>
</amqpadmin>
<amqpconnection>
<connectionsettings>
<connection name="connection"
username="guest" server="devserver-rr1"
password="guest" />
<publisher exchange="orders" />
<asyncreceiver queue="uk_orders" maxthreads="4" />
</connectionsettings>
</amqpconnection>
</configuration>
按行描述配置文件
- 4,7:将配置节解释器映射到项目中的相应类。每个XML元素代表一个对象,必须进行解释
- 13.创建一个称为持久的订单的主题交换(意味着它将持续服务器重新启动)并且不是自动删除
- (如果
autodelete
标志设置为true
,则在所有客户端完成发布后将删除交换)。 - 16.创建一个名为
uk_orders
的队列来表示英国客户的所有订单。队列设置为持久而不是自动删除 - 使用
uk_orders
订阅密钥将uk_orders
队列绑定到orders
交换order.uk.#
。 - 这样,所有以
order.uk
开头的订单都将以该队列结束
- 使用
- 25:配置与服务器的连接字符串。
- 26 ,. 配置发布者将消息发布到订单交换
- 27.配置异步接收器使用4个线程在uk_orders它们到达时从
uk_orders
队列中获取订单。
配置部分可以接受每种类型的AMQP对象(交换,队列和绑定)的列表。
RabbitAdmin
那么在用于创建交换,队列及其绑定时,它会是什么样子?
namespace Sample.Configuration.AMQP.Admin
{
public class RabbitAdmin
{
internal static void InitializeObjects(IConnection Connection)
{
var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var objects = config.GetSection
("AMQPAdmin/AMQPObjectsDeclaration") as AMQPObjectsDeclarationSection;
if (objects != null)
{
Parallel.For(0, objects.ExchangeList.Count, i =>
{
using (IModel channel = Connection.CreateModel())
{
var exchange = objects.ExchangeList[i];
channel.ExchangeDeclare(exchange.Name,
exchange.Type.ToString(), exchange.Durable, exchange.AutoDelete, null);
}
});
Parallel.For(0, objects.QueueList.Count, i =>
{
using (IModel channel = Connection.CreateModel())
{
var queue = objects.QueueList[i];
channel.QueueDeclare(queue.Name, queue.Durable,
queue.Exclusive, queue.AutoDelete, null);
}
});
Parallel.For(0, objects.BindingList.Count, i =>
{
using (IModel channel = Connection.CreateModel())
{
var binding = objects.BindingList[i];
channel.QueueBind(binding.Queue,
binding.Exchange, binding.SuscriptionKey);
}
});
}
}
}
在RabbitAdmin类创建的交流,队列及其绑定。
它接受一个连接对象并使用它与RabbitMQ服务器进行通信。
在InitializeObjects使用平行for循环创建每种类型的对象。
首先创建交换,然后将队列绑定声明为最后一个,因为它们需要队列和交换来绑定两者。
GatewayFactory
在GatewayFactory创建与服务器的连接,调用RabbitAdmin声明中的所有对象,
并提供方法来创建发布者和听众同步助手对象。消息传递给发布者并从队列接收,
消息对象是一个非常简单的对象,它包含一个标题和一个正文。
/// <summary>
/// The object encapsulating all the common message properties
/// transmitted to and received from the message bus.
/// </summary>
public class Message {
public IBasicProperties Properties { get; set;}
public byte[] Body { get; set; }
public string RoutingKey { get; set; }
}
正文是一个简单的字节数组,您的内部数据结构被序列化。
为了能够一般地处理数据结构和消息之间的转换,包中包含以下委托和接口。
namespace Sample.Configuration.AMQP.Gateway
{
public delegate Message ConvertToMessage(IModel channel, object packetToSend);
public interface IConvertToMessage {
Message ConvertObjectToMessage( IModel channel, object packetToSend);
}
}
系统的客户端负责从其数据结构转换为消息结构。
Publishing
using (var gf = new GatewayFactory())
{
var mc = new Sample.Configuration.AMQP.Gateway.Converter.StringToMessageConverter();
var publisher = gf.GetPublisher(mc.ConvertObjectToMessage);
publisher.Publish("Hello world");
}
随着配置的到位,出版是一个四线的事情。
只需创建一个GatewayFactory,在内部读取配置文件并设置所有内容。
然后请求发布者并传递objectToMessage转换处理程序。
该库附带了一个默认string的消息转换器,它在第5行中使用,对XML文档很有用。
否则,必须创建自定义转换器。
为了让您了解如何创建转换器,让我们来看看string转换器。
public class StringToMessageConverter : IConvertToMessage
{
public static readonly string PLAIN_TEXT = "text/plain";
public const string _defaultCharSet = "utf-8";
public string CharSet { get; set; }
public StringToMessageConverter()
{
CharSet = _defaultCharSet;
}
public virtual Message ConvertObjectToMessage
(RabbitMQ.Client.IModel channel, object packetToSend)
{
var properties = channel.CreateBasicProperties();
var bytes = Encoding.GetEncoding(CharSet).GetBytes((string)packetToSend);
properties.ContentType = PLAIN_TEXT;
properties.ContentEncoding = CharSet;
return new Message()
{ Body = bytes, Properties = properties, RoutingKey = string.Empty };
}
}
转换为消息主要是将需要发送的任何内容转换为位数组,让接收者知道内容是什么并设置RoutingKey。
The Asynchronous Receiver (异步接收)
异步接收消息就像发布一样简单。接收方必须将消息的状态与服务器通信 - 它可以被确认,拒绝或重新排队。
class Program
{
static void Main(string[] args)
{
var mp = new MessageProcessor();
using (var cf = new GatewayFactory())
{
cf.GetAsyncReceiver(mp.ConsumeMessage);
}
}
}
class MessageProcessor : IMessageConsumer
{
public void ConsumeMessage
(Message message, RabbitMQ.Client.IModel channel, DeliveryHeader header)
{
try
{
var str = ConvertFromMessageToString(message);
channel.BasicAck(header.DeliveryTag, false);
}
catch (Exception ex)
{
channel.BasicReject(header.DeliveryTag, true);
}
}
public string ConvertFromMessageToString(Message message)
{
var content = string.Empty;
if (message.Properties.ContentType == StringToMessageConverter.PLAIN_TEXT)
{
var encoding = Encoding.GetEncoding
(message.Properties.ContentEncoding ?? "utf-8");
var ms = new MemoryStream(message.Body);
var reader = new StreamReader(ms, encoding, false);
content = reader.ReadToEnd();
}
return content;
}
}
在第8行,我们将消息处理程序传递给异步接收器。
异步接收器是多线程的,并且处理程序应该是无状态的,以便它可以被多个线程同时使用。
响应服务器是通过IModel对象完成的。Delivery标头包含a deliverytag和a flag,表示这是否是第一次传递。
可以在第19行和第20行之间完成对消息有用的东西。