关闭

Microsoft .Net Remoting系列专题之三:Remoting事件处理全接触

标签: .netmicrosoft服务器wrapperstringsqlserver
3241人阅读 评论(1) 收藏 举报
分类:

前言:在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事件(服务端广播改进)压缩包中,大家可以比较一下改进后的程序有何不同!

参考资料:
1、 Ingo Rammer,《Advanced .NET Remoting》
2、 吕震宇,《利用Event松耦合远程对象与远程系统
3、 大坏蛋,《.NET Remoting中的事件处理(.NET Framework 2.0)(一)

posted on 2004-12-13 10:42 Bruce Zhang 阅读(13754) 评论(76)  编辑  收藏 所属分类: .Net Remoting

评论

 

问个问题,假如客户端在注册了服务器广播事件后,并没有取消事件,既:watch.BroadCastEvent -= new BroadCastEventHandler(wrapper.BroadCasting);就退出了,比如突然断电,断网,那么服务器在广播事件的时候会发生错误,请问你是怎么解决这个问题的?
  

回复  引用  查看    

#2楼 [楼主] 2004-12-11 09:31 wayfarer

@beta:

你这个问题很好。我在写本文的时候,没有考虑到这个问题。谢谢你提醒了我。这个问题还真的是比较严重的问题。由于服务器无法获得客户端的状况,它在广播的时候,根本不知客户端是否订阅了事件,又是否取消了事件。

而一旦出现这个问题的时候,广播事件时会抛出异常。所以,我的临时解决方案只能是捕捉这个异常,并提示信息了。
try
{
ServerForm.Obj.BroadCastingInfo(txtInfo.Text);
}
catch
{
MessageBox.Show("客户端非正常关闭!");
}

你的这个提醒,让我发现了我的示例程序中另外一个Bug,就是当用户关闭客户端时,没有取消事件的订阅。所以,应该在关闭窗口的时候,做这件事情:
private void ClientForm_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
watch.BroadCastEvent -= new BroadCastEventHandler(wrapper.BroadCasting);
}

这样关闭其中一个客户端,并不影响服务端广播给其他客户端。

我已更新了供下载的示例程序。
  回复  引用  查看    

#3楼 [未注册] 2004-12-12 12:05 wayfarer

我估计[OneWay]attribute也许能解决如上问题。另外Ingo Rammer在其书中也有提到。我希望能尽快看一下,看是否能给出解决方案。
  回复  引用  查看    

#4楼 [楼主] 2004-12-13 10:48 wayfarer

@beta:
很高兴,你的问题我已经解决了。你可以看本文的第五部分“持续改进”。

非常感谢你的提醒,要不然本文就有一个很大的缺陷了。   回复  引用  查看    

#5楼 [未注册] 2004-12-31 12:42 buaaytt

楼主的又一篇好文,收藏,多谢了   回复  引用  查看    

#6楼 [未注册] 2004-12-31 12:44 buaaytt

有个小问题:
在客户端的窗体加载事件中有下面两行:
HttpChannel channel = new HttpChannel(0);
ChannelServices.RegisterChannel(channel);
我试过了,去掉这两行也可以正确运行。那么到底什么时候需要在客户端注册通道呢?
我的理解是只有当服务器需要回调客户端方法的时候才需要。
不知道对不对,望能解惑
thnx in advance   回复  引用  查看    

#7楼 [楼主] 2004-12-31 13:06 wayfarer

@buaaytt:

是的,客户端的通道确实并不需要注册。至于什么时候需要,我现在也不太清楚。不过,我始终认为,加上这两行比不加好。呵呵:)   回复  引用  查看    

#8楼 [未注册] 2004-12-31 14:01 buaaytt

嗯,看完了。遍历委托链确实是个解决的好方法。
还有个小问题(呵呵,我这个人比较爱挑刺):
第一节实际上并没有把事件用于Remoting,事件的发布,订阅和响应都在服务端实现了。只不过在远程对象里有个事件,但是并没有通过Remoting传给客户端,而是直接传给了服务端。所以通过一个方法也可以实现相同的功能,而不用使用事件了。
所以我觉得应该说明一下,不然可能会对一些初学者理解Remoting造成误会
个人见解,楼主斟酌
非常感谢楼主的这篇文章,为我仔细理解Remoting中的事件提供了很大的帮助,再次对楼主的努力表示感谢   回复  引用  查看    

#9楼 [未注册] 2004-12-31 14:05 buaaytt

不好意思,上文说的“所以通过一个方法也可以实现相同的功能,而不用使用事件了。”这句话有问题,还是要通过事件来实现的。
为我的鲁莽表示歉意   回复  引用  查看    

#10楼 [楼主] 2004-12-31 15:21 wayfarer

@buaaytt:

没什么啊,还得感谢你认真看了我的文章啊。写出来的,如果没人看,那才糟糕。

欢迎你再多提问题,呵呵^_^   回复  引用  查看    

#11楼 [未注册] 2005-01-09 18:44 helin

看后受益很大, 谢谢wayfarer   回复  引用  查看    

#12楼 [未注册] 2005-01-09 18:48 helin

不知道wayfarer有没有考虑过一种情况, 在客户端订阅服务端事件中, 有可能说我某个客户端可以触发服务器端的事件, 从而对所有客户端进行广播, 刚学Remoting, 请指教, 谢谢.   回复  引用  查看    

