【C#】從RabbitMQ的消費者事件窺.NET標準事件

rabbitMQ中,官方文檔中,接收消息最方便且推薦的方法:使用IBasicConsumer消費者接口設置訂閱messages到達隊列後將自動發送,只要訂閱了Received事件,就可以從中接收到隊列消息,而不必主動請求。實現這種消費者(發佈訂閱)模式 ,.NET/C# Client API是通過C#事件。事件的本質就是多播委託。

1.RabbitMQ中的事件

首先我們來看一下在RabbitMQ的使用方式:

1.1 訂閱事件

訂閱消費者的received事件

var consumer = new EventingBasicConsumer(channel);
consumer.Received += (ch, ea) =>
                {
                    var body = ea.Body.ToArray();
                    // copy or deserialise the payload
                    // and process the message
                    // ...
                    channel.BasicAck(ea.DeliveryTag, false);
                };
// this consumer tag identifies the subscription
// when it has to be cancelled
String consumerTag = channel.BasicConsume(queueName, false, consumer);

1.2 定義事件

再來看一下源碼,省略部分源碼

namespace RabbitMQ.Client.Events
{
    ///<summary>Experimental class exposing an IBasicConsumer's
    ///methods as separate events.</summary>
    public class EventingBasicConsumer : DefaultBasicConsumer
    {
        public EventingBasicConsumer(IModel model) : base(model)
        {
        }

        public event EventHandler<BasicDeliverEventArgs> Received;

        ///<summary>
        /// Invoked when a delivery arrives for the consumer.
        /// </summary>
        /// <remarks>
        /// Handlers must copy or fully use delivery body before returning.
        /// Accessing the body at a later point is unsafe as its memory can
        /// be already released.
        /// </remarks>
        public override void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, IBasicProperties properties, ReadOnlyMemory<byte> body)
        {
            base.HandleBasicDeliver(consumerTag, deliveryTag, redelivered, exchange, routingKey, properties, body);
            Received?.Invoke(
                this,
                new BasicDeliverEventArgs(consumerTag, deliveryTag, redelivered, exchange, routingKey, properties, body));
        }
    }
}

1.3 事件傳遞信息

其中received爲事件,當調用HandleBasicDeliver方法便會觸發事件,這也是事件特定,只能在類的內部調用,並傳遞事件源,及事件傳遞信息BasicDeliverEventArgs類,如下

    public class BasicDeliverEventArgs : EventArgs
    {
        ///<summary>Default constructor.</summary>
        public BasicDeliverEventArgs()
        {
        }

        ///<summary>Constructor that fills the event's properties from
        ///its arguments.</summary>
        public BasicDeliverEventArgs(string consumerTag,
            ulong deliveryTag,
            bool redelivered,
            string exchange,
            string routingKey,
            IBasicProperties properties,
            ReadOnlyMemory<byte> body)
        {
            ConsumerTag = consumerTag;
            DeliveryTag = deliveryTag;
            Redelivered = redelivered;
            Exchange = exchange;
            RoutingKey = routingKey;
            BasicProperties = properties;
            Body = body;
        }

        ///<summary>The content header of the message.</summary>
        public IBasicProperties BasicProperties { get; set; }

        ///<summary>The message body.</summary>
        public ReadOnlyMemory<byte> Body { get; set; }

        ///<summary>The consumer tag of the consumer that the message
        ///was delivered to.</summary>
        public string ConsumerTag { get; set; }

        ///<summary>The delivery tag for this delivery. See
        ///IModel.BasicAck.</summary>
        public ulong DeliveryTag { get; set; }

        ///<summary>The exchange the message was originally published
        ///to.</summary>
        public string Exchange { get; set; }

        ///<summary>The AMQP "redelivered" flag.</summary>
        public bool Redelivered { get; set; }

        ///<summary>The routing key used when the message was
        ///originally published.</summary>
        public string RoutingKey { get; set; }
    }

2.標準的.NET事件模式

接下來我們看一下標準的.NET事件模式

2.1 定義事件傳遞信息類EventArgs

public class PriceChangeEventArgs:System.EventArgs
{
    public readonly decimal LastPrice;
    public readonly decimal NewPrice;
    public PriceChangeEventArgs(decimal lastPrice,decimal newPrice)
    {
        LastPrice=lastPrice;
        NewPrice=newPrice;
    }
}
  • System.EventArgs是.net framework中預定義的類,除了靜態的Empty屬性之外,沒有其他成員
    • EventArgs爲事件傳遞信息類的基類
    • 繼承這個基類來自定義事件傳遞信息類

2.2* 爲事件定義委託EventHandler

public delegate void PriceChangedEventHandler<TEventArgs>(object source,TEventArgs e) where TEventArgs:EventArgs;

事件可以說是依賴委託的,實質本來也就是一種類型安全的委託,要想定義事件,必定指定委託,對於能夠定義事件的委託,有如下要求:

  • 返回類型是void
  • 接收兩個參數,第一個參數類型是object,第二個參數類型是EventArgs的子類。
    • 第一個參數表示事件的廣播者(觸發事件的對象)
    • 第二個參數包含事件需要傳遞的信息
  • 名稱必須以EventHandler結尾

在最新的.NET Core事件模式下,爲了事件傳遞參數更更加的靈活,已經不再要求 TEventArgs 必須是派生自 System.EventArgs 的類,上面的代碼就可以不再繼承System.EventArgs

