Microsoft .Net 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、先考虑正常情况。在我的客户端,虽然提供了取消订阅的操作,但并没有考虑用户关闭客户端的情况。即,关闭客户端时,并未取消事件的订阅,所以我们应该在关闭客户端窗体中写入:
{
watch.BroadCastEvent -= new BroadCastEventHandler(wrapper.BroadCasting);
}
2、仅仅是这样还不够。如果客户端并没有正常关闭,而是因为突然断电而导致客户端关闭呢?此时,客户端还没有来得及取消事件订阅呢。在这种情况下,我们需要用到OneWayAttribute。
前 面说到,发送事件一方如果找不到正确的事件订阅者,会发生异常。也就是说,这个事件是unreachable的。幸运的 是,OneWayAttribute恰好解决了这个问题。其实从该特性的命名OneWay,大约也能猜到其中的含义。当事件不可到达,无法发送时,正常情 况下,会返回一个异常信息。如果加上OneWayAttribute,这个事件的发送就变成单向的了。假如此时发生异常,那么系统会自动抛掉该异常信息。 由于没有异常信息的返回,发送信息方会认为发送信息成功了。程序会正常运行,错误的客户端被忽略,而正确的客户端仍然能够收到广播信息。
因此,远程对象的代码就应该是这样:
IBroadCast 成员
public override object InitializeLifetimeService()
{
return null;
}
3、最后的改进
使 用OneWay固然可以解决上述的问题,但不够友好。因为对于广播消息的一方来说,象被蒙上了眼睛一样,对于客户端发生的事情懵然不知。这并不是一个好的 idea。在Ingo Rammer的Advanced .NET Remoting一书中,Ingo Rammer先生提出了一个更好的办法,就是在发送信息一方时,检查了委托链。并在委托链的遍历中来捕获异常。当其中一个委托发生异常时,显示提示信息。 然后继续遍历后面的委托,这样既保证了异常信息的提示,又保证了其他订阅者正常接收消息。因此,我对本例的远程对象进行了修改,注释掉[OneWay], 修改了BroadCastInfo()方法:
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事件(服务端广播改进)压缩包中,大家可以比较一下改进后的程序有何不同!