#13楼 [楼主] 2005-01-10 08:47 wayfarer

@helin

你仔细想想,其实我说的第三种方式“客户端订阅客户端事件”,就可以实现你说的方式。   回复  引用  查看    

#14楼 [未注册] 2005-01-19 11:42 阿三

一个远程对象,能不能同时启两个通道?tcp/http

一个用于和web页通信,一个告知winform?   回复  引用  查看    

#15楼 [楼主] 2005-01-19 13:59 wayfarer

可以,同时注册两个通道。如果,一个是tcp,一个是http,就很简单,直接注册就可以了。

如果两个都是同类型的,如tcp,则必须注册两个名字不同的通道。   回复  引用  查看    

#16楼 [未注册] 2005-01-20 13:50 阿三

博客写得不错,以后我会多来拜访的……
我最近也在学习remoting,不过我学.net没多长时间,大概有半年
还请多指教   回复  引用  查看    

#17楼 [未注册] 2005-01-25 16:36 疑惑

我在两台server上run,结果事件可以注册上,可是触发时却发生错误。
单台server上就没有问题,奇怪   回复  引用  查看    

#18楼 [未注册] 2005-01-25 19:51 Gemili

如何註銷通道?請寫個小例子﹗
我註銷通道後是否再註冊另外一條通道﹗   回复  引用  查看    

#19楼 [未注册] 2005-01-25 20:48 Gemili

補充一下
在Client端已經註冊了通道,如何更換通道﹗當我再註冊時說通道已經註冊過了﹗   回复  引用  查看    

#20楼 [未注册] 2005-01-31 14:06 可卡

请问:我用客户端发传真模式,现在我想实现:当服务器接到一个信息后返回一个接收标志,即告诉客户端已经接收到信息。能不能实现呀,该如何设置呢?谢谢了   回复  引用  查看    

#21楼 [未注册] 2005-01-31 14:11 可卡

请问:我用客户端发传真模式,现在我想实现:当服务器接到一个信息后返回一个接收标志,即告诉客户端已经接收到信息。能不能实现呀,该如何设置呢?谢谢了   回复  引用  查看    

#22楼 [未注册] 2005-01-31 15:17 可卡

我用客户端发传真模式,现在我想实现:当服务器接到一个信息后返回一个接收标志,即告诉客户端已经接收到信息。能不能实现呀,该如何编写呢?谢谢了;刚接触remoting技术不久,但任务的期限又快到了。急呀   回复  引用  查看    

#23楼 [楼主] 2005-01-31 16:14 wayfarer

@可卡:
你可以将“服务端订阅客户端事件”和“客户端订阅服务端事件
”两者结合起来。

这段时间我非常忙,所以,也没有时间去提供具体的解决方案。但解决思路,就是这样。

鉴于在我的博客中,提出有关Remoting的问题的人比较多。目前,我正准备做一个Remoting管理器的开源项目。希望能把这些需求也考虑进去。等项目做到一定程度的时候,希望能够公布beta版本,并将源代码放到博客上。当然,这要比较长一段时间了。   回复  引用  查看    

#24楼 [未注册] 2005-01-31 19:38 可卡

首先感谢wayfarer 为我提供的思路,我会试着做一下;然后就是说句对不起,现在上来一看才知道自己把一个问题提交了三次,真是不好意思,因为当时网络不好,提交的时候总是说无法显示该页,所以我提交了三次。不好意思。   回复  引用  查看    

#25楼 [未注册] 2005-02-02 10:29 可卡

wayfarer: 再帮我提供一个思路吧?现在按你的方法能实现:当服务端接收到客户端的信息后能返回一个标识给客户端,但 是广播的形式,即所有的客户端都同时会接收到标识,你说我应该怎么样才能把标识返回给刚才发信息的那个客户端呀。   回复  引用  查看    

#26楼 [未注册] 2005-02-03 15:14 笨小孩

您的文章真的太好了.
有个问题请教一下,您代码中的if (BroadCastEvent != null)这句怎么翻译成vb代码,谢谢!!

另外请问一下,您的新随笔怎么才能登录,谢谢!   回复  引用  查看    

#27楼  2005-02-15 23:52 ichs

 

在这里学到很多关于.NET Remoting方面的东西,但在测试中却遇到了问题,麻烦帮忙看一下好吗?
我用的是TcpChannel,客户端激活模式.在本地局域网使用没有问题,后来回家后从外网连到公司服务器时可以获得远程对象却无法取得服务器端数据.
后来在客户端用断点跟踪发现远程对象里找到一个字段 _machineAndPort 的值是 "192.168.0.1:8888",这是服务器中的本地局域网地址,当在内存中把它改成外网的IP地址后继续执行就可以了.
如果在服务器端跟踪,在注册信道中找到一个名为_channelURIs 的数组,它的值与上面相同,如果在内存中直接改为外网IP地址,远程使用也没问题.但不知道如何用代码直接改变它的值.
我试着这样去改变它的值:
TcpChannel tcpChannel = new TcpChannel(8888);   
ChannelDataStore channelData = (ChannelDataStore)tcpChannel;
channelData.ChannelUris = "xxx.xxx.xxx.xxx:8888";
从内存中看它的值是改变了,但是却根本无法使用了
没办法只好改用IP地址绑定再试:

   IDictionary tcpProperties = new Hashtable();
   tcpProperties["port"] = 8888;
   tcpProperties["rejectRemoteRequests"] = false;
   tcpProperties["bindTo"] = "xxx.xxx.xxx.xxx";

