《RabbitMQ實戰指南》整理(二)客戶端開發

書中以Java爲例進行相關簡介,這裏筆者以C#爲例進行相關的說明

一、連接RabbitMQ

如下通過給定的參數進行Rabbit的連接,創建之後Channel就可以用來發送或是接受消息了。需要注意的是Connection可以用來創建多個Channel實例,但是Channel不能再線程間共享,應用程序應當爲每一個線程開闢一個Channel,此外多線程之間共享Channel實例是非線程安全的。

using System;
using RabbitMQ.Client;

namespace ClientDevelop
{
    class Program
    {
        static void Main(string[] args)
        {
            var factory = new ConnectionFactory
            {
                UserName = "guest",
                Password = "guest",
                HostName = "localhost"
            };
            using var conn = factory.CreateConnection();//創建連接
            using var channel = conn.CreateModel();//創建信道
        }
    }
}

通常情況下,在調用Createxxx或者newxxx方法之後,可以簡單認爲Connection或Channel已經成功處於開啓狀態,而不會使用isOpen屬性來進行判斷,因爲該方法的返回值依賴於ShutdownCause的存在,有可能會產生競爭。

二、使用交換器和隊列

交換器和隊列是AMQP中high-level層面的構建模塊,應用程序需要確保在使用它們的時候就已經存在了,所以在使用前需要聲明它們。如下:

using System;
using RabbitMQ.Client;

namespace ClientDevelop
{
    class Program
    {
        private const string ExchangeName = "TestExchange";
        private const string RoutingKey = "TestRoute";
        
        static void Main(string[] args)
        {
            var factory = new ConnectionFactory
            {
                UserName = "guest",
                Password = "guest",
                HostName = "localhost"
            };
            using var conn = factory.CreateConnection();//創建連接
            using var channel = conn.CreateModel();//創建信道
            
            channel.ExchangeDeclare(ExchangeName,ExchangeType.Direct);//聲明交換器
            var queueName = channel.QueueDeclare().QueueName;//聲明隊列並獲取名稱
            channel.QueueBind(queueName,ExchangeName,RoutingKey);//綁定交換器和隊列
        }
    }
}

上述代碼展示如何使用路由鍵將隊列和交換器綁定起來,並且聲明的隊列具備如下特性:只對當前應用中的同一個Connection層面可用,同一個Connection的不同Channel可以共用,並且會在應用連接斷開時自動刪除。ExchangeDeclare和QueueDeclare方法可以根據參數的不同有不同的重載形式,可以根據自身的需要進行調整

1、ExchangeDeclare方法詳解

ExchangeDeclare方法參數詳細說明如下:

  • exchange:交換器的名稱
  • type:交換器的類型,常見的有fanout、direct和topic
  • durable:設置是否持久化,設置持久化可以將交換器存盤,在服務器重啓時不會丟失相關的信息
  • autoDelete:設置是否自動刪除,刪除的前提是至少一個隊列或者交換器與這個交換器綁定,之後所有與這個交換器綁定的隊列或者交換器都與此解綁,而不能錯誤理解爲與此交換器連接的客戶端斷開時自動刪除本交換器
  • arguments:其他一些結構化參數

其他類似的方法如ExchangeDeclareNoWait比ExchangeDeclare多設置了一個默認值爲true的noWait參數,意思爲不需要服務器返回任何值。在聲明完一個交換器之後,客戶端緊接着使用這個交換器,必然會發生異常,因此沒有特殊的緣由或場景,是不建議使用該方法的

與聲明創建交換器對應的是刪除交換器的方法ExchangeDelete(string exchange, bool ifUnused = false),其中exchange爲交換器的名稱,ifUnused 用來設置是否在交換器沒有被使用的情況下刪除,true表示只有在沒有被使用的情況下刪除,false表示無論如何都刪除

2、QueueDeclare方法詳解

