0

我正在开发客户端服务器应用程序、Windows Server 和 Linux 客户端。我正在使用多个并发客户端测试我的服务器。我只尝试了来自客户端的 20 个并发连接,我注意到尽管所有 20 个请求都是相同的,但有些请求没有得到处理。他们进入队列,由于某种原因,当轮到他们时,客户端被关闭(客户端连接超时为 5 秒)。

然后我添加了一个 Thread.Sleep(1000),以检查它是否真的是异步的,但后来我意识到它在超时之前不会处理其他请求。尽管事实上

  1. 它是异步的
  2. ManualResetEvent 是在睡觉前设置的。

现在我想知道我在这里错过了什么,因为这主要发生在并发连接上?

public static void StartServer(IPAddress ipAddr, int port)
{
    //IPEndPoint serverEndPoint = new IPEndPoint(ipAddr, port);
    IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Any, port);
    Socket clientListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    try
    {
        clientListener.Bind(serverEndPoint);
        clientListener.Listen(500);
        Console.WriteLine("-- Server Listening: {0}:{1}",ipAddr,port);
        while (true)
        {
            resetEvent.Reset();
            Console.WriteLine("|| Waiting for connection");
            clientListener.BeginAccept(new AsyncCallback(AcceptConnection), clientListener);
            resetEvent.WaitOne();
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}


public static void AcceptConnection(IAsyncResult ar)
{
    // Get the socket that handles the client request.
    Socket listener = (Socket)ar.AsyncState;
    Socket handler = listener.EndAccept(ar);
    // Signal the main thread to continue.
    resetEvent.Set();
    // Create the state object.
    JSStateObject state = new JSStateObject();
    state.workSocket = handler;
    if (handler.Connected)
    {
        Console.WriteLine("** Connected to: {0}", handler.RemoteEndPoint.ToString());
        state.workingDirectory = JSUtilityClass.CreatetTemporaryDirectry();
        try
        {
            Thread.Sleep(1000);
            Receive(state);
        }
        catch (Exception e)
        {
            handler.Shutdown(SocketShutdown.Both);
            handler.Close();
            Console.WriteLine(e.Message);
        }
    }
}
4

1 回答 1

5

我创建了一个发送 100 次连接尝试的测试,并发现一些事情会减慢它的速度。

为什么这么慢?

我在 AcceptConnection 中放了一个断点来查看调用栈,就是这样

ConsoleApplication1.exe!ConsoleApplication1.Program.AcceptConnection(System.IAsyncResult ar) Line 62    C#
        System.dll!System.Net.LazyAsyncResult.Complete(System.IntPtr userToken) + 0x69 bytes    
        System.dll!System.Net.ContextAwareResult.CaptureOrComplete(ref System.Threading.ExecutionContext cachedContext, bool returnContext) + 0xab bytes    
        System.dll!System.Net.ContextAwareResult.FinishPostingAsyncOp(ref System.Net.CallbackClosure closure) + 0x3c bytes  
        System.dll!System.Net.Sockets.Socket.BeginAccept(System.AsyncCallback callback, object state) + 0xe3 bytes  
        ConsoleApplication1.exe!ConsoleApplication1.Program.StartServer(System.Net.IPAddress ipAddr, int port) Line 48 + 0x32 bytes C#

所以回调AcceptConnection是从调用 BeginAccept 的同一线程运行的。我查看FinishPostingAsyncOp了反射器,它使用异步模式,如果队列中已经有一个套接字操作等待处理,它将在当前线程上执行,否则如果没有任何待处理,它'稍后将在不同的线程中处理,例如

SocketAsyncEventArgs sae = new SocketAsyncEventArgs();
sae.Completed += new EventHandler<SocketAsyncEventArgs>(SocketOperation_Completed);
if (!clientListener.AcceptAsync(sae))
    AcceptConnection(clientListener, sae); // operation completed synchronously, process the result
else
    // operation will complete on a IO completion port (different thread) which we'll handle in the Completed event

因此,正如您观察到的那样,程序在这种情况下实际上是完全同步的,并且使用 1 秒 Thread.Sleep 至少需要 100 秒才能接受所有连接,到那时大多数连接都会超时。

解决方案

即使 BeginAccept 方法摘要说

开始异步操作以接受传入的连接尝试。

事实证明还有更多的故事

来自 MSDN http://msdn.microsoft.com/en-AU/library/system.net.sockets.socket.beginaccept.aspx

BeginAccept(Int32, AsyncCallback, Object) 开始异步操作以接受传入的连接尝试并接收客户端应用程序发送的第一个数据块。

因此,它在触发回调之前执行具有短暂超时的读取操作。您可以通过指定receiveSize0 来禁用此功能。 更改

clientListener.BeginAccept(new AsyncCallback(AcceptConnection), clientListener);

clientListener.BeginAccept(0, new AsyncCallback(AcceptConnection), clientListener);

这加快了速度,如果我们从中删除Thread.Sleep(1000)AcceptConnection那么所有连接都会很快被接受。

如果您将其Thread.Sleep(1000)留在那里以模拟工作负载或仅用于测试,那么您可能需要准备服务器来处理此类负载

int minWorkerThreads = 0;
int minCompletionPortThreads = 0;
ThreadPool.GetMinThreads(out minWorkerThreads, out minCompletionPortThreads);
ThreadPool.SetMinThreads(minWorkerThreads, 100);

其中 100 是您希望随时可用以处理套接字操作的线程数量。

只是另一件事,这是个人喜好问题,但只是为了让您知道您可能想BeginAccept从 AcceptConnection 内部调用,这消除了对 while 循环的需要。即改变这个

while (true)
{
    resetEvent.Reset();
    Console.WriteLine("|| Waiting for connection");
    clientListener.BeginAccept(new AsyncCallback(AcceptConnection), clientListener);
    resetEvent.WaitOne();
}

对此

Console.WriteLine("|| Waiting for connection");
clientListener.BeginAccept(new AsyncCallback(AcceptConnection), clientListener);

BeginAccept并放入另一个AcceptConnection

public static void AcceptConnection(IAsyncResult ar)
{
    // Get the socket that handles the client request.
    Socket listener = (Socket)ar.AsyncState;
    // start another listening operation
    listener.BeginAccept(new AsyncCallback(AcceptConnection), listener);
    ... the rest of the method
}
于 2013-05-03T06:11:52.420 回答