在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.EventHandlerEventHandler
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