这样从远程连接是可以使用了,但在本地局域网中却也只能使用那个外网的地址才能使用了.如果直接在内存中修改 _channelURIs 的值就不会有这个问题.
今天又增加了远程事件部份,发现客户端注册的通道在注册远程事件时存在相同的问题.
该怎么办呢?难道你们使用的时候都没有这种现象吗?



  回复  引用  查看    

 

#28楼 [楼主] 2005-02-17 12:40 wayfarer

@ ichs:

因为我在运用Remoting时,没有使用外网。你的这个反馈很有意义。

你可否使用多通道的方法,将本地局域网的IP和外网的IP,注册到不同的通道中,使通道同时存在。不知是否可行?   回复  引用  查看    

#29楼  2005-02-19 21:59 ichs

 

wayfarer:
感谢你百忙中抽出时间来回答我的问题!
C#我是刚开始学,很菜!不要见笑,以前只做过VFP+SQL。
现在是想用SQL做后台,服务器端和客户端用Channel来做,一方面觉得用网页做界面做数据录入和报表打印太难控制,另一个方面因自己懂的东西太少,加上安全性方面想用自己的方式去控制,所以.......
关于你说的用多通道的方式我做了一些测试,现在把这些结果贴出来,请再帮忙看看有否解决办法。
一、 两端都是局域网,但都是直接连上外网的。说到局域网的意思是两端的机器都具有多个IP地址(局域网和外网)。
部份代码如下:
[server]
IDictionary tcpProperties = new Hashtable();
tcpProperties["name"] = "tcpBinary";
tcpProperties["port"] = 8888;
tcpProperties["useIpAddress "] = false;
tcpProperties["rejectRemoteRequests"] = false;
//tcpProperties["bindTo"] = localIPAddress; //注:使用IP地址绑定时将无法使用其它IP地址连接
BinaryClientFormatterSinkProvider tcpClientSinkProvider = new BinaryClientFormatterSinkProvider();
BinaryServerFormatterSinkProvider tcpServerSinkProvider = new BinaryServerFormatterSinkProvider();
tcpServerSinkProvider.TypeFilterLevel = TypeFilterLevel.Full;
TcpChannel tcpChannel = new TcpChannel(tcpProperties,tcpClientSinkProvider,tcpServerSinkProvider);
//ChannelDataStore channelData = (ChannelDataStore)channel.ChannelData;
//SetChannelUris(channelData);
ChannelServices.RegisterChannel(tcpChannel);
RemotingConfiguration.RegisterActivatedServiceType(typeof(SQLServer));
//注:SQLServer为服务对象

[Client]
IDictionary tcpProperties = new Hashtable();
tcpProperties["name"] = "tcpBinary";
tcpProperties["port"] = 0;
tcpProperties["useIpAddress "] = false;
tcpProperties["rejectRemoteRequests"] = false;
BinaryClientFormatterSinkProvider tcpClientSinkProvider = new BinaryClientFormatterSinkProvider();
BinaryServerFormatterSinkProvider tcpServerSinkProvider = new BinaryServerFormatterSinkProvider();
tcpServerSinkProvider.TypeFilterLevel = TypeFilterLevel.Full;
TcpChannel tcpChannel = new TcpChannel(tcpProperties,tcpClientSinkProvider,tcpServerSinkProvider);
//ChannelDataStore channelData = (ChannelDataStore)channel.ChannelData;
//SetChannelUris(channelData);
ChannelServices.RegisterChannel(tcpChannel);

string appUrl = "tcp://xxx.xxx.xxx.xxx:8888;
UrlAttribute urlattr = new UrlAttribute(appUrl);
object[] args = {urlattr};  //远程对象Url
SQLServer sqlServer = (SQLServer)Activator.CreateInstance(typeof(SQLServer),null,args);
 
 [一个公共函数]
  private static void SetChannelUris(ChannelDataStore channelData)
  {
   //获得本机IP地址(最后一个) 即:如果有外网地址使用外网地址,否则使用内网地址
   IPAddress[] ipList = Dns.GetHostByName(Dns.GetHostName()).AddressList;
   string localIPAddress;
   if (ipList.Length > 1)
    localIPAddress = ipList[1].ToString();
   else
    localIPAddress = ipList[0].ToString();
   string localPoint = channelData.ChannelUris[0];
   //取得协议
   int i = localPoint.IndexOf(":");
   string confer = localPoint.Substring(0,i + 3);
   //取得信道的端口号
   i = localPoint.LastIndexOf(":");
   string localPort = localPoint.Substring(i,localPoint.Length - i);
   //重设信道IP地址
   string[] IpAndPort = {confer + localIPAddress + localPort};
   channelData.ChannelUris = IpAndPort;
  }
 
