2011-08-18 12 views
5

Mam infrastrukturę klient/serwer. Obecnie używają TcpClient i TcpListener do wysyłania danych odbioru między wszystkimi klientami i serwerem.Najlepszy sposób na akceptację wielu klientów tcp?

To, co obecnie robię, kiedy dane są odbierane (w jego własnym wątku), jest umieszczane w kolejce dla innego wątku do przetworzenia w celu uwolnienia gniazda, aby było gotowe i otwarte na otrzymywanie nowych danych.

   // Enter the listening loop. 
       while (true) 
       { 
        Debug.WriteLine("Waiting for a connection... "); 

        // Perform a blocking call to accept requests. 
        using (client = server.AcceptTcpClient()) 
        { 
         data = new List<byte>(); 

         // Get a stream object for reading and writing 
         using (NetworkStream stream = client.GetStream()) 
         { 
          // Loop to receive all the data sent by the client. 
          int length; 

          while ((length = stream.Read(bytes, 0, bytes.Length)) != 0) 
          { 
           var copy = new byte[length]; 
           Array.Copy(bytes, 0, copy, 0, length); 
           data.AddRange(copy); 
          } 
         } 
        } 

        receivedQueue.Add(data); 
       } 

Jednak chciałem się dowiedzieć, czy jest lepszy sposób na zrobienie tego. Na przykład, jeśli jest 10 klientów i wszyscy chcą wysyłać dane do serwera w tym samym czasie, jeden przeżyje, podczas gdy wszystkie inne się nie powiedzie. Lub jeśli jeden klient ma wolne połączenie i wieje gniazdo, wszystkie pozostałe komunikaty zatrzymają się .

Czy istnieje sposób, aby móc odbierać dane od wszystkich klientów w tym samym czasie i dodawać odebrane dane w kolejce do przetwarzania po zakończeniu pobierania?

+0

Bezwstydna wtyczka: http://jonathan.dickinsons.co.za/blog/2011/02/net-sockets-and-you/ - dotyka krótko pętli asynchronicznej; i zawiera prawdziwą implementację (nie powinieneś używać 'ThreadPool' tak, jak zasugerował @Jalal). –

Odpowiedz

15

Oto odpowiedź, która pomoże Ci zacząć - czyli bardziej początkujący niż mój blog post.

. NET ma wzorzec asynchroniczny, który obraca się wokół wywołania Początek * i Koniec *. Na przykład - BeginReceive i EndReceive. Niemal zawsze mają one swój odpowiednik inny niż asynchroniczny (w tym przypadku Receive); i osiągnąć ten sam cel.

Najważniejszą rzeczą do zapamiętania jest to, że te gniazda nie tylko wywołują asynchroniczne połączenie - ujawniają coś, co nazywa się IOCP (IO Completion Ports, Linux/Mono ma te dwa, ale ja zapominam nazwę), co jest niezwykle ważne używać na serwerze; sednem tego, co robi IOCP, jest to, że aplikacja nie zużywa wątku podczas oczekiwania na dane.

Jak użyć deseniu Begin/End

Każdy Begin * metoda będzie mieć dokładnie 2 więcej argumentów w comparisson na to non-asynchroniczny odpowiednik.Pierwszy to AsyncCallback, drugi to obiekt. Te dwie rzeczy oznaczają: "tutaj jest metoda wywoływania, gdy skończysz" i "tutaj jest kilka danych, których potrzebuję w tej metodzie." Metoda, która zostanie wywołana, zawsze ma tę samą sygnaturę, w metodzie tej wywołuje się odpowiednik End *, aby uzyskać wynik, który byłby wynikiem synchronicznej synchronizacji. Tak więc na przykład:

private void BeginReceiveBuffer() 
{ 
    _socket.BeginReceive(buffer, 0, buffer.Length, BufferEndReceive, buffer); 
} 

private void EndReceiveBuffer(IAsyncResult state) 
{ 
    var buffer = (byte[])state.AsyncState; // This is the last parameter. 
    var length = _socket.EndReceive(state); // This is the return value of the method call. 
    DataReceived(buffer, 0, length); // Do something with the data. 
} 

Co się dzieje tutaj jest Net rozpoczyna czeka na dane z gniazda, tak szybko, jak to robi danych wywołuje EndReceiveBuffer i przechodzi przez „niestandardowych danych” (w tym przypadku buffer) do niego przez state.AsyncResult. Gdy zadzwonisz pod numer EndReceive, otrzymasz z powrotem długość otrzymanych danych (lub wyrzucisz wyjątek, jeśli coś nie działa).

Lepsze Wzór dla gniazd

Ta forma daje centralną obsługę błędów - może być stosowany wszędzie tam, gdzie wzór asynchroniczny owija strumieniowej jak „coś” (np TCP pojawia się w kolejności, w jakiej została wysłana , więc może być postrzegany jako obiekt Stream).

private Socket _socket; 
private ArraySegment<byte> _buffer; 
public void StartReceive() 
{ 
    ReceiveAsyncLoop(null); 
} 