不帶任何參數的QueueDeclare方法默認創建一個有RabbitMQ命名的排他的、自動刪除的、非持久化的隊列。方法參數詳細說明如下:

  • queue:隊列的名稱
  • durable:設置是否持久化
  • exclusive:設置是否排他。被設置爲排他後,該隊列僅對首次聲明它的連接可見,同一連接的不同信道是可以訪問的;如果一個連接已經聲明瞭一個排他隊列,其他連接是不允許建立同名的排他隊列的;該隊列是持久化的,一旦連接關閉或者客戶端退出,該排他隊列會被自動刪除,因而適用於一個客戶端同時發送和讀取消息的場景
  • autoDelete:設置爲是否自動刪除
  • arguments:設置隊列的其他一些參數

生產者和消費者都可以使用QueueDeclare來聲明一個隊列,但是如果消費者已經在同一個信道上訂閱了另一個隊列,就無法再聲明隊列了,必須先取消訂閱,然後將信道設置爲傳輸模式,之後才能聲明隊列。

和交換器一樣,隊列也有一個QueueDeclareNoWait方法,同樣也需要注意聲明完緊接着使用會發生異常的情況

和交換器一樣,隊列也有對應的刪除方法QueueDelete(string queue, bool ifUnused = false, bool ifEmpty = false),ifEmpty設置爲true表示在隊列爲空的情況下才能夠刪除

3、QueueBind方法詳解

  • queue:隊列名稱
  • exchange:交換器的名稱
  • routingKey:用來綁定隊列和交換器的路由鍵
  • arguments:定義綁定的一些參數

4、ExchangeBind方法詳解

不僅可以將交換器與隊列綁定,也可以將交換器與交換器綁定,方法參數與ExchangeDeclare方法類似。綁定之後Source交換器會將消息轉發到Destination交換器,某種程度上來說Destination交換器可以看作一個隊列,示例如下:

using System;
using System.Text;
using RabbitMQ.Client;

namespace ClientDevelop
{
    class Program
    {
        private const string QueueName = "TestQueue";
        private const string RoutingKey = "TestRoute";

        static void Main(string[] args)
        {
            var factory = new ConnectionFactory
            {
                UserName = "guest",
                Password = "guest",
                HostName = "localhost"
            };
            using var conn = factory.CreateConnection();//創建連接
            using var channel = conn.CreateModel();//創建信道
            
            channel.ExchangeDeclare("source", ExchangeType.Direct, false, true, null);//聲明交換器1
            channel.ExchangeDeclare("destination", ExchangeType.Fanout, false, true, null);//聲明交換器2
            channel.ExchangeBind("destination", "source", RoutingKey);//綁定兩個交換器
            
            channel.QueueDeclare(QueueName, false, false, true, null);//聲明隊列
            channel.QueueBind(QueueName, "destination", RoutingKey);//綁定隊列
            
            channel.BasicPublish("source", RoutingKey, null, Encoding.UTF8.GetBytes("exToExDemo"));//發佈消息
        }
    }
}

5、何時創建

RabbitMQ的消息存儲在隊列中,交換器的使用並不耗費服務器的性能,而隊列會,因此衡量RabbitMQ當前的QPS只需要看隊列即可。按照官方建議,生產者和消費者都應該嘗試創建隊列,這是一個很好的建議但並不適用於所有的情況。在一些已經充分預估了隊列的使用情況下,完全可以先創建好而不是在業務代碼中聲明,同時這樣做的好處是避免匹配異常的情況

三、發送消息

如果要發送一條消息,可以使用Channel的BasicPublish方法,爲了更好的控制發送,可以使用Mandatory參數,或者使用IModel類的CreateBasicProperties方法發送一些特定屬性的信息,常用的參數詳細說明如下:

  • exchange:交換器的名稱
  • routingKey:路由鍵,交換器根據路由鍵將消息存儲到對應的隊列中
  • basicProperties:消息的基本屬性集,包含許多屬性成員
  • body:消息體,真正需要發送的消息

