TcpListener正在排队连接,比我清除它们的速度更快。

9
据我所知,一旦调用Start()TcpListener将排队连接。每次调用AcceptTcpClient(或BeginAcceptTcpClient)时,它将从队列中出列一个项目。
如果我们通过一次发送1,000个连接来对我们的TcpListener应用程序进行负载测试,则队列的构建速度远远快于我们清除它的速度,最终导致客户端超时,因为它没有得到响应,因为它的连接仍然在队列中。但是,服务器似乎没有太大的压力,我们的应用程序没有消耗太多的CPU时间,机器上的其他监视资源也没有流汗。感觉我们现在运行效率不高。
我们调用BeginAcceptTcpListener,然后立即交给ThreadPool线程实际完成工作,然后再次调用BeginAcceptTcpClient。涉及的工作似乎不会给机器带来任何压力,基本上只是3秒的睡眠,然后是字典查找,然后是100字节写入TcpClient的流。
这是我们正在使用的TcpListener代码:
    // Thread signal.
    private static ManualResetEvent tcpClientConnected = new ManualResetEvent(false);

    public void DoBeginAcceptTcpClient(TcpListener listener)
    {
        // Set the event to nonsignaled state.
        tcpClientConnected.Reset();

        listener.BeginAcceptTcpClient(
            new AsyncCallback(DoAcceptTcpClientCallback),
            listener);

        // Wait for signal
        tcpClientConnected.WaitOne();
    }

    public void DoAcceptTcpClientCallback(IAsyncResult ar)
    {
        // Get the listener that handles the client request, and the TcpClient
        TcpListener listener = (TcpListener)ar.AsyncState;
        TcpClient client = listener.EndAcceptTcpClient(ar);

        if (inProduction)
            ThreadPool.QueueUserWorkItem(state => HandleTcpRequest(client, serverCertificate));  // With SSL
        else
            ThreadPool.QueueUserWorkItem(state => HandleTcpRequest(client));  // Without SSL

        // Signal the calling thread to continue.
        tcpClientConnected.Set();
    }

    public void Start()
    {
        currentHandledRequests = 0;
        tcpListener = new TcpListener(IPAddress.Any, 10000);
        try
        {
            tcpListener.Start();

            while (true)
                DoBeginAcceptTcpClient(tcpListener);
        }
        catch (SocketException)
        {
            // The TcpListener is shutting down, exit gracefully
            CheckBuffer();
            return;
        }
    }

我认为答案与使用Sockets而不是TcpListener有关,或者至少要使用TcpListener.AcceptSocket,但我想知道如何做到这一点?
我们提出的一个想法是调用AcceptTcpClient并立即将TcpClient Enqueue到多个Queue<TcpClient>对象之一中。这样,我们可以在单独的线程上轮询这些队列(每个线程一个队列),而不会遇到可能在等待其他Dequeue操作时阻止线程的监视器。然后,每个队列线程都可以使用ThreadPool.QueueUserWorkItemThreadPool线程中完成工作,然后继续出队其队列中的下一个TcpClient。您是否推荐此方法,或者我们的问题在于我们正在使用TcpListener,无论快速出队多少次也无法解决?

1
我认为你可能有一个错误的假设。被丢弃的传入连接并没有从队列中被丢弃。它们从未进入队列,因为队列已经满了。您可以选择指定更大的队列长度。这将在一定程度上缓解问题。真正的问题是,以如此快的速度向服务器发送连接并不是非常现实的。一个能够处理1k客户端的服务器通常仍需要以适度的速度接收连接。 - Kennet Belenky
尝试使用更大的“backlog”值:http://msdn.microsoft.com/zh-cn/library/5kh8wf6s(v=VS.100).aspx - Kennet Belenky
MSDN表示,如果超过队列大小,TcpListener将抛出SocketException异常。我还没有看到任何SocketExceptions(CheckBuffer方法记录),所以我假设我还没有达到其大小限制。但是关于1k连接的观点很好。 - Matt Brindley
只是为了澄清,这些SocketExceptions是在客户端抛出的,而不是服务器端。 - Kennet Belenky
5个回答

3

我写了一些直接使用套接字的代码,但是我没有办法对1000个客户端进行负载测试。您能否尝试测试一下这段代码与您当前的解决方案相比如何?我非常感兴趣测试结果,因为我正在构建一个需要接受很多连接的服务器。

static WaitCallback handleTcpRequest = new WaitCallback(HandleTcpRequest);

