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()方法来关闭对通道的调用。

TcpListener listener = new TcpListener(ipaddress,port);
listener.Stop();

这里的IP地址和端口号,都是Remoting所使用的设置。我把这段代码放到注销通道的方法前。现在我按照原来出现的问题进行测试。首先建立Remoting服务,Marshal远程对象。然后在客户端调用该对象。此时注销通道,紧接着立刻注册通道。OK,没有出现“通常每个套接字地址 (协议/网络地址/端口)
只允许使用一次。
”异常。似乎这个方案是可行的了。

由于用这个方法,必须在构造函数中指定Remoting要用的ip地址和端口号,很难和我原来的程序整合起来,所以我还想试验其他方法。既然StopListening()方法最终还是调用TcpListener的Stop()方法,那么直接显式调用它,是否也可行呢?

我在注销通道的方法中作了如下修改:

   IChannel[] channels = ChannelServices.RegisteredChannels;

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)利用反射创建对象:

//动态加载服务对象的dll;
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该对象:

ObjRef objRef = RemotingServices.Marshal    ((MarshalByRefObject)obj,"Service");

3、停止该服务对象:

public void StopService(Object obj)
        
{
            
try
            
{               
               
          RemotingServices.Disconnect((MarshalByRefObject)obj);                                           
                    }
    
             
catch
            
{
                
throw new Exception("停止指定服务错误");
            }
                
        }

4、注销通道(如前)

另外,我在注销的时候,关闭通道之前,先停止了该远程对象的连接。(为简便起见,代码作了相应修改)

我们注意到,发布对象时,Remoting并没有方法决定该对象具体发布到哪个通道中。经过我的分析,事实上对对象进行Marshal后,Remoting当前的所有通道都存在该远程对象。这也许就是问题症结。

即使如此,仍然让我困惑的是,当我调用Tcp通道的远程对象后,为什么注销/注册Http通道会出现异常,而Tcp通道则不存在问题呢?因为我在注销Http通道时,既停止了Http通道内的服务对象,又停止了对通道的监听,然后再注销了该通道。即使调用Tcp通道的远程对象后,这个对象会驻留在Http通道中,经过我的一系列操作,Http通道的端口应该已经被释放了啊,为什么会抛出异常呢?从工作机制上看,两种类型通道的原理是一样的啊!?

疑惑!!!

posted @ 2004-09-06 13:34  张逸  阅读(5710)  评论(6编辑  收藏  举报