Remoting疑惑续集之再续
如果各位不知道我的疑惑是什么,请看《Remoting疑惑续集》。经过我和CSL的讨论,明白了一些问题,大家可以看文章的评论,也可参考《A Big Problem about the Microsoft .Net Remoting》。最近这段时间,由于要做其他事情,暂时抛开了Remoting。前几天我在改我的Remoting服务管理器时,又根据以前讨论后的结果再做了测试,结果发现一些问题。
根据MSDN的官方文档,ChannelServices.UnregisterChannel(channel),在注销通道的同时,已经释放了通道占用的端口,分析System.Runtime.Remoting的代码(《A Big Problem about the Microsoft .Net Remoting》有简单地分析),也可以看到这点。但不知道是否是Remoting的bug,一旦客户端调用了该通道提供的远程对象后,UnregisterChannel()方法事实上并没有释放掉端口的占用。
虽然CSL说UnregisterChannel()方法,会默认调用StopListening()关闭监听。但我还是想尝试一下显式调用的方法。最初我考虑到Remoting底层的通讯采用的是Socket,所以我想调用TcpListener类的Stop()方法来关闭对通道的调用。
listener.Stop();
这里的IP地址和端口号,都是Remoting所使用的设置。我把这段代码放到注销通道的方法前。现在我按照原来出现的问题进行测试。首先建立Remoting服务,Marshal远程对象。然后在客户端调用该对象。此时注销通道,紧接着立刻注册通道。OK,没有出现“通常每个套接字地址 (协议/网络地址/端口)
只允许使用一次。”异常。似乎这个方案是可行的了。
由于用这个方法,必须在构造函数中指定Remoting要用的ip地址和端口号,很难和我原来的程序整合起来,所以我还想试验其他方法。既然StopListening()方法最终还是调用TcpListener的Stop()方法,那么直接显式调用它,是否也可行呢?
我在注销通道的方法中作了如下修改:
foreach (IChannel channel in channels)
{
switch (channelType)
{
case ChannelType.Http:
if (channel.ChannelName.ToLower().IndexOf("http") >= 0)
{
//关闭和释放资源;
((HttpChannel)channel).StopListening(null);
ChannelServices.UnregisterChannel(channel);
}
break;
case ChannelType.Tcp:
if (channel.ChannelName.ToLower().IndexOf("tcp") >= 0)
{
((TcpChannel)channel).StopListening(null);
ChannelServices.UnregisterChannel(channel);
}
break;
}
}
说明一下:ChannelType是我定义的通道类型枚举。然后我在注册通道的时候,根据通道类型,分别为通道取名为“通道类型+端口号”,例如Tcp通道调用9000端口,则该通道名为:Tcp9000。这段代码是通过RegisteredChannels属性获得已经注册的通道,然后判断通道名,以获得通道的类型,并进行强制转换,进而调用StopListening()方法: ((TcpChannel)channel).StopListening(null);
再测试后,果然是正确的。
写到这里似乎文章该结束了,而且根据讨论的情况,其实应该在上一篇文章就该结束我这啰嗦的疑惑了。不过为了稳妥起见,我还是不断地测试,以保证程序的健壮性。终于在我测试了n次后,无意发现了大的Bug。
我在程序中,是将Remoting的远程对象分别分为Http和Tcp两种通道类型来发布的。客户端在调用远程对象时,根据通道类型,选择Http或者Tcp组成正确的uri来调用。测试时,我在客户端调用Tcp通道的远程对象,然后注销Tcp通道,接着再次注册,No problem!此时我再注销Http通道,接着再次注册,异常令人讨厌的出现了。反之,我在客户端调用Http通道的远程对象,注销和注册Http通道就没有问题,而对Tcp通道进行操作,仍然会抛出异常。
奇怪吧!确实够奇怪的。不过主要还是我的需求奇怪。首先,我的需求是:能够启动和停止指定远程对象,能够提供Tcp通道和Http通道两种类型,能够提供注册/注销通道的功能。
先分析我的实现方式:
1、首先注册通道:
if (!IsRegistered(channelType,port))
{
switch (channelType)
{
case ChannelType.Tcp:
//根据端口号和通道类型注册通道;
IDictionary tcpProp = new Hashtable();
tcpProp["name"] = "tcp" + port.ToString();
tcpProp["port"] = port;
channel = new TcpChannel(tcpProp,
new BinaryClientFormatterSinkProvider(),
new BinaryServerFormatterSinkProvider());
ChannelServices.RegisterChannel(channel);
break;
case ChannelType.Http:
IDictionary httpProp = new Hashtable();
httpProp["name"] = "http" + port.ToString();
httpProp["port"] = port;
channel = new HttpChannel(httpProp,
new SoapClientFormatterSinkProvider(),
new SoapServerFormatterSinkProvider());
ChannelServices.RegisterChannel(channel);
break;
}
}
2、发布远程对象:
1)利用反射创建对象:
string dllPath = info.MainAssembly.AssemblyPath;
Assembly assembly = Assembly.LoadFrom(dllPath);
string typeFullName = null;
typeFullName = nameSpace+"."+mainClass;
Type type = assembly.GetType(typeFullName);
//动态创建服务对象;
Object obj = Activator.CreateInstance(type);
2)Marshal该对象:
3、停止该服务对象:
{
try
{
RemotingServices.Disconnect((MarshalByRefObject)obj);
}
catch
{
throw new Exception("停止指定服务错误");
}
}
4、注销通道(如前)
另外,我在注销的时候,关闭通道之前,先停止了该远程对象的连接。(为简便起见,代码作了相应修改)
我们注意到,发布对象时,Remoting并没有方法决定该对象具体发布到哪个通道中。经过我的分析,事实上对对象进行Marshal后,Remoting当前的所有通道都存在该远程对象。这也许就是问题症结。
即使如此,仍然让我困惑的是,当我调用Tcp通道的远程对象后,为什么注销/注册Http通道会出现异常,而Tcp通道则不存在问题呢?因为我在注销Http通道时,既停止了Http通道内的服务对象,又停止了对通道的监听,然后再注销了该通道。即使调用Tcp通道的远程对象后,这个对象会驻留在Http通道中,经过我的一系列操作,Http通道的端口应该已经被释放了啊,为什么会抛出异常呢?从工作机制上看,两种类型通道的原理是一样的啊!?
疑惑!!!