四、消費消息

RabbitMQ的消費模式有兩種,推模式和拉模式,推模式採用Basic.Consume進行消費,拉模式則調用Basic.Get進行消費

1、推模式

在推模式中,可以使用持續訂閱模式來消費消息,不同的訂閱採用不同的消費者標籤來區分彼此,在同一個信道中的消費者也需要通過唯一的消費者標籤以作區分

服務端:

using System;
using System.Text;
using RabbitMQ.Client;

namespace Producer
{
    class Program
    {
        private const string ExchangeName = "TestExchange";
        private const string QueueName = "TestQueue";
        private const string RoutingKey = "TestRoute";

        static void Main(string[] args)
        {
            var factory = new ConnectionFactory
            {
                HostName = "localhost",
                UserName = "guest",
                Password = "guest"
            };

            using var conn = factory.CreateConnection();//創建連接
            using var channel = conn.CreateModel();//創建信道

            channel.ExchangeDeclare(ExchangeName, ExchangeType.Direct);//聲明交換器
            channel.QueueDeclare(QueueName, false, false, false, null);//聲明隊列
            channel.QueueBind(QueueName, ExchangeName, RoutingKey,null);//綁定交換器和隊列

            //發佈消息
            const string message = "Hello World";
            channel.BasicPublish(ExchangeName, RoutingKey, null, Encoding.UTF8.GetBytes(message));
            Console.WriteLine(" [x] Sent {0}", message);

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }
}

客戶端:

using System;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

namespace Consumer
{
    class Program
    {
        private const string ExchangeName = "TestExchange";
        private const string QueueName = "TestQueue";
        private const string RoutingKey = "TestRoute";
        private const string ConsumerTag = "TestTag";

        static void Main(string[] args)
        {
            var factory = new ConnectionFactory
            {
                HostName = "localhost",
                UserName = "guest",
                Password = "guest"
            };

            using var connection = factory.CreateConnection();
            using var channel = connection.CreateModel();

            channel.ExchangeDeclare(ExchangeName, ExchangeType.Direct);//聲明交換器
            channel.QueueDeclare(QueueName, false, false, false, null);//聲明隊列
            channel.QueueBind(QueueName, ExchangeName, RoutingKey, null);//綁定交換器和隊列

            Console.WriteLine("Waiting for message...");

            //推模式處理數據
            channel.BasicQos(0, 1, false); //未收到消費端確認時不再分發消息
            var consumer = new EventingBasicConsumer(channel);
            consumer.Received += (model, ea) =>
            {
                var body = ea.Body.ToArray();
                var message = Encoding.UTF8.GetString(body);
                Console.WriteLine(" [x] {0}", message);
                channel.BasicAck(ea.DeliveryTag, false);
            };
            channel.BasicConsume(QueueName, false, ConsumerTag, consumer);

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }
}

在BasicConsume方法中我們顯式地設置autoAck爲false,然後在接受到消息後進行顯式ack操作,這樣做可以防止消息不必要的丟失。BasicConsume方法有多個重載,常用的參數說明如下:

  • queue:隊列的名稱
  • autoAck:設置是否自動確認,建議設置成false;
  • consumerTag:消費者標籤,用來區分多個消費者;
  • noLocal:設置成true則表示不能將同一個Connection中生產者發送的消息傳遞給這個Connection中的消費者;
  • exclusive:設置是否排他;
  • arguments:設置消費者的其他參數
  • consumer:設置消費者的回調函數,用來處理RabbitMQ推送過來的消息,比如DefaultConsumer

和生產者一樣,消費者客戶端同樣需要考慮線程安全的問題,消費者客戶端的callback會被分配到Channel不同的線程上,這意味者消費者客戶端可以安全地調用這些阻塞的方法。最常用的做法是一個Channel對應一個消費者,若存在多個那麼其他消費者的callback會被阻塞。

2、拉模式

拉模式通過channel.basicGet方法可以單條地獲取消息,其返回值是GetRespone。如果autoAck爲false,那麼同樣需要調用channel.BasicAck來確認消息被接受

using System;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

namespace Consumer
{
    class Program
    {
        private const string ExchangeName = "TestExchange";
        private const string QueueName = "TestQueue";
        private const string RoutingKey = "TestRoute";
        private const string ConsumerTag = "TestTag";

