Для эффективной реализации асинхронных сокетов каждому сокету потребуется более 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, на практике могут привести к ухудшению производительности, чем простое использование синхронного вызова. Асинхронный вызов требует двух вызовов ядра и выделения кучи, в то время как синхронный вызов происходит в стеке.
Надеюсь, это поможет,
Билл