Aby skutecznie implementować gniazda asynchroniczne, każde gniazdo będzie wymagało więcej niż 1 SocketAsyncEventArgs. Występuje również problem z buforem bajtowym [] w każdym SocketAsyncEventArgs. W skrócie, bufory bajtowe będą przypinane za każdym razem, gdy dojdzie do zarządzanego natywnego przejścia (wysyłanie/odbieranie). Jeśli w razie potrzeby przydzielisz bufory SocketAsyncEventArgs i byte, możesz uruchomić OutOfMemoryExceptions z wieloma klientami z powodu fragmentacji i niezdolności GC do kompaktowania przypiętej pamięci.
Najlepszym sposobem radzenia sobie z tym problemem jest utworzenie klasy SocketBufferPool, która będzie alokować dużą liczbę bajtów i SocketAsyncEventArgs, gdy aplikacja zostanie uruchomiona po raz pierwszy, w ten sposób przypięta pamięć będzie ciągła. Następnie w razie potrzeby ponownie użyj buforów z puli.
W praktyce odkryłem, że najlepiej jest utworzyć klasę otoki wokół klas SocketAsyncEventArgs i SocketBufferPool, aby zarządzać dystrybucją zasobów.
Jako przykład, tutaj jest kod na BeginReceive metody:
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);
}
}
A oto metoda 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);
}
Powyższy kod jest zawarty w klasie TcpSocket że podniesie DataReceived & Zdarzenia DataSent. Należy zwrócić uwagę na przypadek SocketAsyncOperation.ReceiveMessageFrom: block; jeśli gniazdo nie ma błędu, natychmiast uruchamia kolejną BeginReceive(), która przydzieli inny SocketEventArgs z puli.
Inną ważną wiadomością jest właściwość SequenceNumber SocketEventArgs ustawiona w metodzie HandleIOComplete. Chociaż żądania asynchroniczne będą się kończyć w kolejce, nadal podlegają innym warunkom wyścigu wątków. Ponieważ kod wywołuje BeginReceive przed podniesieniem zdarzenia DataReceived, istnieje możliwość, że wątek obsługujący orginalną IOCP zostanie zablokowany po wywołaniu BeginReceive, ale przed wywołaniem zdarzenia, podczas gdy drugi asynchroniczny odbiór zakończy się na nowym wątku, który najpierw podnosi zdarzenie DataReceived. Chociaż jest to dość rzadki przypadek, może się zdarzyć, a właściwość SequenceNumber daje aplikacji zużywającej zdolność do zapewnienia, że dane są przetwarzane we właściwej kolejności.
Jeszcze jeden obszar, o którym należy pamiętać to wysyłanie asynchronów. Często asynchroniczne żądania wysyłania kończą się synchronicznie (SendAsync zwróci wartość false, jeśli połączenie zostanie wykonane synchronicznie) i może znacznie obniżyć wydajność. Dodatkowy narzut asynchronicznego połączenia powracającego do IOCP może w praktyce spowodować gorszą wydajność niż po prostu użycie połączenia synchronicznego. Wywołanie asynchroniczne wymaga dwóch wywołań jądra i alokacji sterty, podczas gdy wywołanie synchroniczne dzieje się na stosie.
Nadzieja to pomaga, Bill