public delegate void PriceChangedEventHandler<TEventArgs>(object source,TEventArgs e);

然後上面的信息傳遞類也是可以改造的。

public class PriceChangeEventArgs
{
    public readonly decimal LastPrice;
    public readonly decimal NewPrice;
    public PriceChangeEventArgs(decimal lastPrice, decimal newPrice)
    {
        LastPrice = lastPrice;
        NewPrice = newPrice;
    }
}

其實.net已經爲我們定義了一個泛型委託System.EventHandler與非泛型委託EventHandler

public delegate void EventHandler<TEventArgs>(object source,TEventArgs e);
public delegate void EventHandler(object? sender, EventArgs e);

這裏使用自定義的委託,還是.NET預定義委託,都是可以的。如果我們使用.NE預定義委託,本小節就可以省略,可有可無。

建議使用預定義委託,畢竟已經有現成的

2.3 使用委託定義事件event

2.3.1 針對自定義委託

public event PriceChangedEventHandler<PriceChangeEventArgs> PriceChanged;

2.3.2 針對預定義的泛型委託

public event EventHandler<PriceChangeEventArgs> PriceChanged;

2.3.3 針對預定義的非泛型委託

public event EventHandler PriceChanged;

2.4 觸發事件的方法On

protected virtual void OnPriceChanged(PriceChangeEventArgs e)
{
    PriceChanged?.Invoke(this,e);
}
  • 標準模式下要求方法名必須和事件一致,前面再加上On,接收一個EventArgs參數
    • rabbitMQ的源碼中,並沒有遵從這種標準,而是使用的Handle前綴。如HandleBasicDeliver,大家靈活使用。
  • 這裏記住 PriceChanged?.Invoke(this,e);事件名?.Invoke(this,事件傳遞參數),等價下面代碼。
if(PriceChanged!=null)
{
    PriceChanged(this,e);
}

2.6 觸發事件

定義了觸發事件的方法,還需要定義觸發事件的條件,事件的本質是類型安全的委託,實質也是封裝了一個多播委託,只是功能上比委託有了跟個多限制,只能在定義事件的類的內部直接調用事件。

public decimal Price
{
    get { return price; }
    set
    {
        if (price == value) return;
        decimal oldPrice = price;
        price = value;
        OnPriceChanged(new PriceChangeEventArgs(oldPrice, price));
    }
}

上述示例中,在屬性的set訪問器,中觸發事件:當價格發生變化時,將觸發股價變化事件(PriceChanged)。

2.5 事件的使用(訂閱事件)

沒有訂閱的事件,永遠不會觸發(因爲爲null)。所以我們需要訂閱事件。

//  一個股票類
//  股票的價格變化訂閱事件
static void Main(string[] args)
{
    Stock st = new Stock("股票");
    st.Price = 100;
    st.PriceChanged += stock_PriceChanged;
    st.Price = 200;
    Console.WriteLine("Hello World!");
}
static void stock_PriceChanged(object sender, PriceChangeEventArgs e)
{
    if ((e.NewPrice - e.LastPrice) / e.LastPrice > 0.1M)
    {
        Console.WriteLine("Alert,10% stock price increase");
    }
}

2.5.1 使用匿名方法訂閱事件

st.PriceChanged += delegate(object o, PriceChangeEventArgs e)  
{  
    var lastPrice = e.LastPrice;
    var newPrice = e.NewPrice;
    //...
};  

2.5.2 使用lambda表達式訂閱事件

使用+=操作符訂閱事件,更多的實際操作是使用lambda表達式,用於接收事件源參數和事件傳遞的信息:

st.PriceChanged += (sender, e) =>
{
    var lastPrice = e.LastPrice;
    var newPrice = e.NewPrice;
    //...
};

3.總結

C#通過事件機制實現線程間(進程內)的通信。讓我想起了MediatR這個庫。

  • 事件的定義是在一個類中
  • 事件的註冊是在另一個類中
  • 在定義事件的類中觸發
  • 傳遞參數至註冊類中使用。

3.1 什麼情況下使用事件?

當我們學習到某種方法總會有疑問,到底什麼時候使用事件,事件能夠辦到的,看起來委託也能辦到。

當事件源將在很長一段時間內觸發事件,基於事件的設計就顯得非常自然,例如RabbitMQ的消費者Recived事件,一旦訂閱了事件,在當前程序的整個生命週期,事件源隨時都可以觸發事件。在CS程序中,UI控件設計示例基本也是基於各種事件。

3.2 事件定義與使用

  • 信息傳遞類:public class xxxEventArgs{}

    • 可繼承EventArgs,也可以自定義
  • *委託:public delegate void EventHandler<xxxEventArgs>(object source,xxxEventArgs e)

    • 使用.NET預定義類(這一步可以省略)
  • 事件:public event EventHandler<xxxEventArgs> EventName;

  • 觸發事件的方法:

    protected virtual void OnEventName(xxxEventArgs e)
    {
      	eventName?.Invoke(this,e);
    }
    
  • 觸發事件:在特定的需求下,執行OnEventName(new xxxEventArgs(){})

  • 訂閱(註冊)事件:xx.EventName+=(src,e)=>{}

參考鏈接

https://www.bilibili.com/video/BV1Ht41137R1

https://docs.microsoft.com/zh-cn/dotnet/csharp/event-pattern

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章