使用这种方法在创建对象时没有问题,但在创建对象后调用远程对象中的方法来获得远程数据时就出错。如果两台机器处于同一个局域网时不会有问题。
我用[金山网镖]来监视连接状况,发现在创建远程对象连接到的是远程IP地址,而在调用远程对象中的函数时要连接的却是对方的局域IP地址,就是说得到的远程对象中的地址指向的是远程方的本地IP。
然后使用了以上代码中注释的那两句,也就是使用下方的一个公共函数来修改信道数据中的IP地址后再注册远程对象,这样使用起来就没有问题了。TCP和HTTP都试过了,没有问题!
当然,修改信道的IP地址可以使用第一个注释中的IP地址绑定来实现,我测试过,如果用IP地址绑定的话,即使是本地的机器要连接时也必须要使用外网的地址才能连接到服务器,如果这个IP地址是动态的话(比如用的是ADSL),当断开外网后要重启服务才能使用,不爽!

接下来是使用远程服务器上的事件,用你的方法也试过,没有问题!

再接下来,客户端在局域网内,要经过使用ISA防火墙的服务器连到Internet上的其它服务器。
问题又来了,用TCP协议,内网无法连接到外网,也没找到有关于TcpChannel设置使用代码服务器的方法。
那就用HttpChannel好了,而且用WebProxy设置了代理服务器。
连接到远程对象:没有问题!
执行远程方法:没有问题!
使用服务器事件:出错了,TMD!又出错了 :(
再用[网镖]去看,老问题出来了,服务器事件要发送数据到客户端,使用的地址是客户端局域网出局用的本地服务IP址:192.168.0.1:8080,晕!
在断点跟踪中看事件对象在内存中的一些参数,不太知道是啥意意,反正找一些关于IP地址的值来看呗!能找到的IP地址有,客户端的局域网IP地址:192.168.0.17:xxxx,还有客户端使用的代理IP地址:192.168.0.1:8080。
没了,找不到其它的了,晕!服务器端当然无法根据这两个地址将事件数据发送到客户端了。
客户端不设置代理服务,也一样,没什么分别,从断点中看数据好象HttpChannel会自动使用浏览器中的代理服务设置,根本不用设置。
我是没办法了,你有空看看有没办法吧,想不出来有什么办法把数据发送到人家客户端中去,人家在是在局域网内网呀。

真真不行的话远程事件就只好不用了,改用其它方法主动去获取数据好了。
还有两个重要的问题:
1、如何获得连接的客户端IP地址?假如有人要用密码穷举,我也好记录它然后拒绝连接是不是 :)
方法在网上找到过,但没有具体使用示例,怪自己太菜,还是不会用。
2、如何用自定义的信道收发器,然后加入一个数据加密功能。
网上找了很久,关于Channel方面的实在太少,在你这里看到的已经是最满意的了,如果你再不帮我,那我可真的没地方问去了。
好了,我下班回家去了 :)
  回复  引用  查看    

 

#30楼 [未注册] 2005-02-21 12:03 琴雨潇箫

拜读之后受益颇多,对Remoting的理解和使用更深了一层,十分感谢:)
另:或许是笔者别有用途,亦或是小疏忽,在“服务器端订阅客户端事件”一节中,服务器端的远程对象注册过程有一步我看不大明白,如下:

BroadCastObj Obj = new BroadCastObj();
ObjRef objRef = RemotingServices.Marshal(Obj,"BroadCastMessage.soap");

第二个语句所得到的objRef并没有使用,而是只需要将给定的 MarshalByRefObject 转换为具有指定 URI 的 ObjRef 类的实例,
所以我觉得只要进行转换而不需要获得转换后的实例,如下:

BroadCastObj Obj = new BroadCastObj();
RemotingServices.Marshal(Obj,"BroadCastMessage.soap");

另外,对上面的“服务器端订阅客户端事件”的服务器端、客户端做了些小改动,改用配置文件进行配置,不才,献丑:

Server配置文件Server.config:

<configuration>
<System.Runtime.Remoting>
<application>
<channels>
<channel ref="http" port="8086">
<serverProviders>
<formatter ref="binary" />
</serverProviders>
<clientProviders>
<formatter ref="binary" />
</clientProviders>
</channel>
</channels>
<service>
<wellknown type="Share.Announcer, Share" objectUri="Announcer" mode="Singleton" />
</service>
</application>
</System.Runtime.Remoting>
</configuration>

服务器端StartServer方法代码:
RemotingConfiguration.Configure( "Server.config" );
_Announcer = new Share.Announcer();
_Announcer.Messenger += new BroadcastHandler( ShowMessage );
RemotingServices.Marshal( _Announcer, "Announcer" );




Client配置文件Client.config:
<configuration>
<System.Runtime.Remoting>
<application>
<channels>
<channel port="0" ref="http">
<clientProviders>
<formatter ref="binary" />
</clientProviders>
<serverProviders>
<formatter ref="binary" />
</serverProviders>
</channel>
</channels>
<client>
<wellknown type="Share.Announcer, Share" url="http://localhost:8086/Announcer" />
</client>
</application>
</System.Runtime.Remoting>
</configuration>


客户端StartClient方法代码: RemotingConfiguration.Configure( "Client.config" );
_IBroadcast = ( Share.IBroadcast )new Share.Announcer();
Share.EventWrapper wrapper = new Share.EventWrapper();
wrapper.WrapperBroadcast += new Share.BroadcastHandler( ShowMessage );
_IBroadcast.Broadcast += new Share.BroadcastHandler( wrapper.SendBroadcast );




