您好,登錄后才能下訂單哦!
這篇文章主要介紹Microsoft .Net Remoting中Remoting事件處理的示例分析,文中介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們一定要看完!
前言:在Remoting中處理事件其實并不復(fù)雜,但其中有些技巧需要你去挖掘出來。正是這些技巧,仿佛森嚴(yán)的壁壘,讓許多人望而生畏,或者是不知所謂,最后放棄了事件在Remoting的使用。關(guān)于這個主題,在網(wǎng)上也有很多討論,相關(guān)的技術(shù)文章也不少,遺憾的是,很多文章概述的都不太全面。我在研究Remoting的時候,也對事件處理發(fā)生了興趣。經(jīng)過參考相關(guān)的書籍、文檔,并經(jīng)過反復(fù)的試驗,深信自己能夠把這個問題闡述清楚了。
本文對于Remoting和事件的基礎(chǔ)知識不再介紹,有興趣的可以看我的系列文章,或查閱相關(guān)的技術(shù)文檔。
本文示例代碼下載:
Remoting事件(客戶端發(fā)傳真)
Remoting事件(服務(wù)端廣播)
Remoting事件(服務(wù)端廣播改進)
應(yīng)用Remoting技術(shù)的分布式處理程序,通常包括三部分:遠(yuǎn)程對象、服務(wù)端、客戶端。因此從事件的方向上看,就應(yīng)該有三種形式:
1、服務(wù)端訂閱客戶端事件
2、客戶端訂閱服務(wù)端事件
3、客戶端訂閱客戶端事件
服務(wù)端訂閱客戶端事件,即由客戶端發(fā)送消息,服務(wù)端捕捉該消息,然后響應(yīng)該事件,相當(dāng)于下級向上級發(fā)傳真。反過來,客戶端訂閱服務(wù)端事件,則是由服務(wù)端發(fā)送消息,此時,所有客戶端均捕獲該消息,激發(fā)事件,相當(dāng)于是一個系統(tǒng)廣播。而客戶端訂閱客戶端事件呢?就類似于聊天了。由某個客戶端發(fā)出消息,其他客戶端捕獲該消息,激發(fā)事件??上У氖?,我并沒有找到私聊的解決辦法。當(dāng)客戶端發(fā)出消息后,只要訂閱了該事件的,都會獲得該信息。
然而不管是哪一種方式,究其實質(zhì),真正包含事件的還是遠(yuǎn)程對象。原理很簡單,我們想一想,在Remoting中,客戶端和服務(wù)端傳遞的內(nèi)容是什么呢?毋庸置疑,是遠(yuǎn)程對象。因此,我們傳遞的事件消息,自然是被遠(yuǎn)程對象所包裹。這就像EMS快遞,遠(yuǎn)程對象是運送信件的汽車,而事件消息就是汽車所裝載的信件。至于事件傳遞的方向,只是發(fā)送者和訂閱者的角色發(fā)生了改變而已。
一、 服務(wù)端訂閱客戶端事件
服務(wù)端訂閱客戶端事件,相對比較簡單。我們就以發(fā)傳真為例。首先,我們必須具備傳真機和要傳真的文件,這就好比我們的遠(yuǎn)程對象。而且這個傳真機上必須具備“發(fā)送”的操作按鈕。這就好比是遠(yuǎn)程對象中的一個委托。當(dāng)客戶發(fā)送傳真時,就需要在客戶端上激活一個發(fā)送消息的方法,這就好比我們按了“發(fā)送”按鈕。消息發(fā)送到服務(wù)端后,觸發(fā)事件,這個事件正是服務(wù)端訂閱的。服務(wù)端獲得該事件消息后,再處理相關(guān)業(yè)務(wù)。這就好比接收傳真的人員,當(dāng)傳真收到后,會聽到接通的聲音,此時選擇“接收”后,該消息就被捕獲了。
現(xiàn)在,我們就來模擬這個流程。首先定義遠(yuǎn)程對象,這個對象處理的應(yīng)該是一個發(fā)送傳真的業(yè)務(wù):
首先是遠(yuǎn)程對象的公共接口(Common.dll):
public delegate void FaxEventHandler(string fax); public interface IFaxBusiness { void SendFax(string fax); }
注意,在公共接口程序集中,定義了一個公共委托。
然后我們定義具體處理傳真業(yè)務(wù)的遠(yuǎn)程對象類(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; } }
這個遠(yuǎn)程對象中,事件的類型就是我們在公共程序集Common.dll中定義的委托類型。SendFax實現(xiàn)了接口IFaxBusiness中的方法。這個方法的簽名和定義的委托一致,它調(diào)用了事件FaxSendedEvent。
特殊的地方是我們定義的遠(yuǎn)程對象最好是重寫MarshalByRefObject類的InitializeLifetimeService()方法。返回null值表明這個遠(yuǎn)程對象的生命周期為無限大。為什么要重寫該方法呢?道理不言自明,如果生命周期不進行限制的話,一旦遠(yuǎn)程對象的生命周期結(jié)束,事件就無法激活了。
接下來就是分別實現(xiàn)客戶端和服務(wù)端了。服務(wù)端是一個Windows應(yīng)用程序,界面如下:
我們在加載窗體的時候,注冊通道和遠(yuǎn)程對象:
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模式,注冊了一個遠(yuǎn)程對象。注意看,這段代碼和一般的Remoting服務(wù)端有什么區(qū)別?對了,它多了一行注冊事件的代碼:
FaxBusiness.FaxSendedEvent += new FaxEventHandler(OnFaxSended);
這行代碼,就好比我們服務(wù)端的傳真機,一直切換為“自動”模式。它會一直監(jiān)聽著來自客戶端的傳真信息,一旦傳真信息從客戶端發(fā)過來了,則響應(yīng)事件方法,即OnFaxSended方法:
public void OnFaxSended(string fax) { txtFax.Text += fax; txtFax.Text += System.Environment.NewLine; }
這個方法很簡單,就是把客戶端發(fā)過來的Fax顯示到txtFax文本框控件上。
而客戶端呢?仍然是一個Windows應(yīng)用程序。代碼非常簡單,首先為了簡便其見,我們?nèi)匀蛔屗谘b載窗體的時候,激活遠(yuǎn)程對象:
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客戶端應(yīng)用程序沒有什么不同。該寫傳真了!我們在窗體上放一個文本框?qū)ο?,改其Multiline屬性為true。再放一個按鈕,負(fù)責(zé)發(fā)送傳真:
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("請輸入傳真內(nèi)容!"); } } private string GetIpAddress() { IPHostEntry ipHE = Dns.GetHostByName(Dns.GetHostName()); return ipHE.AddressList[0].ToString(); }
在這個按鈕單擊事件中,只需要調(diào)用遠(yuǎn)程對象faxBus的SendFax()方法就OK了,非常簡單。可是慢著,為什么你的代碼有這么多行啊?其實,沒有什么奇怪的,我只是想到發(fā)傳真的客戶可能會很多。為了避免服務(wù)端人員犯糊涂,搞不清楚是誰發(fā)的,所以要求在傳真上加上各自的簽名,也就是客戶端的IP地址了。既然要獲得計算機的IP地址,請一定要記得加上對DNS的命名空間引用:
using System.Net;
因為我們嚴(yán)格按照分布式處理程序的部署方式,所以在客戶端只需要添加公共程序集(Common.dll)的引用就可以了。而在服務(wù)端呢,則必須添加公共程序集和遠(yuǎn)程對象程序集兩者的引用。
OK,程序完成,我們來看看這個簡陋的傳真機:
客戶端:
嘿嘿,做夢都想放假啊。好的,傳真寫好了,發(fā)送吧!再看看服務(wù)端,great,老板已經(jīng)收到我的請假條傳真了!
二、 客戶端訂閱服務(wù)端事件
嘿嘿,吃甘蔗要先吃甜的一段,做事情我也喜歡先做容易的?,F(xiàn)在,好日子過去了,該吃點苦頭了。我們先回憶一下剛才的實現(xiàn)方法,再來思考怎么實現(xiàn)客戶端訂閱服務(wù)端事件?
在前一節(jié),事件被放到遠(yuǎn)程對象中,客戶端激活對象后,就可以發(fā)送消息了。而在服務(wù)端,只需要訂閱該事件就可以。現(xiàn)在思路應(yīng)該反過來,由客戶端訂閱事件,服務(wù)端發(fā)送消息。就這么簡單嗎?先不要高興得太早。我們想一想,發(fā)送消息的任務(wù)是誰來完成的?是遠(yuǎn)程對象。而遠(yuǎn)程對象是什么時候創(chuàng)建的呢?我們仔細(xì)思考Remoting的幾種激活方式,不管是服務(wù)端激活,還是客戶端激活,他們的工作原理都是:客戶端決定了服務(wù)器創(chuàng)建遠(yuǎn)程對象實例的時機,例如調(diào)用了遠(yuǎn)程對象的方法。而服務(wù)端所作的工作則是注冊該遠(yuǎn)程對象。
回憶這三種激活方式在服務(wù)端的代碼:
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這個詞語,它表達的含義就是注冊。也就是說,在服務(wù)端并沒有顯示的創(chuàng)建遠(yuǎn)程對象實例。沒有該實例,又如何廣播消息呢?
或許有人會想,在注冊遠(yuǎn)程對象之后,顯式實例該對象不就可以了嗎?也就是說,在注冊后加上這一段代碼:
BroadCastObj obj = new BroadCastObj();
然而,我們要明白一個事實:就是服務(wù)端和客戶端是處于兩個不同的應(yīng)用程序域中。因此在Remoting中,客戶端獲得的遠(yuǎn)程對象實際是服務(wù)端注冊對象的代理。如果我們在注冊后,人工去創(chuàng)建一個實例,而非Remoting在激活后自動創(chuàng)建的對象,那么客戶端獲得的對象與服務(wù)端人工創(chuàng)建的實例是兩個迥然不同的對象??蛻舳双@得的代理對象并沒有指向你剛才創(chuàng)建的obj實例。所以obj發(fā)送的消息,客戶端根本無法捕捉。
那么,我們只有望洋興嘆,束手無策了嗎?別著急,別忘了在服務(wù)器注冊對象方法中,還有一種方法,即Marshal方法啊。還記得Marshal的實現(xiàn)方式嗎?
BroadCastObj Obj = new BroadCastObj(); ObjRef objRef = RemotingServices.Marshal(Obj,"BroadCastMessage.soap");
這個方法與前不一樣。前面的三種方式,遠(yuǎn)程對象是根據(jù)客戶端調(diào)用的方式,來自動創(chuàng)建的。而Marshal方法呢?則顯式地創(chuàng)建了遠(yuǎn)程對象實例,然后將其Marshal到通道中,形成ObjRef指向?qū)ο蟮拇怼V灰芷跊]有結(jié)束,這個對象就一直存在。而此時客戶端獲得的對象,正是創(chuàng)建的Obj實例的代理。
OK,這個問題解決了,我們來看看具體實現(xiàn)。
公共程序集和遠(yuǎn)程對象與前相似,就不再贅述,只附上代碼:
公共程序集:
public delegate void BroadCastEventHandler(string info); public interface IBroadCast { event BroadCastEventHandler BroadCastEvent; void BroadCastingInfo(string info); }
遠(yuǎn)程對象類:
public event BroadCastEventHandler BroadCastEvent; #region IBroadCast 成員 //[OneWay] public void BroadCastingInfo(string info) { if (BroadCastEvent != null) { BroadCastEvent(info); } } #endregion public override object InitializeLifetimeService() { return null; }
下面,該實現(xiàn)服務(wù)端了。在實現(xiàn)之前,我還想羅嗦幾句。在第一節(jié)中,我們實現(xiàn)了服務(wù)端訂閱客戶端事件。由于訂閱事件是在服務(wù)端發(fā)生的,因此事件本身并未被傳送。被序列化的僅僅是傳遞的消息,即Fax而已。現(xiàn)在,方向發(fā)生了改變,傳送消息的是服務(wù)端,客戶端訂閱了事件。但這個事件是放在遠(yuǎn)程對象中的,因此事件必須被序列化。而在.Net Framework1.1中,微軟對序列化的安全級別進行了限制。有關(guān)委托和事件的序列化、反序列化默認(rèn)是禁止的,所以我們應(yīng)該將TypeFilterLevel的屬性值設(shè)置為Full枚舉值。因此在服務(wù)端注冊通道的方式就發(fā)生了改變:
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;此語句即設(shè)置序列化安全級別的。要使用TypeFilterLevel屬性,必須申明命名空間:
using System.Runtime.Serialization.Formatters;
而后面兩條語句就是注冊遠(yuǎn)程對象。由于在我的廣播程序中,發(fā)送廣播消息是放在另一個窗口中,因此我將該遠(yuǎn)程對象聲明為公共靜態(tài)對象:
public static BroadCastObj Obj = null;
然后在調(diào)用窗口事件中加入:
private void ServerForm_Load(object sender, System.EventArgs e) { StartServer(); lbMonitor.Items.Add("Server started!"); }
來看看界面,首先啟動服務(wù)端主窗口:
我放了一個ListBox控件來顯示一些信息,例如顯示服務(wù)器啟動了。而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("請輸入信息!"); }
但是很簡單,就是調(diào)用遠(yuǎn)程對象的發(fā)送消息方法而已。
現(xiàn)在該實現(xiàn)客戶端了。我們可以參照前面的例子,只是把服務(wù)端改為客戶端而已。另外考慮到序列化安全級別的問題,所以代碼會是這樣:
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); }
注意客戶端通道的端口號應(yīng)設(shè)置為0,這表示客戶端自動選擇可用的端口號。如果要設(shè)置為指定的端口號,則必須保證與服務(wù)端通道的端口號不相同。
然后是,BroadCastEventHandler委托的方法:
public void BroadCastingMessage(string message) { txtMessage.Text += "I got it:" + message; txtMessage.Text += System.Environment.NewLine; }
客戶端界面如圖:
好,下面讓我們滿懷期盼,來運行這段程序。首先啟動服務(wù)端應(yīng)用程序,然后啟動客戶端。哎呀,糟糕,居然出現(xiàn)了錯誤信息!
“人之不如意事,十常居八九?!辈挥镁趩?,讓我們分析原因。首先看看錯誤信息,它報告我們沒有找到Client程序集。然而事實上,Client程序集當(dāng)然是有的。那么再來調(diào)試一下,是哪一步出現(xiàn)的問題呢?設(shè)置好斷點,進行逐語句跟蹤。前面注冊通道一切正常,當(dāng)運行到watch.BroadCastEvent += new BroadCastEventHandler(BroadCastingMessage)語句時,錯誤出現(xiàn)了!
也就是說,遠(yuǎn)程對象的創(chuàng)建是成功的,但在訂閱事件的時候失敗了。原因是什么呢?原來,客戶端的委托是通過序列化后獲得的,在訂閱事件的時候,委托試圖裝載包含與簽名相同的方法的程序集,也就是BroadCastingMessage方法所在的程序集Client。然而這個裝載的過程發(fā)生在服務(wù)端,而在服務(wù)端,并沒有Client程序集存在,自然就發(fā)生了上面的異常。
原因清楚了,怎么解決?首先BroadCastingMessage方法肯定是在客戶端中,所以不可避免,委托裝載Client程序集的過程也必須在客戶端完成。而服務(wù)端事件又是由遠(yuǎn)程對象來捕獲的,因此,在客戶端注冊的也就必須是遠(yuǎn)程對象事件了。一個要求必須在客戶端,一個又要求必須在服務(wù)端,事情出現(xiàn)了自相矛盾的地方。
那么,讓我們先想想這樣一個例子。假設(shè)我們要交換x和y的值,該這樣完成?很簡單,引入一個中間變量就可以了。
int x=1,y=2,z; z = x; x = y; y = z;
這個游戲相信大家都會玩吧,那么好的,我們也需要引入這樣一個“中間”對象。這個中間對象和原來的遠(yuǎn)程對象在事件處理方面,代碼完全一致:
public class EventWrapper:MarshalByRefObject { public event BroadCastEventHandler LocalBroadCastEvent; //[OneWay] public void BroadCasting(string message) { LocalBroadCastEvent(message); } public override object InitializeLifetimeService() { return null; } }
不過不同之處在于:這個Wrapper類必須在客戶端和服務(wù)端上都要部署,所以,這個類應(yīng)該放在公共程序集Common.dll中。
現(xiàn)在再來修改原來的客戶端代碼:
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);
為什么這樣做就可以了呢?也許畫一幅圖就很容易說明,可惜我的藝術(shù)天分實在很糟糕,我希望以后可以改進這一點。還是用文字來說明吧。
前面說,委托要裝載client程序集?,F(xiàn)在我們把遠(yuǎn)程對象委托裝載的權(quán)利移交給EventWrapper。因為這個類對象是放在客戶端的,所以它要裝載client程序集絲毫沒有問題。語句:
EventWrapper wrapper = new EventWrapper(); wrapper.LocalBroadCastEvent += new BroadCastEventHandler(BroadCastingMessage);
實現(xiàn)了這個功能。
不過此時雖然訂閱了事件,但事件還是客戶端的,沒有與服務(wù)端聯(lián)系起來。而服務(wù)端的事件是放到遠(yuǎn)程對象中的,所以,還要訂閱事件,這個任務(wù)由遠(yuǎn)程對象watch來完成。但此時它訂閱的不再是BroadCastingMessage了,而是EventWrapper的觸發(fā)事件方法BroadCasting。那么此時委托同樣要裝載程序集,但此時裝載的就是BroadCasting所在的程序集了。由于裝載發(fā)生的地點是在服務(wù)端。呵呵,高興的是,BroadCasting所在的程序集正是公共程序集(前面已說過,EventWrapper應(yīng)放到公共程序集Common.dll中),而公共程序集在服務(wù)端和客戶端都已經(jīng)部署了。自然就不會出現(xiàn)找不到程序集的問題了。
注意:EventWrapper因為要重寫InitializeLifetimeService()方法,所以仍然要繼承MarshalByRefObject類。
現(xiàn)在再來運行程序。首先運行服務(wù)端;然后運行客戶端,OK,客戶端窗體出現(xiàn)了:
然后我們在服務(wù)端單擊“BroadCast”按鈕,發(fā)送廣播消息:
單擊“Send”發(fā)送,再來看看客戶端,會是怎樣?Fine,I got it!
怎么樣,很酷吧!你也可以同時打開多個客戶端,它們都將收到這個廣播信息。如果你覺得這個廣播聲音太吵,那就請你在客戶端取消廣播吧。在Cancle按鈕中:
private void btnCancle_Click(object sender, System.EventArgs e) { watch.BroadCastEvent -= new BroadCastEventHandler(wrapper.BroadCasting); MessageBox.Show("取消訂閱廣播成功!"); }
當(dāng)然這個時候wrapper對象應(yīng)該被申明為private對象了:
private EventWrapper wrapper = null;
取消后,你試著再廣播一下,恭喜你,你不會聽到噪音了!
三、 客戶端訂閱客戶端事件
有了前面的基礎(chǔ),再來看客戶端訂閱客戶端事件,就簡單多了。而本文寫到這里,我也很累了,你也被我啰嗦得不耐煩了。你心里在喊,“饒了我吧!”其實,我又何嘗不是如此。所以我只提供一個思路,有興趣的朋友,可以自己寫一個程序。
其實方法很簡單,和第二種情況類似。發(fā)送信息的客戶端,只需要獲得遠(yuǎn)程對象后,發(fā)送消息就可以了。而接收信息的客戶端,負(fù)責(zé)訂閱該事件。由于事件都是放到遠(yuǎn)程對象中,因此訂閱的方法和第二種情況沒有什么區(qū)別!
特殊的情況是,我們可以用第三種情況來代替第二種。只要你把發(fā)送信息的客戶端放到服務(wù)端就可以了。當(dāng)然需要做一些額外的工作,有興趣的朋友可以去實現(xiàn)一下。在我的示例程序中,已經(jīng)用這種方法模擬實現(xiàn)了服務(wù)端的廣播,大家可以去看看。
四、 一點補充
我在前面的事件處理中,使用的都是默認(rèn)的EventArgs。如果要定義自己的EventArgs,就不相同了。因為該信息是傳值序列化,因此必須加上[Serializable],且必須放到公共程序集中,部署到服務(wù)端和客戶端。例如:
[Serializable] public class BroadcastEventArgs:EventArgs { private string msg = null; public BroadcastEventArgs(string message) { msg = message; } public string Message { get {return msg;} } }
五、持續(xù)改進(經(jīng)Beta的提醒,我改進了我的程序,并對文章進行了修改 2004年12月13日)
也許,細(xì)心的讀者注意到了,在我的遠(yuǎn)程對象類和EventWrapper類中,觸發(fā)事件方法的Attribute[OneWay]被我注釋掉了。我看到很多資料上寫到,在Remoting中處理事件,觸發(fā)事件的方法必須具有這個Attribute。這個attribute究竟有什么用?
在發(fā)送事件消息的時候,事件的訂閱者會觸發(fā)事件,然后響應(yīng)該事件。然而當(dāng)事件的訂閱者發(fā)生錯誤的時候呢?例如,發(fā)送事件消息的時候,才發(fā)現(xiàn)根本沒有事件訂閱者;或者事件的訂閱者出現(xiàn)故障,如斷電、或異常關(guān)機。此時,發(fā)送事件一方會因為找不到正確的事件訂閱者,而發(fā)生異常。以我的程序為例。當(dāng)我們分別打開服務(wù)端和客戶端程序的時候,此時廣播信息正常。然而,當(dāng)我們關(guān)閉客戶端后,由于該客戶端沒有取消訂閱,此時異常發(fā)生,提示信息如圖:
?。ú恢罏槭裁?,這個異常與客戶端連接服務(wù)端出現(xiàn)的異常一樣。這個異常容易讓人產(chǎn)生誤會。)
如果這個時候我們同時打開了多個客戶端,那么其他客戶端就會因為這一個客戶端關(guān)閉造成的錯誤,而無法收到廣播信息。那么讓我們先做第一步改進:
1、先考慮正常情況。在我的客戶端,雖然提供了取消訂閱的操作,但并沒有考慮用戶關(guān)閉客戶端的情況。即,關(guān)閉客戶端時,并未取消事件的訂閱,所以我們應(yīng)該在關(guān)閉客戶端窗體中寫入:
private void ClientForm_Closing(object sender, System.ComponentModel.CancelEventArgs e) { watch.BroadCastEvent -= new BroadCastEventHandler(wrapper.BroadCasting); }
2、僅僅是這樣還不夠。如果客戶端并沒有正常關(guān)閉,而是因為突然斷電而導(dǎo)致客戶端關(guān)閉呢?此時,客戶端還沒有來得及取消事件訂閱呢。在這種情況下,我們需要用到OneWayAttribute。
前面說到,發(fā)送事件一方如果找不到正確的事件訂閱者,會發(fā)生異常。也就是說,這個事件是unreachable的。幸運的是,OneWayAttribute恰好解決了這個問題。其實從該特性的命名OneWay,大約也能猜到其中的含義。當(dāng)事件不可到達,無法發(fā)送時,正常情況下,會返回一個異常信息。如果加上OneWayAttribute,這個事件的發(fā)送就變成單向的了。假如此時發(fā)生異常,那么系統(tǒng)會自動拋掉該異常信息。由于沒有異常信息的返回,發(fā)送信息方會認(rèn)為發(fā)送信息成功了。程序會正常運行,錯誤的客戶端被忽略,而正確的客戶端仍然能夠收到廣播信息。
因此,遠(yuǎn)程對象的代碼就應(yīng)該是這樣:
public event BroadCastEventHandler BroadCastEvent;
IBroadCast 成員
public override object InitializeLifetimeService() { return null; }
3、最后的改進
使用OneWay固然可以解決上述的問題,但不夠友好。因為對于廣播消息的一方來說,象被蒙上了眼睛一樣,對于客戶端發(fā)生的事情懵然不知。這并不是一個好的idea。在Ingo Rammer的Advanced .NET Remoting一書中,Ingo Rammer先生提出了一個更好的辦法,就是在發(fā)送信息一方時,檢查了委托鏈。并在委托鏈的遍歷中來捕獲異常。當(dāng)其中一個委托發(fā)生異常時,顯示提示信息。然后繼續(xù)遍歷后面的委托,這樣既保證了異常信息的提示,又保證了其他訂閱者正常接收消息。因此,我對本例的遠(yuǎn)程對象進行了修改,注釋掉[OneWay],修改了BroadCastInfo()方法:
//[OneWay] public void BroadCastingInfo(string info) { if (BroadCastEvent != null) { BroadCastEventHandler tempEvent = null; int index = 1; //記錄事件訂閱者委托的索引,為方便標(biāo)識,從1開始。 foreach (Delegate del in BroadCastEvent.GetInvocationList()) { try { tempEvent = (BroadCastEventHandler)del; tempEvent(info); } catch { MessageBox.Show("事件訂閱者" + index.ToString() + "發(fā)生錯誤,系統(tǒng)將取消事件訂閱!"); BroadCastEvent -= tempEvent; } index++; } } else { MessageBox.Show("事件未被訂閱或訂閱發(fā)生錯誤!"); } }
我們來試驗一下。首先打開服務(wù)端,然后同時打開三個客戶端。廣播消息:
消息發(fā)送正常。
接著關(guān)閉其中一個客戶端窗口,再廣播消息(注意為模擬客戶端異常情況,應(yīng)在ClientForm_Closing方法中把第一步改進的取消訂閱代碼注釋。否則不會發(fā)生異常。難道你真的愿意用斷電來導(dǎo)致異常發(fā)生嗎^_^),結(jié)果如圖:
此時服務(wù)端報告了“事件訂閱者1發(fā)生錯誤,系統(tǒng)將取消事件訂閱”。注意此時另外兩個客戶端,還是和前面一樣,只有兩條廣播信息。
當(dāng)我們點擊提示框的“確定”按鈕后,廣播仍然發(fā)送:
通過這樣的改進后,程序更加的完善,也更加的健壯和友好!
以上是“Microsoft .Net Remoting中Remoting事件處理的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對大家有幫助,更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。