使用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行之間完成對消息有用的東西。