只是Server端信道端口采用8086,公共类配件和远程处理对象类的命名发生了些变化,采用Http协议,信道格式使用Binary   回复  引用  查看    

#31楼 [未注册] 2005-02-21 12:03 琴雨潇箫

拜读之后受益颇多,对Remoting的理解和使用更深了一层,十分感谢:)
另:或许是笔者别有用途,亦或是小疏忽,在“服务器端订阅客户端事件”一节中,服务器端的远程对象注册过程有一步我看不大明白,如下:

BroadCastObj Obj = new BroadCastObj();
ObjRef objRef = RemotingServices.Marshal(Obj,"BroadCastMessage.soap");

第二个语句所得到的objRef并没有使用,而是只需要将给定的 MarshalByRefObject 转换为具有指定 URI 的 ObjRef 类的实例,
所以我觉得只要进行转换而不需要获得转换后的实例,如下:

BroadCastObj Obj = new BroadCastObj();
RemotingServices.Marshal(Obj,"BroadCastMessage.soap");

另外,对上面的“服务器端订阅客户端事件”的服务器端、客户端做了些小改动,改用配置文件进行配置,不才,献丑:

Server配置文件Server.config:

<configuration>
<System.Runtime.Remoting>
<application>
<channels>
<channel ref="http" port="8086">
<serverProviders>
<formatter ref="binary" />
</serverProviders>
<clientProviders>
<formatter ref="binary" />
</clientProviders>
</channel>
</channels>
<service>
<wellknown type="Share.Announcer, Share" objectUri="Announcer" mode="Singleton" />
</service>
</application>
</System.Runtime.Remoting>
</configuration>

服务器端StartServer方法代码:
RemotingConfiguration.Configure( "Server.config" );
_Announcer = new Share.Announcer();
_Announcer.Messenger += new BroadcastHandler( ShowMessage );
RemotingServices.Marshal( _Announcer, "Announcer" );




Client配置文件Client.config:
<configuration>
<System.Runtime.Remoting>
<application>
<channels>
<channel port="0" ref="http">
<clientProviders>
<formatter ref="binary" />
</clientProviders>
<serverProviders>
<formatter ref="binary" />
</serverProviders>
</channel>
</channels>
<client>
<wellknown type="Share.Announcer, Share" url="http://localhost:8086/Announcer" />
</client>
</application>
</System.Runtime.Remoting>
</configuration>


客户端StartClient方法代码: RemotingConfiguration.Configure( "Client.config" );
_IBroadcast = ( Share.IBroadcast )new Share.Announcer();
Share.EventWrapper wrapper = new Share.EventWrapper();
wrapper.WrapperBroadcast += new Share.BroadcastHandler( ShowMessage );
_IBroadcast.Broadcast += new Share.BroadcastHandler( wrapper.SendBroadcast );




只是Server端信道端口采用8086,公共类配件和远程处理对象类的命名发生了些变化,采用Http协议,信道格式使用Binary   回复  引用  查看    

#32楼  2005-02-21 12:09 琴雨潇箫

抱歉...未登录情况下碰到网络故障,回复了两次...   回复  引用  查看    

#33楼 [未注册] 2005-02-23 16:08 helin

如果Server端本身就是IIS host的, 也就是说在Server端没有.exe来起Remoting, 该怎么办呢, 谢谢.   回复  引用  查看    

#34楼 [未注册] 2005-03-02 12:08 freebug

写得太好了,让我受益匪浅啊。
但我还有一个问题要请教,我把你的例子下载了,把winform客服端改成了web客户端,向服务端发消息很成功,但服务端发广播客服端没反应,好像没收到,不知道这是怎么回事,你们用web客户端试一下看看行不行啊?   回复  引用  查看    

#35楼 [未注册] 2005-03-02 14:28 freebug

写得太好了,让我受益匪浅啊。
但我还有一个问题要请教,我把你的例子下载了,把winform客服端改成了web客户端,向服务端发消息很成功,但服务端发广播客服端没反应,好像没收到,不知道这是怎么回事,你们用web客户端试一下看看行不行啊?   回复  引用  查看    

#36楼 [未注册] 2005-06-20 14:09 phoenixwonder

楼主, 如果现在选择第二种事件触发方式(即客户端订阅服务器端事件),如果我的服务器端想对某一个客户端产生事件,而不是广播, 请问能不能够实现呢?   回复  引用  查看    

#37楼 [未注册] 2005-07-19 23:26 蓬莱病书生

正需要这方面资料,感谢楼主,写出这么好的文章   回复  引用  查看    

#38楼  2005-07-20 15:37 cloud

拜读之后,受益非浅。想请教,远程技术如何绕过防火墙进行通讯,能够在不改变防火墙配置的情况下,进行远程通讯   回复  引用  查看    

#39楼 [未注册] 2005-08-01 09:46 bindsang

我在做的时候发现一个问题请教一下楼主,在客户端注册服务端事件的类型中,我按照楼主的程序写好以后,一开始运行都没有问题,时间长了,客户端非正常断开的多了,这时候客户端到客户端的广播会显得特别的慢,要过四秒钟左右才能收到,这时非得重启一下服务(我的服务端用的是Windows Service, 客户端用的是Asp.NET)   回复  引用  查看    

#40楼 [未注册] 2005-08-06 00:16 sswv

