前言:在Remoting中處理事件其實並不複雜,但其中有些技巧須要你去挖掘出來。正是這些技巧,彷彿森嚴的壁壘,讓許多人望而生畏,或者是不知所謂,最後放棄了事件在Remoting的使用。關於這個主題,在網上也有不少討論,相關的技術文章也很多,遺憾的是,不少文章概述的都不太全面。我在研究Remoting的時候,也對事件處理髮生了興趣。通過參考相關的書籍、文檔,並通過反覆的試驗,深信本身可以把這個問題闡述清楚了。
本文對於Remoting和事件的基礎知識再也不介紹,有興趣的能夠看個人系列文章,或查閱相關的技術文檔。html
本文示例代碼下載:安全
應用Remoting技術的分佈式處理程序,一般包括三部分:遠程對象、服務端、客戶端。所以從事件的方向上看,就應該有三種形式:
一、服務端訂閱客戶端事件
二、客戶端訂閱服務端事件
三、客戶端訂閱客戶端事件ide
服務端訂閱客戶端事件,即由客戶端發送消息,服務端捕捉該消息,而後響應該事件,至關於下級向上級發傳真。反過來,客戶端訂閱服務端事件,則是由服務端發送消息,此時,全部客戶端均捕獲該消息,激發事件,至關因而一個系統廣播。而客戶端訂閱客戶端事件呢?就相似於聊天了。由某個客戶端發出消息,其餘客戶端捕獲該消息,激發事件。惋惜的是,我並無找到私聊的解決辦法。當客戶端發出消息後,只要訂閱了該事件的,都會得到該信息。post
然而無論是哪種方式,究其實質,真正包含事件的仍是遠程對象。原理很簡單,咱們想想,在Remoting中,客戶端和服務端傳遞的內容是什麼呢?毋庸置疑,是遠程對象。所以,咱們傳遞的事件消息,天然是被遠程對象所包裹。這就像EMS快遞,遠程對象是運送信件的汽車,而事件消息就是汽車所裝載的信件。至於事件傳遞的方向,只是發送者和訂閱者的角色發生了改變而已。idea
1、 服務端訂閱客戶端事件
服務端訂閱客戶端事件,相對比較簡單。咱們就以發傳真爲例。首先,咱們必須具有傳真機和要傳真的文件,這就比如咱們的遠程對象。並且這個傳真機上必須具有「發送」的操做按鈕。這就比如是遠程對象中的一個委託。當客戶發送傳真時,就須要在客戶端上激活一個發送消息的方法,這就比如咱們按了「發送」按鈕。消息發送到服務端後,觸發事件,這個事件正是服務端訂閱的。服務端得到該事件消息後,再處理相關業務。這就比如接收傳真的人員,當傳真收到後,會聽到接通的聲音,此時選擇「接收」後,該消息就被捕獲了。spa
如今,咱們就來模擬這個流程。首先定義遠程對象,這個對象處理的應該是一個發送傳真的業務:
首先是遠程對象的公共接口(Common.dll):3d
public delegate void FaxEventHandler(string fax); public interface IFaxBusiness { void SendFax(string fax); }
注意,在公共接口程序集中,定義了一個公共委託。
而後咱們定義具體處理傳真業務的遠程對象類(FaxBusiness.dll),在這個類中,先要添加對公共接口程序集的引用:
public class FaxBusiness:MarshalByRefObject,IFaxBusiness { public static event FaxEventHandler FaxSendedEvent; #region public void SendFax(string fax) { if (FaxSendedEvent != null) { FaxSendedEvent(fax); } } #endregion public override object InitializeLifetimeService() { return null; } }
這個遠程對象中,事件的類型就是咱們在公共程序集Common.dll中定義的委託類型。SendFax實現了接口IFaxBusiness中的方法。這個方法的簽名和定義的委託一致,它調用了事件FaxSendedEvent。
特殊的地方是咱們定義的遠程對象最好是重寫MarshalByRefObject類的InitializeLifetimeService()方法。返回null值代表這個遠程對象的生命週期爲無限大。爲何要重寫該方法呢?道理不言自明,若是生命週期不進行限制的話,一旦遠程對象的生命週期結束,事件就沒法激活了。
接下來就是分別實現客戶端和服務端了。服務端是一個Windows應用程序,界面以下:
咱們在加載窗體的時候,註冊通道和遠程對象:
private void ServerForm_Load(object sender, System.EventArgs e) { HttpChannel channel = new HttpChannel(8080); ChannelServices.RegisterChannel(channel); RemotingConfiguration.RegisterWellKnownServiceType( typeof(FaxBusiness),"FaxBusiness.soap",WellKnownObjectMode.Singleton); FaxBusiness.FaxSendedEvent += new FaxEventHandler(OnFaxSended); }
咱們採用的是SingleTon模式,註冊了一個遠程對象。注意看,這段代碼和通常的Remoting服務端有什麼區別?對了,它多了一行註冊事件的代碼:
FaxBusiness.FaxSendedEvent += new FaxEventHandler(OnFaxSended);
這行代碼,就比如咱們服務端的傳真機,一直切換爲「自動」模式。它會一直監聽着來自客戶端的傳真信息,一旦傳真信息從客戶端發過來了,則響應事件方法,即OnFaxSended方法:
public void OnFaxSended(string fax) { txtFax.Text += fax; txtFax.Text += System.Environment.NewLine; }
這個方法很簡單,就是把客戶端發過來的Fax顯示到txtFax文本框控件上。
而客戶端呢?仍然是一個Windows應用程序。代碼很是簡單,首先爲了簡便其見,咱們仍然讓它在裝載窗體的時候,激活遠程對象:
private void ClientForm_Load(object sender, System.EventArgs e) { HttpChannel channel = new HttpChannel(0); ChannelServices.RegisterChannel(channel); faxBus = (IFaxBusiness)Activator.GetObject(typeof(IFaxBusiness), "http://localhost:8080/FaxBusiness.soap"); }
呵呵,能夠說客戶端激活對象的方法和普通的Remoting客戶端應用程序沒有什麼不一樣。該寫傳真了!咱們在窗體上放一個文本框對象,改其Multiline屬性爲true。再放一個按鈕,負責發送傳真:
private void btnSend_Click(object sender, System.EventArgs e) { if (txtFax.Text != String.Empty) { string fax = "來自" + GetIpAddress() + "客戶端的傳真:" + System.Environment.NewLine; fax += txtFax.Text; faxBus.SendFax(fax); } else { MessageBox.Show("請輸入傳真內容!"); } } private string GetIpAddress() { IPHostEntry ipHE = Dns.GetHostByName(Dns.GetHostName()); return ipHE.AddressList[0].ToString(); }
在這個按鈕單擊事件中,只須要調用遠程對象faxBus的SendFax()方法就OK了,很是簡單。但是慢着,爲何你的代碼有這麼多行啊?其實,沒有什麼奇怪的,我只是想到發傳真的客戶可能會不少。爲了不服務端人員犯糊塗,搞不清楚是誰發的,因此要求在傳真上加上各自的簽名,也就是客戶端的IP地址了。既然要得到計算機的IP地址,請必定要記得加上對DNS的命名空間引用:
using System.Net;
由於咱們嚴格按照分佈式處理程序的部署方式,因此在客戶端只須要添加公共程序集(Common.dll)的引用就能夠了。而在服務端呢,則必須添加公共程序集和遠程對象程序集二者的引用。
OK,程序完成,咱們來看看這個簡陋的傳真機:
客戶端:
嘿嘿,作夢都想放假啊。好的,傳真寫好了,發送吧!再看看服務端,great,老闆已經收到個人請假條傳真了!
2、 客戶端訂閱服務端事件
嘿嘿,吃甘蔗要先吃甜的一段,作事情我也喜歡先作容易的。如今,好日子過去了,該吃點苦頭了。咱們先回憶一下剛纔的實現方法,再來思考怎麼實現客戶端訂閱服務端事件?
在前一節,事件被放到遠程對象中,客戶端激活對象後,就能夠發送消息了。而在服務端,只須要訂閱該事件就能夠。如今思路應該反過來,由客戶端訂閱事件,服務端發送消息。就這麼簡單嗎?先不要高興得太早。咱們想想,發送消息的任務是誰來完成的?是遠程對象。而遠程對象是何時建立的呢?咱們仔細思考Remoting的幾種激活方式,無論是服務端激活,仍是客戶端激活,他們的工做原理都是:客戶端決定了服務器建立遠程對象實例的時機,例如調用了遠程對象的方法。而服務端所做的工做則是註冊該遠程對象。
回憶這三種激活方式在服務端的代碼:
SingleCall激活方式:
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(BroadCastObj),"BroadCastMessage.soap",
WellKnownObjectMode.Singlecall);
SingleTon激活方式:
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(BroadCastObj),"BroadCastMessage.soap",
WellKnownObjectMode.Singleton);
客戶端激活方式:
RemotingConfiguration.ApplicationName = 「BroadCastMessage.soap」
RemotingConfiguration.RegisterActivatedServiceType(typeof(BroadCastObj));
請注意Register這個詞語,它表達的含義就是註冊。也就是說,在服務端並無顯示的建立遠程對象實例。沒有該實例,又如何廣播消息呢?
或許有人會想,在註冊遠程對象以後,顯式實例該對象不就能夠了嗎?也就是說,在註冊後加上這一段代碼:
BroadCastObj obj = new BroadCastObj();
然而,咱們要明白一個事實:就是服務端和客戶端是處於兩個不一樣的應用程序域中。所以在Remoting中,客戶端得到的遠程對象實際是服務端註冊對象的代理。若是咱們在註冊後,人工去建立一個實例,而非Remoting在激活後自動建立的對象,那麼客戶端得到的對象與服務端人工建立的實例是兩個迥然不一樣的對象。客戶端得到的代理對象並無指向你剛纔建立的obj實例。因此obj發送的消息,客戶端根本沒法捕捉。
那麼,咱們只有望洋興嘆,一籌莫展了嗎?彆着急,別忘了在服務器註冊對象方法中,還有一種方法,即Marshal方法啊。還記得Marshal的實現方式嗎?
BroadCastObj Obj = new BroadCastObj();
ObjRef objRef = RemotingServices.Marshal(Obj,"BroadCastMessage.soap");
這個方法與前不同。前面的三種方式,遠程對象是根據客戶端調用的方式,來自動建立的。而Marshal方法呢?則顯式地建立了遠程對象實例,而後將其Marshal到通道中,造成ObjRef指向對象的代理。只要生命週期沒有結束,這個對象就一直存在。而此時客戶端得到的對象,正是建立的Obj實例的代理。
OK,這個問題解決了,咱們來看看具體實現。
公共程序集和遠程對象與前類似,就再也不贅述,只附上代碼:
//公共程序集: public delegate void BroadCastEventHandler(string info); public interface IBroadCast { event BroadCastEventHandler BroadCastEvent; void BroadCastingInfo(string info); } //遠程對象類: public event BroadCastEventHandler BroadCastEvent; #region IBroadCast 成員 //[OneWay] public void BroadCastingInfo(string info) { if (BroadCastEvent != null) { BroadCastEvent(info); } } #endregion public override object InitializeLifetimeService() { return null; }
下面,該實現服務端了。在實現以前,我還想羅嗦幾句。在第一節中,咱們實現了服務端訂閱客戶端事件。因爲訂閱事件是在服務端發生的,所以事件自己並未被傳送。被序列化的僅僅是傳遞的消息,即Fax而已。如今,方向發生了改變,傳送消息的是服務端,客戶端訂閱了事件。但這個事件是放在遠程對象中的,所以事件必須被序列化。而在.Net Framework1.1中,微軟對序列化的安全級別進行了限制。有關委託和事件的序列化、反序列化默認是禁止的,因此咱們應該將TypeFilterLevel的屬性值設置爲Full枚舉值。所以在服務端註冊通道的方式就發生了改變:
private void StartServer() { BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full; IDictionary props = new Hashtable(); props["port"] = 8080; HttpChannel channel = new HttpChannel(props,clientProvider,serverProvider); ChannelServices.RegisterChannel(channel); Obj = new BroadCastObj(); ObjRef objRef = RemotingServices.Marshal(Obj,"BroadCastMessage.soap"); }
注意語句serverProvider.TypeFilterLevel = TypeFilterLevel.Full;此語句即設置序列化安全級別的。要使用TypeFilterLevel屬性,必須申明命名空間:
using System.Runtime.Serialization.Formatters;
然後面兩條語句就是註冊遠程對象。因爲在個人廣播程序中,發送廣播消息是放在另外一個窗口中,所以我將該遠程對象聲明爲公共靜態對象:
public static BroadCastObj Obj = null;
而後在調用窗口事件中加入:
private void ServerForm_Load(object sender, System.EventArgs e) { StartServer(); lbMonitor.Items.Add("Server started!"); }
來看看界面,首先啓動服務端主窗口:
我放了一個ListBox控件來顯示一些信息,例如顯示服務器啓動了。而BroadCast按鈕就是廣播消息的,單擊該按鈕,會彈出一個對話框:
BraodCast按鈕的代碼:
private void btnBC_Click(object sender, System.EventArgs e) { BroadCastForm bcForm = new BroadCastForm(); bcForm.StartPosition = FormStartPosition.CenterParent; bcForm.ShowDialog(); }
在對話框中,最主要的就是Send按鈕:
if (txtInfo.Text != string.Empty) { ServerForm.Obj.BroadCastingInfo(txtInfo.Text); } else { MessageBox.Show("請輸入信息!"); }
可是很簡單,就是調用遠程對象的發送消息方法而已。
如今該實現客戶端了。咱們能夠參照前面的例子,只是把服務端改成客戶端而已。另外考慮到序列化安全級別的問題,因此代碼會是這樣:
private void ClientForm_Load(object sender, System.EventArgs e) { BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full; IDictionary props = new Hashtable(); props["port"] = 0; HttpChannel channel = new HttpChannel(props,clientProvider,serverProvider); ChannelServices.RegisterChannel(channel); watch = (IBroadCast)Activator.GetObject( typeof(IBroadCast),"http://localhost:8080/BroadCastMessage.soap"); watch.BroadCastEvent += new BroadCastEventHandler(BroadCastingMessage); }
注意客戶端通道的端口號應設置爲0,這表示客戶端自動選擇可用的端口號。若是要設置爲指定的端口號,則必須保證與服務端通道的端口號不相同。
而後是,BroadCastEventHandler委託的方法:
public void BroadCastingMessage(string message) { txtMessage.Text += "I got it:" + message; txtMessage.Text += System.Environment.NewLine; }
客戶端界面如圖:
好,下面讓咱們滿懷期盼,來運行這段程序。首先啓動服務端應用程序,而後啓動客戶端。哎呀,糟糕,竟然出現了錯誤信息!
「人之不如意事,十常居八九。」不用沮喪,讓咱們分析緣由。首先看看錯誤信息,它報告咱們沒有找到Client程序集。然而事實上,Client程序集固然是有的。那麼再來調試一下,是哪一步出現的問題呢?設置好斷點,進行逐語句跟蹤。前面註冊通道一切正常,當運行到watch.BroadCastEvent += new BroadCastEventHandler(BroadCastingMessage)語句時,錯誤出現了!
也就是說,遠程對象的建立是成功的,但在訂閱事件的時候失敗了。緣由是什麼呢?原來,客戶端的委託是經過序列化後得到的,在訂閱事件的時候,委託試圖裝載包含與簽名相同的方法的程序集,也就是BroadCastingMessage方法所在的程序集Client。然而這個裝載的過程發生在服務端,而在服務端,並無Client程序集存在,天然就發生了上面的異常。
緣由清楚了,怎麼解決?首先BroadCastingMessage方法確定是在客戶端中,因此不可避免,委託裝載Client程序集的過程也必須在客戶端完成。而服務端事件又是由遠程對象來捕獲的,所以,在客戶端註冊的也就必須是遠程對象事件了。一個要求必須在客戶端,一個又要求必須在服務端,事情出現了自相矛盾的地方。
那麼,讓咱們先想一想這樣一個例子。假設咱們要交換x和y的值,該這樣完成?很簡單,引入一箇中間變量就能夠了。
int x=1,y=2,z;
z = x;
x = y;
y = z;
這個遊戲相信你們都會玩吧,那麼好的,咱們也須要引入這樣一個「中間」對象。這個中間對象和原來的遠程對象在事件處理方面,代碼徹底一致:
public class EventWrapper:MarshalByRefObject { public event BroadCastEventHandler LocalBroadCastEvent; //[OneWay] public void BroadCasting(string message) { LocalBroadCastEvent(message); } public override object InitializeLifetimeService() { return null; } }
不過不一樣之處在於:這個Wrapper類必須在客戶端和服務端上都要部署,因此,這個類應該放在公共程序集Common.dll中。
如今再來修改原來的客戶端代碼:
watch = (IBroadCast)Activator.GetObject(
typeof(IBroadCast),"http://localhost:8080/BroadCastMessage.soap");
watch.BroadCastEvent += new BroadCastEventHandler(BroadCastingMessage);
修改成:
watch = (IBroadCast)Activator.GetObject(
typeof(IBroadCast),"http://localhost:8080/BroadCastMessage.soap");
EventWrapper wrapper = new EventWrapper();
wrapper.LocalBroadCastEvent += new BroadCastEventHandler(BroadCastingMessage);
watch.BroadCastEvent += new BroadCastEventHandler(wrapper.BroadCasting);
爲何這樣作就能夠了呢?也許畫一幅圖就很容易說明,惋惜個人藝術天分實在很糟糕,我但願之後能夠改進這一點。仍是用文字來講明吧。
前面說,委託要裝載client程序集。如今咱們把遠程對象委託裝載的權利移交給EventWrapper。由於這個類對象是放在客戶端的,因此它要裝載client程序集絲毫沒有問題。語句:
EventWrapper wrapper = new EventWrapper();
wrapper.LocalBroadCastEvent += new BroadCastEventHandler(BroadCastingMessage);
實現了這個功能。
不過此時雖然訂閱了事件,但事件仍是客戶端的,沒有與服務端聯繫起來。而服務端的事件是放到遠程對象中的,因此,還要訂閱事件,這個任務由遠程對象watch來完成。但此時它訂閱的再也不是BroadCastingMessage了,而是EventWrapper的觸發事件方法BroadCasting。那麼此時委託一樣要裝載程序集,但此時裝載的就是BroadCasting所在的程序集了。因爲裝載發生的地點是在服務端。呵呵,高興的是,BroadCasting所在的程序集正是公共程序集(前面已說過,EventWrapper應放到公共程序集Common.dll中),而公共程序集在服務端和客戶端都已經部署了。天然就不會出現找不到程序集的問題了。
注意:EventWrapper由於要重寫InitializeLifetimeService()方法,因此仍然要繼承MarshalByRefObject類。
如今再來運行程序。首先運行服務端;而後運行客戶端,OK,客戶端窗體出現了:
而後咱們在服務端單擊「BroadCast」按鈕,發送廣播消息:
單擊「Send」發送,再來看看客戶端,會是怎樣?Fine,I got it!
怎麼樣,很酷吧!你也能夠同時打開多個客戶端,它們都將收到這個廣播信息。若是你以爲這個廣播聲音太吵,那就請你在客戶端取消廣播吧。在Cancle按鈕中:
private void btnCancle_Click(object sender, System.EventArgs e) { watch.BroadCastEvent -= new BroadCastEventHandler(wrapper.BroadCasting); MessageBox.Show("取消訂閱廣播成功!"); }
固然這個時候wrapper對象應該被申明爲private對象了:
private EventWrapper wrapper = null;
取消後,你試着再廣播一下,恭喜你,你不會聽到噪音了!
3、 客戶端訂閱客戶端事件
有了前面的基礎,再來看客戶端訂閱客戶端事件,就簡單多了。而本文寫到這裏,我也很累了,你也被我囉嗦得不耐煩了。你內心在喊,「饒了我吧!」其實,我又未嘗不是如此。因此我只提供一個思路,有興趣的朋友,能夠本身寫一個程序。
其實方法很簡單,和第二種狀況相似。發送信息的客戶端,只須要得到遠程對象後,發送消息就能夠了。而接收信息的客戶端,負責訂閱該事件。因爲事件都是放到遠程對象中,所以訂閱的方法和第二種狀況沒有什麼區別!
特殊的狀況是,咱們能夠用第三種狀況來代替第二種。只要你把發送信息的客戶端放到服務端就能夠了。固然須要作一些額外的工做,有興趣的朋友能夠去實現一下。在個人示例程序中,已經用這種方法模擬實現了服務端的廣播,你們能夠去看看。
4、 一點補充
我在前面的事件處理中,使用的都是默認的EventArgs。若是要定義本身的EventArgs,就不相同了。由於該信息是傳值序列化,所以必須加上[Serializable],且必須放到公共程序集中,部署到服務端和客戶端。例如:
[Serializable] public class BroadcastEventArgs:EventArgs { private string msg = null; public BroadcastEventArgs(string message) { msg = message; } public string Message { get {return msg;} } }
5、持續改進(經Beta的提醒,我改進了個人程序,並對文章進行了修改 2004年12月13日)
也許,細心的讀者注意到了,在個人遠程對象類和EventWrapper類中,觸發事件方法的Attribute[OneWay]被我註釋掉了。我看到不少資料上寫到,在Remoting中處理事件,觸發事件的方法必須具備這個Attribute。這個attribute究竟有什麼用?
在發送事件消息的時候,事件的訂閱者會觸發事件,而後響應該事件。然而當事件的訂閱者發生錯誤的時候呢?例如,發送事件消息的時候,才發現根本沒有事件訂閱者;或者事件的訂閱者出現故障,如斷電、或異常關機。此時,發送事件一方會由於找不到正確的事件訂閱者,而發生異常。以個人程序爲例。當咱們分別打開服務端和客戶端程序的時候,此時廣播信息正常。然而,當咱們關閉客戶端後,因爲該客戶端沒有取消訂閱,此時異常發生,提示信息如圖:
(不知道爲何,這個異常與客戶端鏈接服務端出現的異常同樣。這個異常容易讓人產生誤會。)
若是這個時候咱們同時打開了多個客戶端,那麼其餘客戶端就會由於這一個客戶端關閉形成的錯誤,而沒法收到廣播信息。那麼讓咱們先作第一步改進:
一、先考慮正常狀況。在個人客戶端,雖然提供了取消訂閱的操做,但並無考慮用戶關閉客戶端的狀況。即,關閉客戶端時,並未取消事件的訂閱,因此咱們應該在關閉客戶端窗體中寫入:
private void ClientForm_Closing(object sender, System.ComponentModel.CancelEventArgs e) { watch.BroadCastEvent -= new BroadCastEventHandler(wrapper.BroadCasting); }
二、僅僅是這樣還不夠。若是客戶端並無正常關閉,而是由於忽然斷電而致使客戶端關閉呢?此時,客戶端尚未來得及取消事件訂閱呢。在這種狀況下,咱們須要用到OneWayAttribute。
前面說到,發送事件一方若是找不到正確的事件訂閱者,會發生異常。也就是說,這個事件是unreachable的。幸運的是,OneWayAttribute剛好解決了這個問題。其實從該特性的命名OneWay,大約也能猜到其中的含義。當事件不可到達,沒法發送時,正常狀況下,會返回一個異常信息。若是加上OneWayAttribute,這個事件的發送就變成單向的了。假如此時發生異常,那麼系統會自動拋掉該異常信息。因爲沒有異常信息的返回,發送信息方會認爲發送信息成功了。程序會正常運行,錯誤的客戶端被忽略,而正確的客戶端仍然可以收到廣播信息。
所以,遠程對象的代碼就應該是這樣:
public event BroadCastEventHandler BroadCastEvent; #region IBroadCast 成員 //[OneWay] public void BroadCastingInfo(string info) { if (BroadCastEvent != null) { BroadCastEvent(info); } } #endregion public override object InitializeLifetimeService() { return null; }
三、最後的改進
使用OneWay當然能夠解決上述的問題,但不夠友好。由於對於廣播消息的一方來講,象被蒙上了眼睛同樣,對於客戶端發生的事情懵然不知。這並非一個好的idea。在Ingo Rammer的Advanced .NET Remoting一書中,Ingo Rammer先生提出了一個更好的辦法,就是在發送信息一方時,檢查了委託鏈。並在委託鏈的遍歷中來捕獲異常。當其中一個委託發生異常時,顯示提示信息。而後繼續遍歷後面的委託,這樣既保證了異常信息的提示,又保證了其餘訂閱者正常接收消息。所以,我對本例的遠程對象進行了修改,註釋掉[OneWay],修改了BroadCastInfo()方法:
//[OneWay] public void BroadCastingInfo(string info) { if (BroadCastEvent != null) { BroadCastEventHandler tempEvent = null; int index = 1; //記錄事件訂閱者委託的索引,爲方便標識,從1開始。 foreach (Delegate del in BroadCastEvent.GetInvocationList()) { try { tempEvent = (BroadCastEventHandler)del; tempEvent(info); } catch { MessageBox.Show("事件訂閱者" + index.ToString() + "發生錯誤,系統將取消事件訂閱!"); BroadCastEvent -= tempEvent; } index++; } } else { MessageBox.Show("事件未被訂閱或訂閱發生錯誤!"); } }
咱們來試驗一下。首先打開服務端,而後同時打開三個客戶端。廣播消息:
消息發送正常。
接着關閉其中一個客戶端窗口,再廣播消息(注意爲模擬客戶端異常狀況,應在ClientForm_Closing方法中把第一步改進的取消訂閱代碼註釋。不然不會發生異常。難道你真的願意用斷電來致使異常發生嗎^_^),結果如圖:
此時服務端報告了「事件訂閱者1發生錯誤,系統將取消事件訂閱」。注意此時另外兩個客戶端,仍是和前面同樣,只有兩條廣播信息。
當咱們點擊提示框的「肯定」按鈕後,廣播仍然發送:
經過這樣的改進後,程序更加的完善,也更加的健壯和友好!
附:
示例代碼說明:
一、 Remoting事件(客戶端發傳真)壓縮包:爲第一節內容;
二、 Remoting事件(服務端廣播)壓縮包:爲第二節、第三節內容,其中:
第二節代碼包含於:
#region 客戶端訂閱服務端事件
#endregion
第三節代碼包含於:
#region 客戶端訂閱客戶端事件
#endregion
若是要實現第二節的程序,請註釋掉第三節代碼;反之亦然。示例程序默認爲第二節程序。
三、 運行示例程序時,請先運行服務端程序,而後運行客戶端程序。不然會拋出「基礎鏈接已關閉」的異常。
四、 解決方案均放在Common(或ICommon)文件夾中。
五、改進後的代碼放到Remoting事件(服務端廣播改進)壓縮包中,你們能夠比較一下改進後的程序有何不一樣!
參考資料:
一、 Ingo Rammer,《Advanced .NET Remoting》
二、 呂震宇,《利用Event鬆耦合遠程對象與遠程系統》
三、 大壞蛋,《.NET Remoting中的事件處理(.NET Framework 2.0)(一)》