Microsoft .Net Remoting系列專題之三:Remoting事件處理全接觸

javaprogramers發表於2006-05-14

前言:在Remoting中處理事件其實並不複雜,但其中有些技巧需要你去挖掘出來。正是這些技巧,彷彿森嚴的壁壘,讓許多人望而生畏,或者是不知所謂,最後放棄了事件在Remoting的使用。關於這個主題,在網上也有很多討論,相關的技術文章也不少,遺憾的是,很多文章概述的都不太全面。我在研究Remoting的時候,也對事件處理髮生了興趣。經過參考相關的書籍、文件,並經過反覆的試驗,深信自己能夠把這個問題闡述清楚了。
本文對於Remoting和事件的基礎知識不再介紹,有興趣的可以看我的系列文章,或查閱相關的技術文件。

本文示例程式碼下載:

Remoting事件(客戶端發傳真)

Remoting事件(服務端廣播)

Remoting事件(服務端廣播改進)

應用Remoting技術的分散式處理程式,通常包括三部分:遠端物件、服務端、客戶端。因此從事件的方向上看,就應該有三種形式:
1、服務端訂閱客戶端事件
2、客戶端訂閱服務端事件
3、客戶端訂閱客戶端事件

服務端訂閱客戶端事件,即由客戶端傳送訊息,服務端捕捉該訊息,然後響應該事件,相當於下級向上級發傳真。反過來,客戶端訂閱服務端事件,則是由服務端傳送訊息,此時,所有客戶端均捕獲該訊息,激發事件,相當於是一個系統廣播。而客戶端訂閱客戶端事件呢?就類似於聊天了。由某個客戶端發出訊息,其他客戶端捕獲該訊息,激發事件。可惜的是,我並沒有找到私聊的解決辦法。當客戶端發出訊息後,只要訂閱了該事件的,都會獲得該資訊。

然而不管是哪一種方式,究其實質,真正包含事件的還是遠端物件。原理很簡單,我們想一想,在Remoting中,客戶端和服務端傳遞的內容是什麼呢?毋庸置疑,是遠端物件。因此,我們傳遞的事件訊息,自然是被遠端物件所包裹。這就像EMS快遞,遠端物件是運送信件的汽車,而事件訊息就是汽車所裝載的信件。至於事件傳遞的方向,只是傳送者和訂閱者的角色發生了改變而已。

一、 服務端訂閱客戶端事件
服務端訂閱客戶端事件,相對比較簡單。我們就以發傳真為例。首先,我們必須具備傳真機和要傳真的檔案,這就好比我們的遠端物件。而且這個傳真機上必須具備“傳送”的操作按鈕。這就好比是遠端物件中的一個委託。當客戶傳送傳真時,就需要在客戶端上啟用一個傳送訊息的方法,這就好比我們按了“傳送”按鈕。訊息傳送到服務端後,觸發事件,這個事件正是服務端訂閱的。服務端獲得該事件訊息後,再處理相關業務。這就好比接收傳真的人員,當傳真收到後,會聽到接通的聲音,此時選擇“接收”後,該訊息就被捕獲了。

現在,我們就來模擬這個流程。首先定義遠端物件,這個物件處理的應該是一個傳送傳真的業務:
首先是遠端物件的公共介面(Common.dll):
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,老闆已經收到我的請假條傳真了!


 

二、 客戶端訂閱服務端事件

嘿嘿,吃甘蔗要先吃甜的一段,做事情我也喜歡先做容易的。現在,好日子過去了,該吃點苦頭了。我們先回憶一下剛才的實現方法,再來思考怎麼實現客戶端訂閱服務端事件?

在前一節,事件被放到遠端物件中,客戶端啟用物件後,就可以傳送訊息了。而在服務端,只需要訂閱該事件就可以。現在思路應該反過來,由客戶端訂閱事件,服務端傳送訊息。就這麼簡單嗎?先不要高興得太早。我們想一想,傳送訊息的任務是誰來完成的?是遠端物件。而遠端物件是什麼時候建立的呢?我們仔細思考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;


 
取消後,你試著再廣播一下,恭喜你,你不會聽到噪音了!

三、 客戶端訂閱客戶端事件

有了前面的基礎,再來看客戶端訂閱客戶端事件,就簡單多了。而本文寫到這裡,我也很累了,你也被我囉嗦得不耐煩了。你心裡在喊,“饒了我吧!”其實,我又何嘗不是如此。所以我只提供一個思路,有興趣的朋友,可以自己寫一個程式。

其實方法很簡單,和第二種情況類似。傳送資訊的客戶端,只需要獲得遠端物件後,傳送訊息就可以了。而接收資訊的客戶端,負責訂閱該事件。由於事件都是放到遠端物件中,因此訂閱的方法和第二種情況沒有什麼區別!