感谢楼主,我正愁这个呢。

前面说的Client对Client的“私聊的解决办法”,我有一个方案,就是给每个Client一个ID属性,发现的消息也带一个目标ID。Client收到消息后看看消息的目标ID和自己的ID相同才显示,否则不显示……

呵呵,浪费了大量网络资源。   回复  引用  查看    

#41楼 [未注册] 2005-08-18 14:49 辛俊杰

请问上述客户端订阅服务器端事件的方法是不是在客户端激活模式下不可以用啊,我试了很多次,发现主要是因为,用RemotingServices.Marshal处理后的对象与客户端用createInstance产生的对象不同所致,请问该怎么办,谢谢!   回复  引用  查看    

#42楼 [未注册] 2005-08-23 10:25 初学者

您好,我下载了你的例子,在同一台机子上运行可以,但分别放到两台机子上怎么就不行呢?需要作什么设置吗?   回复  引用  查看    

#43楼 [未注册] 2005-10-10 16:17 井底之蛙

感谢Wayfarer!
我在做一个小东东,想实现象局域网游戏一样随便指定一台server,所有的client都能与server自动连接,而用Remoting技术,在client端必须先知道Server的ip,否则无法生成远程对象实例,我若不想手动在client更改这个ip,应该怎么解决?   回复  引用  查看    

#44楼 [未注册] 2005-11-29 16:47 starhit

好文章   回复  引用  查看    

#45楼 [未注册] 2005-12-01 10:27 kawashima

你好,刚学.Net Remoting,我们现在需要在应用程序域中传递DataSet对象,请问如何序列化DataSet对象啊,期待你的回复,我现在非常急,谢谢!   回复  引用  查看    

#46楼 [未注册] 2005-12-08 17:35 lampson

这样做了以后,怎么控制客户端的生命周期呢?   回复  引用  查看    

#47楼 [未注册] 2005-12-31 20:07 Legend

你的文章忒好了~~~~~~~~~~!!!!!!!!!!!
我搞了一个星期的东西在你这里找到答案了
i love you!!!
i love you!!!
i love you!!!
i love you!!!
i love you!!!
i love you!!!   回复  引用  查看    

#48楼 [未注册] 2005-12-31 20:36 Legend

不好意思,回复之后再看回帖,确实觉得有点灌水的嫌疑,是太激动了,请见谅!

总结一下上面的回帖,有以下问题:(不知道楼主有没有时间来回复哦~~~)
(1)wayfarer: 再帮我提供一个思路吧?现在按你的方法能实现:当服务端接收到客户端的信息后能返回一个标识给客户端,但 是广播的形式,即所有的客户端都同时会接收到标识,你说我应该怎么样才能把标识返回给刚才发信息的那个客户端呀。

(2)楼主, 如果现在选择第二种事件触发方式(即客户端订阅服务器端事件),如果我的服务器端想对某一个客户端产生事件,而不是广播, 请问能不能够实现呢?

(3)拜读之后,受益非浅。想请教,远程技术如何绕过防火墙进行通讯,能够在不改变防火墙配置的情况下,进行远程通讯

(4)我在做的时候发现一个问题请教一下楼主,在客户端注册服务端事件的类型中,我按照楼主的程序写好以后,一开始运行都没有问题,时间长了,客户端非正常断开的多了,这时候客户端到客户端的广播会显得特别的慢,要过四秒钟左右才能收到,这时非得重启一下服务(我的服务端用的是Windows Service, 客户端用的是Asp.NET)


(5)感谢Wayfarer!
我在做一个小东东,想实现象局域网游戏一样随便指定一台server,所有的client都能与server自动连接,而用Remoting技术,在client端必须先知

道Server的ip,否则无法生成远程对象实例,我若不想手动在client更改这个ip,应该怎么解决?

(6)你好,刚学.Net Remoting,我们现在需要在应用程序域中传递DataSet对象,请问如何序列化DataSet对象啊,期待你的回复,我现在非常急,谢谢!   回复  引用  查看    

#49楼 [未注册] 2006-01-11 16:46 彬彬

只能在一台机器上运行成功,多台机器部署就失败了,请问如何修改?   回复  引用  查看    

#50楼 [未注册] 2006-03-15 15:20 蓝牙

首先感谢Wayfarer
另外有个问题就是客户端使用watch的BroadCastingInfo方法发送消息可以吗,我测试了一下有时候能发出去但是客户端会从此没有反应了,是什么原因呀?   回复  引用  查看    

#51楼 [未注册] 2006-03-15 22:17 huayu

我用的remoting怎么就是在互连网上运行不了,局域网行,我用的http通道,怎么弄也不行!恳请Wayfarer指点!谢啦   回复  引用  查看    

#52楼 [未注册] 2006-04-14 16:07 搞不懂

客户端注册了远程对象中的事件后,当服务器端的远程对象事件被触发时,是由远程对象根据事件委托中的信道数据执行客户端的事件方法。
当客户端使用代理或路由器上网时,从事件委托中获得的信道数据是客户端内网的IP地址和端口而不是路由器转接时分配的外网地址和端口,那么也就无法执行客户端的事件方法了。
到底怎样才能实现远程事件呢?   回复  引用  查看    

#53楼 [未注册] 2006-04-15 20:59 minghaochan