        static void Main(string[] args)
        {
            var factory = new ConnectionFactory
            {
                HostName = "localhost",
                UserName = "guest",
                Password = "guest"
            };

            using var connection = factory.CreateConnection();
            using var channel = connection.CreateModel();

            channel.ExchangeDeclare(ExchangeName, ExchangeType.Direct);//聲明交換器
            channel.QueueDeclare(QueueName, false, false, false, null);//聲明隊列
            channel.QueueBind(QueueName, ExchangeName, RoutingKey, null);//綁定交換器和隊列

            Console.WriteLine("Waiting for message...");

            //拉模式處理數據
            var res = channel.BasicGet(QueueName, false);
            Console.WriteLine(Encoding.Default.GetString(res.Body.ToArray()));
            channel.BasicAck(res.DeliveryTag, false);

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }
}

注:Basic.Consume將信道設置爲投遞模式,直到取消隊列的訂閱爲止,在投遞期間,RabbitMq會不斷地推送消息給消費者。如果只想從隊列獲得單條消息,可以使用Basic.Get進行消費,但不能將其放在循環中替代Basic.Consume,這樣做會嚴重影響性能。要實現高吞吐量,消費者理應使用Basic.Consume方法

五、消費端的確認與拒絕

爲保證消息從隊列可靠地到達消費者,RabbitMQ提供了消息確認機制。消費者在訂閱隊列時,可以指定autoAck參數,當其爲false時,RabbitMQ會等待消費者顯式地回覆信號後才從內存或磁盤中移除消息(實際上是先打上刪除標記再刪除);此外對於服務端而言,隊列中的消息會分爲兩種,一種是等待傳遞給消費者的消息,一種是已經傳遞給消費者的消息,如果RabbitMQ一直沒有收到消費者的確認信號,並且消息的消費者已經斷開連接,RabbitMQ會安排消息重新進入隊列進行投遞。RabbitMQ判斷是否需要重新已投遞的唯一依據是某消息的對應的消費者是否已經斷開。可以參照Web管理頁面中的Ready和Uncaked字段進行查看

如果像明確拒絕當前的消息而不是確認,可以使用BasicReject(ulong deliveryTag, bool requeue)命令,其中deliverTag是消息的編號,requeue爲true則RabbitMQ會重新將這條消息存入隊列,以便發送給下一個訂閱的消費者,如果爲false該消息會被移除。BasicReject命令一次只能拒絕一條消息,如果想要批量拒絕,可以使用BasicNack(ulong deliveryTag, bool multiple, bool requeue)方法,multiple參數爲false時表示拒絕編號爲deliveryTag的這一條消息,爲true時表示拒絕deliveryTag編號之前所有未被當前消費者確認的消息。

BasicRecover(bool requeue)方法可以用來請求RabbitMQ重新發送給未被確認的消息,requeue爲true時未被確認的消息會被重新加入到隊列中,如果爲false會被分配給與之前相同的消費者

六、關閉連接

Connection關閉時,Channel也會自動關閉。AMQP協議中的Connection和Channel採用相同的方式來管理網絡失敗、內部錯誤和顯式地關閉連接,兩者的生命週期如下:

  • Open:開啓狀態,代表當前對象可用;
  • Closing:正在關閉的狀態,當前對象被顯式的調用關閉方法時會產生關閉請求對內部對象進行相應的操做,並等待這些關閉操作的完成;
  • Closed:已經關閉的狀態,當前對象已經接受到所有內部對象已完成關閉動作的通知,並且其自身也已關閉
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章