特殊的情況是,我們可以用第三種情況來代替第二種。只要你把傳送資訊的客戶端放到服務端就可以了。當然需要做一些額外的工作,有興趣的朋友可以去實現一下。在我的示例程式中,已經用這種方法模擬實現了服務端的廣播,大家可以去看看。

四、 一點補充

我在前面的事件處理中,使用的都是預設的EventArgs。如果要定義自己的EventArgs,就不相同了。因為該資訊是傳值序列化,因此必須加上[Serializable],且必須放到公共程式集中,部署到服務端和客戶端。例如:
[Serializable]
public class BroadcastEventArgs:EventArgs
{
 private string msg = null;
 public BroadcastEventArgs(string message)
 {
  msg = message;
 }

 public string Message
 {
  get {return msg;}
 }
}

五、持續改進(經Beta的提醒,我改進了我的程式,並對文章進行了修改 2004年12月13日)

也許,細心的讀者注意到了,在我的遠端物件類和EventWrapper類中,觸發事件方法的Attribute[OneWay]被我註釋掉了。我看到很多資料上寫到,在Remoting中處理事件,觸發事件的方法必須具有這個Attribute。這個attribute究竟有什麼用?

在傳送事件訊息的時候,事件的訂閱者會觸發事件,然後響應該事件。然而當事件的訂閱者發生錯誤的時候呢?例如,傳送事件訊息的時候,才發現根本沒有事件訂閱者;或者事件的訂閱者出現故障,如斷電、或異常關機。此時,傳送事件一方會因為找不到正確的事件訂閱者,而發生異常。以我的程式為例。當我們分別開啟服務端和客戶端程式的時候,此時廣播資訊正常。然而,當我們關閉客戶端後,由於該客戶端沒有取消訂閱,此時異常發生,提示資訊如圖:

(不知道為什麼,這個異常與客戶端連線服務端出現的異常一樣。這個異常容易讓人產生誤會。)

如果這個時候我們同時開啟了多個客戶端,那麼其他客戶端就會因為這一個客戶端關閉造成的錯誤,而無法收到廣播資訊。那麼讓我們先做第一步改進:

1、先考慮正常情況。在我的客戶端,雖然提供了取消訂閱的操作,但並沒有考慮使用者關閉客戶端的情況。即,關閉客戶端時,並未取消事件的訂閱,所以我們應該在關閉客戶端窗體中寫入:

        private void ClientForm_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        
{
            watch.BroadCastEvent 
-= new BroadCastEventHandler(wrapper.BroadCasting);
        }

2、僅僅是這樣還不夠。如果客戶端並沒有正常關閉,而是因為突然斷電而導致客戶端關閉呢?此時,客戶端還沒有來得及取消事件訂閱呢。在這種情況下,我們需要用到OneWayAttribute。

前面說到,傳送事件一方如果找不到正確的事件訂閱者,會發生異常。也就是說,這個事件是unreachable的。幸運的是,OneWayAttribute恰好解決了這個問題。其實從該特性的命名OneWay,大約也能猜到其中的含義。當事件不可到達,無法傳送時,正常情況下,會返回一個異常資訊。如果加上OneWayAttribute,這個事件的傳送就變成單向的了。假如此時發生異常,那麼系統會自動拋掉該異常資訊。由於沒有異常資訊的返回,傳送資訊方會認為傳送資訊成功了。程式會正常執行,錯誤的客戶端被忽略,而正確的客戶端仍然能夠收到廣播資訊。

因此,遠端物件的程式碼就應該是這樣:

public event BroadCastEventHandler BroadCastEvent;

IBroadCast 成員

public override object InitializeLifetimeService()
{
 
return null;
}

3、最後的改進

使用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發生錯誤,系統將取消事件訂閱”。注意此時另外兩個客戶端,還是和前面一樣,只有兩條廣播資訊。

當我們點選提示框的“確定”按鈕後,廣播仍然傳送:

通過這樣的改進後,程式更加的完善,也更加的健壯和友好!

附:
示例程式碼說明:
1、 Remoting事件(客戶端發傳真)壓縮包:為第一節內容;
2、 Remoting事件(服務端廣播)壓縮包:為第二節、第三節內容,其中:
第二節程式碼包含於:
#region 客戶端訂閱服務端事件
#endregion
第三節程式碼包含於:
#region 客戶端訂閱客戶端事件
#endregion
如果要實現第二節的程式,請註釋掉第三節程式碼;反之亦然。示例程式預設為第二節程式。
3、 執行示例程式時,請先執行服務端程式,然後執行客戶端程式。否則會丟擲“基礎連線已關閉”的異常。
4、 解決方案均放在Common(或ICommon)資料夾中。

5、改進後的程式碼放到Remoting事件(服務端廣播改進)壓縮包中,大家可以比較一下改進後的程式有何不同!

 

相關文章