未处理的“System.Security.SecurityException”类型的异常出现在 mscorlib.dll 中。

其他信息: 不允许类型 System.DelegateSerializationHolder 和从中派生的类型(例如 System.DelegateSerializationHolder)在此安全级别上被反序列化。   回复  引用  查看    

#54楼 [未注册] 2006-05-11 15:37 丁丁猫转世

在客户端意外断电后出现"无法连接到远程服务器的错误"看似莫名其妙,客户端没了怎么会在服务器出现无法连接到远程服务器的错误呢?

其实我们反过来想就好理解了,wrapper是在客户端实例化的,服务器调用wrapper的时候其实是把客户端当成了服务器端来操作,这时wrapper对于服务器端来说又成了远程对象.

另外,楼主说"EventWrapper因为要重写InitializeLifetimeService()方法,所以仍然要必须继承MarshalByRefObject类."其实EventWrapper要继承MarshalByRefObject类,并不是仅仅为了重写InitializeLifetimeService()方法,虽然这是必要的,最关键的一点是EventWrapper要作为远程对象来供服务器端调用,所以才会必须继承MarshalByRefObject类.
  回复  引用  查看    

#55楼 [未注册] 2006-09-18 23:30 haha[匿名]

Remoting事件(客户端发传真)报错,
public void OnFaxSended(string fax)
{
txtFax.Text += fax;
txtFax.Text += System.Environment.NewLine;
}
中的txtFax.Text += fax;错误提示为:
线程间操作无效,从不是创建控件"txtFax"的线持访问它
如何解决?   回复  引用  查看    

#56楼 [未注册] 2006-10-10 19:27 feng[匿名]

您好!如果将上面两个例子结合起来啊?
就是 一个客户端通过服务器端通知其他客户端   回复  引用  查看    

#57楼 [未注册] 2006-10-20 10:38 free_fly

好文章,拜读之后,受益非浅   回复  引用  查看    

#58楼 [未注册] 2006-10-20 13:29 small[匿名]

很好!好久没有看到高水平的文章了
不知道文章中所说的“私聊”(估计就是“悄悄话”的意思)这个问题解决了没有?期待中。。。。   回复  引用  查看    

#59楼 [未注册] 2006-12-28 18:37 qualylee

收藏!!:)   回复  引用  查看    

#60楼 [未注册] 2007-01-24 11:30 Michael

您好,看了您的文章对于我学习remoting很有帮助,所以非常感谢您。现在我为了巩固remoting方面的知识,开始做一个小型的聊天程序,现在遇到一个问题需要请教您一下。

现在每个客户端上线的时候都会通过调用远程对象的Login方法将本机的IP和用户名传入服务端,并通过一个远程事件来获得所有在线用户的列表,这个列表是在服务端保存的以IP为Key的Hashtable,并通过各个客户端登录时收集以及在客户端退出时移除。但是当有客户端非正常退出时就无法通知服务端来移除列表中的这一项,用户列表就无法得到更新。目前我判断用户是否在线是通过事件触发时是否发生异常,并将发生异常的委托从委托链中移除,但是用户列表由于得不到这个委托对应订阅者的IP就没法移除了。

所以我想请问有没有方法在服务端得到这个委托所对应订阅者的信息呢?或者说服务端提供的事件被多少客户端订阅了?   回复  引用  查看    

#61楼 [未注册] 2007-01-24 17:45 JAngel

Remoting事件(客户端发传真)报错,
public void OnFaxSended(string fax)
{
txtFax.Text += fax;
txtFax.Text += System.Environment.NewLine;
}
中的txtFax.Text += fax;错误提示为:
线程间操作无效,从不是创建控件"txtFax"的线持访问它
为什么我运行回出错呢?   回复  引用  查看    

#62楼 [未注册] 2007-02-27 22:12 yesorno2003

看了你的文章后,受溢颇深。先感谢一下。
将代码下载下来也试了一下。第三节的客户端订阅客户端的也试了一下。不错。

但是,在测试客户端订阅客户端时,发现有这么一个问题,就是客户端可以接受别的客户发来的消息,但是不能发送消息。
如果在注册客户端通道时,加上props["name"] = "ClientHttp";这么一句,就可以发送消息,但是又不能接受消息了。

请问这该怎么处理。谢谢。。   回复  引用  查看    

#63楼 [未注册] 2007-03-21 05:11 柳疃

听了你的系统介绍,也下载了示例程序,但是在Remoting事件(服务端广播改进)运行过程中发生错误,没有一次能运行成功,代码行为:tempEvent(info);出现的错误是system.net.sockets.socketException,错误描述为:由于连接方在一段时间内没有答复或连接的主机没有反应.,请问是为什么?能否给我发一份完整的能运行的例子到我的邮箱,谢谢
sqlu2006@gmail.com
我用的VS2005英文版,framework2.0

万分感谢   回复  引用  查看    

#64楼 [未注册] 2007-03-21 05:12 柳疃

听了你的系统介绍,也下载了示例程序,但是在Remoting事件(服务端广播改进)运行过程中发生错误,没有一次能运行成功,代码行为:tempEvent(info);出现的错误是system.net.sockets.socketException,错误描述为:由于连接方在一段时间内没有答复或连接的主机没有反应.,请问是为什么?能否给我发一份完整的能运行的例子到我的邮箱,谢谢
sqlu2006@gmail.com
我用的VS2005英文版,framework2.0   回复  引用  查看    

