2009-10-08 19 views
7

Chcę utworzyć asynchroniczny serwer gniazda przy użyciu zdarzenia SocketAsyncEventArgs.Projektowanie serwera za pomocą SocketAsyncEventArgs

Serwer powinien zarządzać około 1000 połączeń w tym samym czasie. Jaki jest najlepszy sposób na obsługę logiki dla każdego pakietu?

Projekt serwera jest oparty na this MSDN example, więc każde gniazdo będzie miało własne SocketAsyncEventArgs do odbierania danych.

  1. Czy rzeczy logiki wewnątrz funkcji otrzymać. Nie zostanie utworzony narzut, ale ponieważ następne wywołanie funkcji ReceiveAsync() nie zostanie wykonane przed zakończeniem logiki, nowe dane nie będą mogły zostać odczytane z gniazda. Dwa główne pytania to dla mnie: Jeśli klient wysyła dużo danych i przetwarzanie logiczne jest ciężkie, jak system poradzi sobie z nim (pakiety utracone, ponieważ bufor jest pełny)? Ponadto, jeśli wszyscy klienci wysyłają dane w tym samym czasie, czy będzie ich 1000, czy też istnieje wewnętrzny limit i nowy wątek nie może zostać uruchomiony, zanim inny zakończy wykonywanie?

  2. Użyj kolejki. Funkcja odbierania będzie bardzo krótka i wykonana szybko, ale będziesz mieć przyzwoity narzut z powodu kolejki. Problemem jest to, że jeśli wątki robocze nie są wystarczająco szybkie w przypadku dużego obciążenia serwera, kolejka może być pełna, więc być może trzeba wymusić spadek pakietów. Otrzymujesz również problem Producent/Konsument, który prawdopodobnie spowolni całą kolejkę z wieloma blokadami.

Jaki będzie lepszy projekt, logika w funkcji odbierania, logika w wątkach roboczych lub coś zupełnie innego, co do tej pory przegapiłem.

Kolejne zadanie związane z wysyłaniem danych.

Czy lepiej jest mieć SocketAsyncEventArgs przywiązanego do gniazda (analogowego do zdarzenia odbioru) i użyć systemu buforowego do wykonania jednego połączenia nadawczego dla kilku małych pakietów (powiedzmy, że pakiety czasami bywały czasami! Wysyłaj bezpośrednio po inny) lub użyć innego SocketAsyncEventArgs dla każdego pakietu i przechowywać je w puli, aby je ponownie wykorzystać?

Odpowiedz

10

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

-1

W kodzie, to zrobić:

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

Ale to jest błąd, aby to zrobić. Istnieje powód, dla którego wywołanie zwrotne nie jest wywoływane, gdy kończy się synchronicznie, takie działanie może wypełnić stos.

Wyobraź sobie, że każda funkcja ReceiveAsync zawsze powraca synchronicznie. Jeśli twój HandleIOCompleted był przez jakiś czas, możesz przetworzyć wynik, który powrócił synchronicznie na tym samym poziomie stosu. Jeśli nie powróciło synchronicznie, przerwiesz na chwilę. Ale robiąc to, co robisz, kończysz tworzenie nowego przedmiotu w stosie ... więc jeśli masz pecha na tyle, spowodujesz wyjątki przepełnienia stosu.

Powiązane problemy