static void Main()
{
    var e = new SocketAsyncEventArgs();
    e.Completed += new EventHandler<SocketAsyncEventArgs>(e_Completed);

    var socket = new Socket(
        AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    socket.Bind(new IPEndPoint(IPAddress.Loopback, 8181));
    socket.Listen((int)SocketOptionName.MaxConnections);
    socket.AcceptAsync(e);

    Console.WriteLine("--ready--");
    Console.ReadLine();
    socket.Close();
}

static void e_Completed(object sender, SocketAsyncEventArgs e)
{
    var socket = (Socket)sender;
    ThreadPool.QueueUserWorkItem(handleTcpRequest, e.AcceptSocket);
    e.AcceptSocket = null;
    socket.AcceptAsync(e);
}

static void HandleTcpRequest(object state)
{
    var socket = (Socket)state;
    Thread.Sleep(100); // do work
    socket.Close();
}

+1 这更符合我的做法。简而言之,不要依赖主线程来启动每个异步接受操作。第一次从主线程开始异步接受,然后从回调方法本身开始每个后续的异步接受。 - Matt Davis

2

在其他问题中已经提到过,但我建议在tcpListener.Start()方法中使用允许您将backlog设置为比您预期的一次最大连接数更高的重载:


    public void Start()
    {
        currentHandledRequests = 0;
        tcpListener = new TcpListener(IPAddress.Any, 10000);
        try
        {
            tcpListener.Start(1100);  // This is the backlog parameter

            while (true)
                DoBeginAcceptTcpClient(tcpListener);
        }
        catch (SocketException)
        {
            // The TcpListener is shutting down, exit gracefully
            CheckBuffer();
            return;
        }
    }

基本上,此选项设置了允许等待调用Accept的“挂起”TCP连接的数量。如果您无法快速接受连接,并且此后备队列已满,则TCP连接将被自动拒绝,您甚至没有机会处理它们。

如其他人所述,另一个可能性是加快处理传入连接的速度。但即使您可以加快接受时间,仍应将后备队列设置为较高的值。


2
除非我理解错了,你正在调用BeingAcceptTcpClient,它是异步的,但是接着你又调用WaitOne()来等待异步代码完成,这实际上使得该过程变成同步的。你的代码一次只能接受一个客户端。或者我完全疯了?至少,这似乎会导致很多无谓的上下文切换。

以上代码来自 MSDN:http://msdn.microsoft.com/en-us/library/system.net.sockets.tcplistener.beginaccepttcpclient.aspx - 那么您的意思是我应该在 DoAcceptTcpClientCallback 方法的第一行再次调用 Begin 吗?在另一个 Begin 调用之前不需要先调用 End 吗? - Matt Brindley
您可以在调用listener.EndAcceptTcpClient后的DoAcceptTcpClientCallback 结束时调用listener.BeginAcceptTcpClient。ManualResetEvent不是必需的。但是Begin/EndAcceptTcpClient方法存在一些性能问题,这就是为什么将AcceptAsync方法添加到Socket类中的原因。 - dtb
啊,好的,我明白了。我不确定再次调用Begin与使用ManualResetEvent会提高性能多少?我现在已经将其更改为删除ManualResetEvent,并在End之后但ThreadPool之前调用Begin,我会看看它的表现如何。 - Matt Brindley

1
首先要问自己的问题是,“一次性有1000个连接合理吗?”我个人认为这种情况不太可能发生。更有可能的是在短时间内出现了1000个连接。
我有一个TCP测试程序,用于测试我的服务器框架,它可以批量处理X个连接,每批Y个连接,每批之间间隔Z毫秒;我个人认为这比“一次性处理大量连接”更符合实际情况。它是免费的,可能会有所帮助,您可以从这里获取:http://www.lenholgate.com/blog/2005/11/windows-tcpip-server-performance.html 正如其他人所说,增加监听队列,更快地处理连接,如果可能的话使用异步接受...

我正在使用Seige(请Google它)软件来模拟压力测试,但它只支持HTTP协议 :) - hoodoos
你真的期望在生产环境中同时拥有1000个连接吗? - Len Holgate

0
只是一个建议:为什么不同步地接受客户端(使用AcceptTcpClient而不是BeginAcceptTcpClient),然后在新线程上处理客户端?这样,您就不必等待客户端被处理完才能接受下一个客户端。

这个被downvote是因为,正如Thomas所指出的那样,在AcceptTcpClient方面同步处理需要为每个客户端分配1个线程。默认情况下,.NET将自动为每个新线程分配1MB。这不具有良好的可扩展性。 - kmarks2
@kmarks2 - 是的,所以只需将“在新线程上处理客户端”替换为“在线程池中处理客户端”,答案就完美了。我认为这里的重点是TcpListener可以完美地扩展而无需使用异步方法。此外,人们可以停止每次有人尝试使用TcpListener类时都回到Socket类,就像接受的答案一样。这个其他答案对应于问题:https://dev59.com/n2435IYBdhLWcg3wkA9e - Simon Mourier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接