#65楼 [未注册][TrackBack] 2007-04-13 10:56 Sean(陈阳)

[引用提示]Sean(陈阳)引用了该文章, 地址: http://www.cnblogs.com/chy417/archive/2007/04/13/711735.html   回复  引用  查看    

#66楼 [未注册][TrackBack] 2007-04-13 10:57 Sean(陈阳)

[引用提示]Sean(陈阳)引用了该文章, 地址: http://www.cnblogs.com/chy417/archive/2007/04/13/711741.html   回复  引用  查看    

#67楼 [未注册][TrackBack] 2007-04-13 10:59 Sean(陈阳)

[引用提示]Sean(陈阳)引用了该文章, 地址: http://www.cnblogs.com/chy417/archive/2007/04/13/711745.html   回复  引用  查看    

#68楼 [未注册][TrackBack] 2007-05-14 13:52 cainiao

Microsoft.NetRemoting系列专题之三:Remoting事件处理全接触 我写的.NetRemoting系列专题: Microsoft.NetRemoting系列专题...
[引用提示]cainiao引用了该文章, 地址: http://www.cnblogs.com/rd0204/archive/2007/05/14/745735.html   回复  引用  查看    

#69楼 [未注册] 2007-06-16 10:31 Fox

樓主拜讀了您的文章後,受益良多,不過有一個問題,不知您發現沒有,在調用客戶端的方法中,若對界面控件進行操作,提示線程出錯的問題,出錯提示為"Cross-thread operation not valid: Control 'CurrencyGrid' accessed from a thread other than the thread it was created on",不知您遇到過沒有?如果有遇到請告知是如何解決的,我知道多線程在操作同一控件時有一個同步和鎖定的問題,調用控件的Invoke方法,通過指代來掃行。但我用此方法最依然出現該錯誤,請指教,非常感謝!   回复  引用  查看    

#70楼 [未注册] 2007-06-16 17:49 Fox

這個問題已經解決了,調用BeginInvoke就可以讓多個線程訪問同一控件,但用Invoke卻不行,如果是自己創建的多個線程,用Invoke卻可以,這個問題日後再深究,謝謝了!   回复  引用  查看    

#71楼 [未注册][TrackBack] 2007-07-12 09:23 啊东hd

Microsoft.NetRemoting系列专题之一:.NetRemoting基础篇Microsoft.NetRemoting系列专题之二:Marshal、Disconnect与生命周期...
[引用提示]啊东hd引用了该文章, 地址: http://www.cnblogs.com/wskaihd/archive/2007/07/12/814986.html   回复  引用  查看    

#72楼  2007-07-24 10:57 阿平

我在用你的示例:
Remoting事件(服务端广播改进)
时,在单机运行客户端、服务器发送消息没有问题,但是在两台机器上测试,一台服务器,一台客户端,就在客户端产生异常:“输入流是无效的二进制格式。开始内容(以字节为单位)是: 3C-21-44-4F-43-54-59-50-45-20-48-54-4D-4C-20-50-55...”,异常类型是serializationexception,不知道楼主是否遇到过这个问题?   回复  引用  查看    

#73楼 [未注册][TrackBack] 2007-08-14 11:16 杨艳峰

Microsoft.NetRemoting系列专题之三:Remoting事件处理全接触 前言:在Remoting中处理事件其实并不复杂,但其中有些技巧需要你去挖掘出来。正是这些技巧,仿佛森...
[引用提示]杨艳峰引用了该文章, 地址: http://www.cnblogs.com/Apex-yang/archive/2007/08/14/854982.html   回复  引用  查看    

#74楼 [未注册][TrackBack] 2007-08-23 17:40 zhangh

Microsoft.NetRemoting系列专题之一:.NetRemoting基础篇 Microsoft.NetRemoting系列专题之二:Marshal、Disconnect与...
[引用提示]zhangh引用了该文章, 地址: http://www.cnblogs.com/zhangh/archive/2007/08/23/867155.html   回复  引用  查看    

#75楼 [未注册] 2007-09-07 01:14 QQ43725443

文章写得不错。顺便回答一下问题:
----------------------------------------------------------
HttpChannel channel = new HttpChannel(0);
ChannelServices.RegisterChannel(channel);
我试过了,去掉这两行也可以正确运行。那么到底什么时候需要在客户端注册通道呢?
我的理解是只有当服务器需要回调客户端方法的时候才需要。
----------------------------------------------------------
没错,在某些地方是可以正常运行,但是,如果当你有远程对象从客服端上传过来,比如Process对象,那么就需要注册信道,否则就会报错。   回复  引用  查看    

#76楼 [未注册] 2007-09-12 15:20 zhfeng

直接把例子改成tcpchannel形式,程序就报错,“服务器提交了协议冲突. Section=ResponseStatusLine” 。出现这个错误,我该怎么修改?   回复  引用  查看   

#1楼 [未注册] 2004-12-10 17:06 beta
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:307920次
    • 积分:4070
    • 等级:
    • 排名:第7874名
    • 原创:36篇
    • 转载:264篇
    • 译文:2篇
    • 评论:86条
    最新评论
    Sysbase PowerDesigner 12