// Note that this method is not guaranteed (in fact 
// unlikely) to remain on a single thread across 
// async invocations. 
private void ReceiveAsyncLoop(IAsyncResult result) 
{ 
    try 
    { 
     // This only gets called once - via StartReceive() 
     if (result != null) 
     { 
      int numberOfBytesRead = _socket.EndReceive(result); 
      if(numberOfBytesRead == 0) 
      { 
       OnDisconnected(null); // 'null' being the exception. The client disconnected normally in this case. 
       return; 
      } 

      var newSegment = new ArraySegment<byte>(_buffer.Array, _buffer.Offset, numberOfBytesRead); 
      // This method needs its own error handling. Don't let it throw exceptions unless you 
      // want to disconnect the client. 
      OnDataReceived(newSegment); 
     } 

     // Because of this method call, it's as though we are creating a 'while' loop. 
     // However this is called an async loop, but you can see it the same way. 
     _socket.BeginReceive(_buffer.Array, _buffer.Offset, _buffer.Count, SocketFlags.None, ReceiveAsyncLoop, null); 
    } 
    catch (Exception ex) 
    { 
     // Socket error handling here. 
    } 
} 

Przyjmowanie Wiele Połączenia

Co ogólnie zrobić, to napisać klasy, który zawiera gniazda itd. (Jak również swoją pętlę asynchroniczny) i utworzyć dla każdego klienta. Tak na przykład:

public class InboundConnection 
{ 
    private Socket _socket; 
    private ArraySegment<byte> _buffer; 

    public InboundConnection(Socket clientSocket) 
    { 
     _socket = clientSocket; 
     _buffer = new ArraySegment<byte>(new byte[4096], 0, 4096); 
     StartReceive(); // Start the read async loop. 
    } 

    private void StartReceive() ... 
    private void ReceiveAsyncLoop() ... 
    private void OnDataReceived() ... 
} 

Każde połączenie klient powinien być śledzone przez klasy serwera (tak, że można odłączyć je równo, gdy serwer zostanie zamknięty, a także poszukiwania/wyszukać je).

+0

+1 Dzięki za wspaniały materiał. – Dylan

+0

Zapomniałem wspomnieć, że możesz również akceptować klientów w ten sam sposób, np. BeginAcceptTcpClient. Możesz tak samo ustawić pętlę asynchroniczną. –

+1

Link do postu na blogu nie działa. Ale tutaj jest on archive.org: https://web.archive.org/web/20121127003207/http://jonathan.dickinsons.co.za/blog/2011/02/net-sockets-and-you –

0

Zazwyczaj używam puli wątków z kilkoma wątkami. Po każdym nowym połączeniu uruchamiam obsługę połączenia (w twoim przypadku - wszystko, co robisz w klauzuli using) w jednym z wątków z puli.

Dzięki temu osiągniesz obydwie wydajności, ponieważ pozwalasz na kilka jednocześnie akceptowanych połączeń, a także ograniczasz liczbę zasobów (wątków itp.) Przydzielanych do obsługi połączeń przychodzących.

masz dobry przykład here

Powodzenia

+0

Znowu z rzeczą "Nici na klienta". To jest naprawdę zła praktyka. –

1

należy użyć metody asynchronicznego odczytu danych, przykładem jest:

// Enter the listening loop. 
while (true) 
{ 
    Debug.WriteLine("Waiting for a connection... "); 

    client = server.AcceptTcpClient(); 

    ThreadPool.QueueUserWorkItem(new WaitCallback(HandleTcp), client); 
} 

private void HandleTcp(object tcpClientObject) 
{ 
    TcpClient client = (TcpClient)tcpClientObject; 
    // Perform a blocking call to accept requests. 

    data = new List<byte>(); 

    // Get a stream object for reading and writing 
    using (NetworkStream stream = client.GetStream()) 
    { 
     // Loop to receive all the data sent by the client. 
     int length; 

     while ((length = stream.Read(bytes, 0, bytes.Length)) != 0) 
     { 
      var copy = new byte[length]; 
      Array.Copy(bytes, 0, copy, 0, length); 
      data.AddRange(copy); 
     } 
    } 

    receivedQueue.Add(data); 
} 

Ponadto należy rozważyć użycie AutoResetEvent lub ManualResetEvent do być powiadamiany o dodaniu nowych danych do kolekcji, tak aby wątek, który obsługuje dane, będzie wiedział, kiedy zostaną odebrane dane, a jeśli używasz 4.0 lepiej wyłączyć korzystanie z BlockingCollection zamiast Queue.

+0

Już używasz BlockingCollection. Używam też metod synchronicznych, ponieważ mam dedykowany wątek do odbierania plików. W powyższym przykładzie, jeśli dwóch klientów łączy się w tym samym czasie, czy serwer.AcceptTcpClient zaakceptuje oba, czy też będzie czekał w kolejce, aż TcpListener będzie dostępny (po HandleTcp)? Uwaga! Jeśli korzystasz z .Net 4, powinieneś użyć biblioteki zadań zamiast ThreadPool. – Dylan

+0

Przyjmuje oba, dlatego używamy tutaj "Thread", ponieważ odczyta on pierwsze dane w innym wątku, więc nie zablokuje bieżącego wątku, więc 'ThreadPoo.Queu..' zwróci uchwyt do wątek wywołujący natychmiast i jednocześnie utworzyć nowy wątek do obsługi klienta. –

+0

-1 do korzystania z ThreadPool. W .Net 3.5 powinieneś używać 'Begin/End-Receive'. W .Net 4.0 możesz użyć nowego modelu zdarzeń lub licencji TPL. Twój kod w ogóle nie korzysta z IOCP; co sprawia, że ​​jest to kiepska sugestia. –

1

Do tego celu należy użyć asynchronicznego programowania gniazd. Spójrz na numer example dostarczony przez MSDN.

+1

+1 za cytowanie MSDN - źródło prawdy :). –

Powiązane problemy