TcpListener ставит в очередь соединения быстрее, чем я могу их очистить - PullRequest
9 голосов
/ 30 апреля 2010

Насколько я понимаю, TcpListener будет ставить в очередь соединения, как только вы позвоните Start(). Каждый раз, когда вы звоните AcceptTcpClient (или BeginAcceptTcpClient), он удаляет один элемент из очереди.

Если мы нагрузочно протестируем наше приложение TcpListener, отправив к нему 1000 подключений за один раз, очередь будет построена гораздо быстрее, чем мы можем очистить, что приведет (в конечном итоге) к тайм-аутам от клиента, поскольку оно не получило ответ, его связь все еще была в очереди. Однако сервер, кажется, не находится под большим давлением, наше приложение не потребляет много процессорного времени, а другие отслеживаемые ресурсы на машине не потеют. Такое ощущение, что сейчас мы работаем недостаточно эффективно.

Мы вызываем 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 и сразу Enqueue TcpClient в один из множества Queue<TcpClient> объектов. Таким образом, мы можем опрашивать эти очереди в отдельных потоках (по одной очереди на поток), не сталкиваясь с мониторами, которые могут блокировать поток во время ожидания других операций Dequeue. Каждый поток очереди может затем использовать ThreadPool.QueueUserWorkItem, чтобы выполнить работу в потоке ThreadPool, а затем перейти к снятию с очереди следующего TcpClient в своей очереди. Вы бы порекомендовали этот подход, или наша проблема в том, что мы используем TcpListener, и быстрое снятие очереди не поможет это исправить?

Ответы [ 5 ]

3 голосов
/ 30 апреля 2010

Я собрал некоторый код, который использует сокеты напрямую, но мне не хватает средств для выполнения нагрузочного теста с 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();
}
2 голосов
/ 01 мая 2010

В других вопросах было упомянуто, но я хотел бы предложить в вашем методе tcpListener.Start () использовать перегрузку, которая позволяет установить в бэклоге число, превышающее максимальное число ожидаемых соединений в одно время:


    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;
        }
    }

По сути, эта опция устанавливает, сколько разрешенных «ожидающих» TCP-соединений ожидают вызова Accept. Если вы не принимаете соединения достаточно быстро, и это отставание заполняется, соединения TCP будут автоматически отклонены, и у вас даже не будет возможности обработать их.

Как уже упоминалось, другой возможностью является ускорение обработки входящих соединений. Тем не менее, вам все равно следует установить более высокое значение для журнала невыполненных работ, даже если вы можете ускорить время принятия.

2 голосов
/ 30 апреля 2010

Если я что-то упускаю, вы вызываете BeingAcceptTcpClient, который является асинхронным, но затем вы вызываете WaitOne (), чтобы дождаться завершения асинхронного кода, что эффективно делает процесс синхронным. Ваш код может принимать только одного клиента за раз. Или я совсем сумасшедший? По крайней мере, это похоже на большое переключение контекста даром.

1 голос
/ 30 апреля 2010

Просто предложение: почему бы не принять клиентов синхронно (используя AcceptTcpClient вместо BeginAcceptTcpClient), а затем обработать клиента в новом потоке? Таким образом, вам не придется ждать обработки клиента, прежде чем вы сможете принять следующий.

1 голос
/ 30 апреля 2010

Первое, о чем вы должны спросить себя, - это «1000 соединений одновременно разумно». Лично я думаю, что вы вряд ли попадете в такую ​​ситуацию. Скорее всего, у вас 1000 подключений за короткий промежуток времени.

У меня есть тестовая программа TCP, которую я использую для тестирования моей серверной инфраструктуры, она может делать что-то вроде X соединений в общей сложности в пакетах Y с промежутком Z мс между каждым пакетом; я лично считаю, что это более реальный мир, чем «огромное количество сразу». Это бесплатно, это может помочь, вы можете получить его здесь: http://www.lenholgate.com/blog/2005/11/windows-tcpip-server-performance.html

Как уже говорили другие, увеличивайте количество прослушиваний, обрабатывайте соединения быстрее, по возможности используйте асинхронное принятие ...

...