Проектирование сервера с использованием SocketAsyncEventArgs - PullRequest
7 голосов
/ 08 октября 2009

Я хочу создать асинхронный сервер сокетов, используя событие SocketAsyncEventArgs.

Сервер должен управлять около 1000 подключений одновременно. Каков наилучший способ обработки логики для каждого пакета?

Дизайн сервера основан на этом примере MSDN , поэтому каждый сокет будет иметь свой собственный SocketAsyncEventArgs для получения данных.

  1. Выполнить логику внутри функции приема . Не будет создано никаких служебных данных, но поскольку следующий вызов ReceiveAsync () не будет выполнен до завершения логики, новые данные не могут быть прочитаны из сокета. Два основных вопроса для меня: если клиент отправляет много данных и логическая обработка тяжелая, как система справится с этим (пакеты потеряны из-за переполнения буфера)? Кроме того, если все клиенты отправляют данные одновременно, будет ли 1000 потоков или существует внутренний лимит, и новый поток не сможет запуститься, пока другой не завершит выполнение?

  2. Использовать очередь. Функция приема будет очень короткой и будет выполняться быстро, но вы будете иметь приличные издержки из-за очереди. Проблемы состоят в том, что если ваши рабочие потоки недостаточно быстры при большой нагрузке на сервер, ваша очередь может заполниться, поэтому, возможно, вам придется принудительно отбрасывать пакеты. Вы также получаете проблему Producer / Consumer, которая, вероятно, может замедлить всю очередь со многими блокировками.

Итак, что будет лучшим дизайном, логикой в ​​функции приема, логикой в ​​рабочих потоках или чем-то совершенно другим, что я пропустил до сих пор.

Еще один квест по отправке данных.

Лучше ли привязывать SocketAsyncEventArgs к сокету (аналог события приема) и использовать буферную систему, чтобы сделать один вызов отправки для нескольких небольших пакетов (скажем, в противном случае пакеты иногда бывают отправлены напрямую один за другим) ) или использовать разные SocketAsyncEventArgs для каждого пакета и сохранять их в пуле для их повторного использования?

Ответы [ 2 ]

10 голосов
/ 08 октября 2009

Для эффективной реализации асинхронных сокетов каждому сокету потребуется более 1 SocketAsyncEventArgs. Существует также проблема с буфером byte [] в каждом SocketAsyncEventArgs. Короче говоря, байтовые буферы будут закреплены всякий раз, когда происходит управляемый собственный переход (отправка / получение). Если вы выделите SocketAsyncEventArgs и байтовые буферы по мере необходимости, вы можете столкнуться с OutOfMemoryExceptions со многими клиентами из-за фрагментации и неспособности GC сжать закрепленную память.

Лучший способ справиться с этим - создать класс SocketBufferPool, который будет выделять большое количество байтов и SocketAsyncEventArgs при первом запуске приложения, таким образом закрепленная память будет непрерывной. Затем просто используйте буферы из пула по мере необходимости.

На практике я обнаружил, что лучше всего создать класс-оболочку вокруг SocketAsyncEventArgs и класса SocketBufferPool для управления распределением ресурсов.

В качестве примера, вот код для метода BeginReceive:

private void BeginReceive(Socket socket)
    {
        Contract.Requires(socket != null, "socket");

        SocketEventArgs e = SocketBufferPool.Instance.Alloc();
        e.Socket = socket;
        e.Completed += new EventHandler<SocketEventArgs>(this.HandleIOCompleted);

        if (!socket.ReceiveAsync(e.AsyncEventArgs)) {
            this.HandleIOCompleted(null, e);
        }
    }

А вот метод HandleIOCompleted:

private void HandleIOCompleted(object sender, SocketEventArgs e)
    {
        e.Completed -= this.HandleIOCompleted;
        bool closed = false;

        lock (this.sequenceLock) {
            e.SequenceNumber = this.sequenceNumber++;
        }

        switch (e.LastOperation) {
            case SocketAsyncOperation.Send:
            case SocketAsyncOperation.SendPackets:
            case SocketAsyncOperation.SendTo:
                if (e.SocketError == SocketError.Success) {
                    this.OnDataSent(e);
                }
                break;
            case SocketAsyncOperation.Receive:
            case SocketAsyncOperation.ReceiveFrom:
            case SocketAsyncOperation.ReceiveMessageFrom:
                if ((e.BytesTransferred > 0) && (e.SocketError == SocketError.Success)) {
                    this.BeginReceive(e.Socket);
                    if (this.ReceiveTimeout > 0) {
                        this.SetReceiveTimeout(e.Socket);
                    }
                } else {
                    closed = true;
                }

                if (e.SocketError == SocketError.Success) {
                    this.OnDataReceived(e);
                }
                break;
            case SocketAsyncOperation.Disconnect:
                closed = true;
                break;
            case SocketAsyncOperation.Accept:
            case SocketAsyncOperation.Connect:
            case SocketAsyncOperation.None:
                break;
        }

        if (closed) {
            this.HandleSocketClosed(e.Socket);
        }

        SocketBufferPool.Instance.Free(e);
    }

Приведенный выше код содержится в классе TcpSocket, который будет вызывать события DataReceived & DataSent. Следует обратить внимание на случай SocketAsyncOperation.ReceiveMessageFrom: block; если у сокета не было ошибки, он немедленно запускает другой BeginReceive (), который выделит другой SocketEventArgs из пула.

Другим важным примечанием является свойство SocketEventArgs SequenceNumber, установленное в методе HandleIOComplete. Хотя асинхронные запросы будут выполняться в порядке очереди, вы по-прежнему подвержены другим условиям гонки потоков. Поскольку код вызывает BeginReceive до возбуждения события DataReceived, существует вероятность, что поток, обслуживающий первоначальный IOCP, заблокируется после вызова BeginReceive, но до того, как обработать событие, пока второй асинхронный прием завершится, в новом потоке, который сначала вызывает событие DataReceived. Хотя это довольно редкий крайний случай, он может возникнуть, и свойство SequenceNumber дает приложению-потребителю возможность гарантировать, что данные обрабатываются в правильном порядке.

Еще одна область, о которой следует знать, - это асинхронные отправки. Часто запросы асинхронной отправки завершаются синхронно (SendAsync возвращает false, если вызов завершен синхронно) и может серьезно снизить производительность. Дополнительные издержки асинхронного вызова, возвращаемого на IOCP, на практике могут привести к ухудшению производительности, чем простое использование синхронного вызова. Асинхронный вызов требует двух вызовов ядра и выделения кучи, в то время как синхронный вызов происходит в стеке.

Надеюсь, это поможет, Билл

0 голосов
/ 22 марта 2013

В своем коде вы делаете это:

if (!socket.ReceiveAsync(e.AsyncEventArgs)) {
  this.HandleIOCompleted(null, e);
}

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

Представьте, что каждый ReceiveAsync всегда возвращается синхронно. Если ваш HandleIOCompleted был через некоторое время, вы можете обработать результат, который возвращался синхронно на том же уровне стека. Если он не вернулся синхронно, вы сломаете время. Но, выполняя то, что вы делаете, вы в конечном итоге создаете новый элемент в стеке ... поэтому, если вам не повезет, вы вызовете исключения переполнения стека.

...