在使用Remoting之前一直很关心它的并发处理能力。之前写了一个小测试,两个Client线程访问远程对象,一个应该是立即返回,另外一个应该是线程休眠3秒钟后返回。结果发现两个线程的远程调用都是在3秒后返回。
今天在翻阅MSDN中关于Remoting的章节时,发现有以下一些记载:
[使用 .NET Framework 编程 -> .NET 远程处理概述 -> 信道 -> 信道规则]
远程对象共享信道;远程对象不拥有信道。
由于每个客户端连接都是在它自己的线程中处理的,因此单个信道可以同时服务于多个客户端。
照这段话的描述来看,服务端为每一个客户端连接都分配一个独立线程进行处理,那么,Remoting服务端应该是支持并发处理的,不太可能因为线程阻塞造成两个线程都是3秒以后返回。
于是今天配合SockMon 2005,把Remoting调用中的端口和网络使用情况进行了跟踪,发现我是被最初的那个测试误导了。通过跟踪,也了解了一些Remoting的具体实现细节,我以前困惑的一些问题,比如网络连接何时创建,何时销毁,等等问题,也都有了答案。
首先我们看一下我的那个测试程序:
这是远程对象,在i为奇数的时候,线程休眠3秒后返回,否则立即返回。
public string Call(int i) {
long t1 = DateTime.Now.Ticks;
if ( i % 2 == 1 )
System.Threading.Thread.Sleep( 3000 );
long t2 = DateTime.Now.Ticks;
return string.Format( "Hello [I={0},T={1}]", i, t2 - t1 );
}
}
客户端,执行 (new ClientTest ()).Run():
public void Run() {
ChannelServices.RegisterChannel( new TcpClientChannel() );
for ( int i = 0; i < 2; i++ )
{
Thread t = new Thread( new ThreadStart( Test ) );
t.Start();
}
}
int i = 0;
private void Test() {
int t = i++;
TestClass tc = (TestClass) Activator.GetObject(
typeof( TestClass ), "tcp://localhost:8086/Test" );
Console.WriteLine( tc.Call( t ) );
}
}
运行这个测试程序,发现有3种情况:
1、正常运行后两个线程几乎都是在3秒以后,返回:
Hello [I=0, T=0] Hello [I=1, T=30000384]
2、用SockMon监视网络的时候,再运行测试程序,
I=0的消息马上返回,I=1的是3秒以后返回。这个结果应该是正常的。
3、用RemotingConfiguration.Configure( "Client.exe.config" ); 替换掉
ChannelServices.RegisterChannel( new TcpClientChannel() );
也就是说用配置文件注册通道的时候,
不管SockMon有没有开启,测试程序的结果也都是正常的,同第2种情况。
这第2和第3两种情况着实让人郁闷,出现测试结果全然不同的原因是什么?服务端到底是怎样处理的?让我们先来看看跟踪数据:
No. | 对象 | 线程 | Local Port |
Remote Port |
操作 | 备注 |
1 | Server | 4584 | 8086 | 绑定端口并开始监听 | 发生在信道注册 ChannelServices .RegisterChannel | |
2 | Client | 5132 | 2296 | 8086 | 连接并发送请求tcp://localhost:8086/Test | 发生在远程方法调用 tc.Call( t ),t = 0 |
3 | Client | 4276 | 2297 | 8086 | 连接并发送请求tcp://localhost:8086/Test | 发生在远程方法调用 tc.Call( t ),t = 1 |
4 | Server | 780 | 8086 | 2096 | 返回信息 Hello [I=0, T=0] | |
5 | Client | 5132 | 2296 | 8086 | 接受到 Hello [I=0, T=0] | |
6 | Server | 780 | 8086 | 2096 | 返回信息 Hello [I=1, T=30000384] |
|
7 | Client | 4276 | 2297 | 8086 | 接受到 Hello [I=1, T=30000384] |
我们来大致回顾一下这个流程。首先Server端在信道注册(ChannelServices.RegisterChannel)以后,就会立即在指定端口开始监听。然后Client开始创建第一个TestClass。并调用它的远程方法Call()。这时,Client选取一个新的端口(2296)与服务器进行通信请求。然后我们看到第二个线程(ID:4276)里又创建一个TestClass,并在新端口(2297)上连接服务器。
到这一步为止,我们可以看到非常重要的一个事实,就是执行 Activator.GetObject() 时,实际上并没有访问网络。
只有在实际调用远程方法的时候,Remoting才实际开始通过网络通信向服务器请求处理。对于在服务器端激活的SAO对象来说,也正是在这个时候创建远程对象(注:Singleton在全局范围内只创建一个对象实例,SingleCall则为每个客户端创建一个对象实例,所以实际上这个时候并不一定全都是创建新的对象实例)
其次,Client在不同的线程当中,使用了不同的端口与服务器通信。也就是说开启了两个网络连接。而在测试代码中,不管是通过配置文件,还是通过代码注册信道,我们实际上都只注册了一个TcpClientChannel。在这一点上还是有些疑问... 另外,对于客户端信道来说,似乎无法指定其端口。每次系统都无视我指定的端口,而自行开启别的端口进行通信 -_-
好了,我们从No.4继续。可以看到Server分别处理了Call(0), Call(1)两个调用。而且I=0的时候,中间没有进行线程休眠,所以处理耗时基本为0。但是我注意到,Server在实际处理远程调用时,是用了另外一个线程(ID:780)。也就是说,在Server端,4584线程负责监听,而780线程则另外开辟用来处理客户请求。而且,对于Client在2296和2297两个端口上的请求,Server端用的是同一个线程进行处理!
等等,有没有可能是这样一种情况:
/-----线程A
SERVER(4584线程):监听 ____/----780线程:负责联络ClientA -------\-----线程B
\----XXX线程:负责联络ClientX------/----线程C
\----线程D
也就是说主监听线程在接受到不同客户端发来的请求时,开辟新的通信线程与客户进行沟通。而每个通信线程只负责网络层的传输,仍不负责实际的远程处理,而是再开辟新的多线程分配任务的处理呢?似乎有这样的可能,因为这样的好处是实现真正的并发处理。不管客户端是单线程还是多线程,只要有请求过来,都会立即交去处理。
我们把代码稍微改动一下,注释掉 TestClass.Call() 方法里的 // if ( i % 2 == 1 ) ,这样只要调用Call(),结果都会在3秒以后返回。
测试结果,在网络跟踪上与上一次无异。但是两个 Hello 几乎是在3秒以后同时出现。这也就说明了服务端两个Call()方法差不多是同时调用的。但这次客户端的两个请求,在网络层的跟踪看来,仍然是在一个通信线程(780)里进行。这就验证了我在上面的猜想有可能是正确的。因为通过SockMon跟踪到的线程,只是Socket相关的线程,并不能代表所有的线程的使用情况。很可能远程方法是由哪个未知名的线程完成的。
我们再来做今天最后一个测试。同时开两个Client。代码修改如下:
public string Call() {
return string.Format( "Hello " + this.GetHashCode().ToString() );
}
}
class ClientTest {
public void Run() {
ChannelServices.RegisterChannel( new TcpClientChannel(
"Channel" + this.GetHashCode().ToString(),
new BinaryClientFormatterSinkProvider() ) );
TestClass tc = (TestClass) Activator.GetObject(
typeof( TestClass ), "tcp://localhost:8086/Test" );
Console.WriteLine( tc.Call() );
}
}
先开启一个Server,然后执行两个Client。
No. | 对象 | 线程 | LocalPort | RemotePort | 操作 |
1 | Server | 4008 | 8086 | 绑定端口并开始监听 | |
2 | Client1 | 2524 | 1174 | 8086 | 连接并发送请求 |
3 | Server | 2840 | 8086 | 1174 | 返回信息 |
4 | Client1 | 2524 | 1174 | 8086 | 接受信息 |
5 | Client2 | 2500 | 1175 | 8086 | 连接并发送请求 |
6 | Server | 2292 | 8086 | 1175 | 返回信息 |
7 | Client2 | 2524 | 1174 | 8086 | 接受信息 |
我们看到,这回Server一个监听线程(4008),两个Client分别使用不同的线程和端口,这点和之前的情况差不多。但是这次Server针对Client1和Client2终于使用了两个不同的通信线程。那么Remoting怎么区分通信线程呢?我想是根据我们注册的信道。之前我们一个Client两个线程,都是通过同一个 TcpClientChannel。所以Server也相应的用同一个通信线程。而现在每个Client都使用独立的信道,Server端相应的也使用不同的网络连接。
总结来说,Server端有一个主线程在我们创建ServerChannel时所指定的端口上负责监听。当客户端有请求时,Server为每一个ClientChannel分配一个辅助通信线程和一个专门的网络连接。在每一个信道上,可以同时处理多个请求。
顺便提一下,通过网络跟踪还验证了这句话:
MSDN:TcpChannel 打开并缓存与当时正在向另一个服务器发出请求的线程一样多的连接。客户端上的套接字连接将在处于不活动状态 15-20 秒钟之后关闭。
No. | 对象 | 线程 | LocalPort | RemotePort | 操作 |
8 | Client1 | 2524 | 1174 | 8086 | 关闭Socket,结束线程 |
9 | Server | 2840 | 8086 | 1174 | 关闭Socket,结束线程 |
10 | Client2 | 2500 | 1175 | 8086 | 关闭Socket,结束线程 |
11 | Server | 2292 | 8086 | 1175 | 关闭Socket,结束线程 |
在15秒未进行操作后,两个Channel在服务器和客户端的通信线程均被结束。