深入讨论.NET Socket的Accept方法
考虑一个问题,假如同时有50个连接请求进入一个服务器(这种情况对于普通负载的Web服务器都是很常见的)会怎么样?阻塞式I/O只能循环调用Accept,一个一个对50个连接进行Accept操作,而选择模型也是一样。异步模型呢?假如我们预先发起了100个BeginAccept操作,异步模型能够同时处理50个连接么?MSDN没有回答这个问题,我们只有向.NET framework的代码来寻求解答了。
打开Reflector工具,找到Socket类型的BeginAccept方法,这个方法有三种overload,我们首先来看比较简单的一种(由于是用Reflector工具反编译的,代码的局部变量名无意义):
public IAsyncResult BeginAccept(AsyncCallback callback, object state)
{
if (this.CanUseAcceptEx)
{
return this.BeginAccept(0, callback, state);
}
if (Logging.On)
{
Logging.Enter(Logging.Sockets, this, "BeginAccept", "");
}
if (this.CleanedUp)
{
throw new ObjectDisposedException(base.GetType().FullName);
}
AcceptAsyncResult result1 = new AcceptAsyncResult(this, state, callback);
result1.StartPostingAsyncOp(false);
this.DoBeginAccept(result1);
result1.FinishPostingAsyncOp(ref this.Caches.AcceptClosureCache);
if (Logging.On)
{
Logging.Exit(Logging.Sockets, this, "BeginAccept", result1);
}
return result1;
}
函数的前三行首先判断,是否可以直接使用AcceptEx,如果可以则调用BeginAccept的第三种overload方式。这里要说明两点,1.为什么要用AcceptEx;2.为什么不总是用AcceptEx而是要有条件的使用。第一个问题是因为在Winsock2种,AcceptEx的速度快过accept方法(MSDN是这么说的:A program can make a connection to a socket more quickly using AcceptEx instead of the accept function.)。第二个问题是因为AcceptEx是Winsock2的扩展函数,Win98和以下的版本都不支持这个函数。
放下直接调用第三种overload方式的函数不提,我们先继续往下看。接下来的三行代码是做log的,再接下来的三行代码是检查Socket对象是否已经被Disposed了。
接下来的两行代码首先构造一个AsyncResult的实例,然后设定AsyncResult的StartPostingAsyncOp(开始投递异步操作)属性为false。终于,我们到了最关键的地方执行实际的DoBeginAccept操作。用Reflector打开这个方法,我们看到这个方法最关键的思路是把Socket设置到非阻塞模式,然后将应用程序投递的Accept请求放入Accept队列中,为了避免阻塞,.NET framework使用WSAEventSelect和线程池共同配合,一旦有新连接进入,WSAEventSelect会触发Socket.AcceptCallback方法的执行,逐个处理AcceptQueue中的Accept请求。因为此时Socket处于非阻塞模式,无论是否有未决的连接,Accept都会会立即返回,在没有未决连接的情况下,Accept会返回一个WouldBlock的错误代码。如果出现了这样的错误,就继续等待下一次的WSAEventSelect事件触发。讨论了这么长,大家应该对public IAsyncResult BeginAccept(AsyncCallback callback, object state)的原理清楚了吧,这种overload方式依然未能解决逐个调用Accept来处理未决连接的问题,虽然比阻塞式和选择式I/O有所改善,但是在不能调用AcceptEx的情况下,性能依然会有所损失。那么让我们继续来看看另外两种BeginAccept的情况吧。
public IAsyncResult BeginAccept(int receiveSize, AsyncCallback callback, object state)
{
return this.BeginAccept(null, receiveSize, callback, state);
}
这个overload形式直接调用了第三种,我们也直接来分析第三种BeginAccept。
public IAsyncResult BeginAccept(Socket acceptSocket,
int receiveSize,
AsyncCallback callback,
object state)
{
if (Logging.On)
{
Logging.Enter(Logging.Sockets, this, "BeginAccept", "");
}
if (this.CleanedUp)
{
throw new ObjectDisposedException(base.GetType().FullName);
}
if (receiveSize < 0)
{
throw new ArgumentOutOfRangeException("size");
}
AcceptOverlappedAsyncResult result1 =
new AcceptOverlappedAsyncResult(this, state, callback);
result1.StartPostingAsyncOp(false);
this.DoBeginAccept(acceptSocket, receiveSize, result1);
result1.FinishPostingAsyncOp(ref this.Caches.AcceptClosureCache);
if (Logging.On)
{
Logging.Exit(Logging.Sockets, this, "BeginAccept", result1);
}
return result1;
}
前面的几行代码依旧是log、检查Socket状态和参数有效性,我们直接来分析最核心的DoBeginAccept(acceptSocket, receiveSize, result1)方法。使用Reflector查看这个DoBeginAccept,它比较简单我把代码贴出来,
AcceptOverlappedAsyncResult asyncResult)
2 {
3 int num2;
4 if (!ComNetOS.IsWinNt)
5 {
6 throw new PlatformNotSupportedException(SR.GetString("WinNTRequired"));
7 }
8 if (this.m_RightEndPoint == null)
9 {
10 throw new InvalidOperationException(SR.GetString("net_sockets_mustbind"));
11 }
12 if (!this.isListening)
13 {
14 throw new InvalidOperationException(SR.GetString("net_sockets_mustlisten"));
15 }
16 if (acceptSocket == null)
17 {
18 acceptSocket = new Socket(this.addressFamily, this.socketType, this.protocolType);
19 }
20 else if (acceptSocket.m_RightEndPoint != null)
21 {
22 throw new InvalidOperationException(SR.GetString("net_sockets_namedmustnotbebound",
new object[] { "acceptSocket" }));
23 }
24 asyncResult.AcceptSocket = acceptSocket;
25 int num1 = this.m_RightEndPoint.Serialize().Size + 0x10;
26 byte[] buffer1 = new byte[receiveSize + (num1 * 2)];
27 asyncResult.SetUnmanagedStructures(buffer1, num1);
28 SocketError error1 = SocketError.Success;
29 if (!UnsafeNclNativeMethods.OSSOCK.AcceptEx(this.m_Handle,
acceptSocket.m_Handle,
Marshal.UnsafeAddrOfPinnedArrayElement(asyncResult.Buffer, 0),
receiveSize,
num1, num1, out num2, asyncResult.OverlappedHandle))
30 {
31 error1 = (SocketError) Marshal.GetLastWin32Error();
32 }
33 error1 = asyncResult.CheckAsyncCallOverlappedResult(error1);
34 if (error1 != SocketError.Success)
35 {
36 SocketException exception1 = new SocketException(error1);
37 this.UpdateStatusAfterSocketError(exception1);
38 if (Logging.On)
39 {
40 Logging.Exception(Logging.Sockets, this, "BeginAccept", exception1);
41 }
42 throw exception1;
43 }
44 }
45
46
47
前面是一堆参数有效性检查、对象状态检查、操作系统环境有效性检查的代码。接着初始化了接收数据缓冲区,调用AcceptEx方法,处理错误,直截了当。也就是说,对于这个overload方式,BeginAccept不是自己逐个处理未决连接,而是交给了操作系统内核来完成,会有更好的效率。并且,这个方法的第一个参数,使我们有可能重用Socket对象,由于创建Socket对象是一个相对比较耗时的操作,所以在需要处理大量连接的服务器程序中,能够重用Socket对象更加降低了系统消耗。