Jak napisać skalowalny serwer oparty na protokole Tcp / Ip


148

Jestem na etapie projektowania, pisząc nową aplikację usługi Windows, która akceptuje połączenia TCP / IP dla długotrwałych połączeń (tj. Nie jest to jak HTTP, gdzie jest wiele krótkich połączeń, ale raczej klient łączy się i pozostaje połączony przez wiele godzin lub dni lub nawet tygodnie).

Szukam pomysłów na najlepszy sposób zaprojektowania architektury sieci. Muszę uruchomić co najmniej jeden wątek dla usługi. Rozważam użycie Asynch API (BeginRecieve itp.), Ponieważ nie wiem, ilu klientów będę podłączać w danym momencie (prawdopodobnie setki). Zdecydowanie nie chcę rozpoczynać wątku dla każdego połączenia.

Dane będą płynąć do klientów głównie z mojego serwera, ale czasami będą wysyłane polecenia od klientów. Jest to przede wszystkim aplikacja monitorująca, w której mój serwer okresowo wysyła dane o stanie do klientów.

Jakieś sugestie dotyczące najlepszego sposobu, aby uczynić to tak skalowalnym, jak to tylko możliwe? Podstawowy przepływ pracy? Dzięki.

EDYCJA: Żeby było jasne, szukam rozwiązań opartych na .net (C # jeśli to możliwe, ale każdy język .net będzie działał)

UWAGA: Aby otrzymać nagrodę, oczekuję czegoś więcej niż prostej odpowiedzi. Potrzebowałbym działającego przykładu rozwiązania, albo jako wskaźnika do czegoś, co mógłbym pobrać, albo krótkiego przykładu w linii. I musi być oparty na .net i Windows (dopuszczalny jest dowolny język .net)

EDYCJA: Chcę podziękować wszystkim, którzy udzielili dobrych odpowiedzi. Niestety mogłem zaakceptować tylko jedną i zdecydowałem się zaakceptować bardziej znaną metodę Begin / End. Rozwiązanie Esaca może być lepsze, ale wciąż jest na tyle nowe, że nie wiem na pewno, jak się sprawdzi.

Głosowałem za wszystkimi odpowiedziami, które uważałem za dobre, chciałbym zrobić dla was więcej. Dzięki jeszcze raz.


1
Czy jesteś absolutnie pewien, że musi to być połączenie długotrwałe? Trudno to stwierdzić na podstawie ograniczonych informacji, ale zrobiłbym to tylko wtedy, gdyby było to absolutnie konieczne ..
markt

Tak, to musi trwać długo. Dane muszą być aktualizowane w czasie rzeczywistym, więc nie mogę wykonywać okresowego odpytywania, dane muszą być wysyłane do klienta na bieżąco, co oznacza stałe połączenie.
Erik Funkenbusch

1
To nie jest ważny powód. Http obsługuje długo działające połączenia. Po prostu otwierasz połączenie i czekasz na odpowiedź (zablokowana ankieta). Działa to dobrze w przypadku wielu aplikacji w stylu AJAX itp. Jak myślisz, jak działa Gmail :-)
TFD

2
Gmail działa na zasadzie okresowego odpytywania poczty e-mail, nie utrzymuje długotrwałego połączenia. Jest to dobre w przypadku wiadomości e-mail, gdzie odpowiedź w czasie rzeczywistym nie jest wymagana.
Erik Funkenbusch

2
Polling lub pulling dobrze się skaluje, ale szybko rozwija się utajenie. Wypychanie nie jest również skalowane, ale pomaga zmniejszyć lub wyeliminować opóźnienia.
andrewbadera

Odpowiedzi:


92

W przeszłości napisałem coś podobnego. Z moich badań lata temu wynika, że ​​najlepszym rozwiązaniem było napisanie własnej implementacji gniazd, przy użyciu gniazd asynchronicznych. Oznaczało to, że klienci, którzy tak naprawdę nic nie robili, potrzebowali stosunkowo niewielkich zasobów. Wszystko, co ma miejsce, jest obsługiwane przez pulę wątków .net.

Napisałem to jako klasę zarządzającą wszystkimi połączeniami dla serwerów.

Po prostu użyłem listy do przechowywania wszystkich połączeń klientów, ale jeśli potrzebujesz szybszego wyszukiwania większych list, możesz ją napisać w dowolny sposób.

private List<xConnection> _sockets;

Potrzebujesz również gniazda faktycznie nasłuchującego połączeń przychodzących.

private System.Net.Sockets.Socket _serverSocket;

Metoda start w rzeczywistości uruchamia gniazdo serwera i rozpoczyna nasłuchiwanie wszelkich połączeń przychodzących.

public bool Start()
{
  System.Net.IPHostEntry localhost = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
  System.Net.IPEndPoint serverEndPoint;
  try
  {
     serverEndPoint = new System.Net.IPEndPoint(localhost.AddressList[0], _port);
  }
  catch (System.ArgumentOutOfRangeException e)
  {
    throw new ArgumentOutOfRangeException("Port number entered would seem to be invalid, should be between 1024 and 65000", e);
  }
  try
  {
    _serverSocket = new System.Net.Sockets.Socket(serverEndPoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
   }
   catch (System.Net.Sockets.SocketException e)
   {
      throw new ApplicationException("Could not create socket, check to make sure not duplicating port", e);
    }
    try
    {
      _serverSocket.Bind(serverEndPoint);
      _serverSocket.Listen(_backlog);
    }
    catch (Exception e)
    {
       throw new ApplicationException("Error occured while binding socket, check inner exception", e);
    }
    try
    {
       //warning, only call this once, this is a bug in .net 2.0 that breaks if 
       // you're running multiple asynch accepts, this bug may be fixed, but
       // it was a major pain in the ass previously, so make sure there is only one
       //BeginAccept running
       _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
    }
    catch (Exception e)
    {
       throw new ApplicationException("Error occured starting listeners, check inner exception", e);
    }
    return true;
 }

Chciałbym tylko zauważyć, że kod obsługi wyjątków wygląda źle, ale powodem tego jest to, że miałem tam kod falseblokujący wyjątki, aby wszelkie wyjątki były pomijane i zwracane, jeśli ustawiono opcję konfiguracji, ale chciałem go usunąć ze względu na zwięzłość.

_ServerSocket.BeginAccept (new AsyncCallback (acceptCallback)), _serverSocket) powyżej zasadniczo ustawia nasze gniazdo serwera na wywołanie metody acceptCallback za każdym razem, gdy użytkownik się połączy. Ta metoda jest uruchamiana z puli wątków .Net, która automatycznie obsługuje tworzenie dodatkowych wątków roboczych, jeśli masz wiele operacji blokujących. Powinno to optymalnie obsłużyć każde obciążenie serwera.

    private void acceptCallback(IAsyncResult result)
    {
       xConnection conn = new xConnection();
       try
       {
         //Finish accepting the connection
         System.Net.Sockets.Socket s = (System.Net.Sockets.Socket)result.AsyncState;
         conn = new xConnection();
         conn.socket = s.EndAccept(result);
         conn.buffer = new byte[_bufferSize];
         lock (_sockets)
         {
           _sockets.Add(conn);
         }
         //Queue recieving of data from the connection
         conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
         //Queue the accept of the next incomming connection
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
       catch (SocketException e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
       catch (Exception e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
     }

Powyższy kod w zasadzie właśnie zakończył akceptację nadchodzącego połączenia, kolejek, BeginReceivektóre są wywołaniami zwrotnymi, które zostaną uruchomione, gdy klient wyśle ​​dane, a następnie kolejkuje następne, acceptCallbackktóre zaakceptuje następne połączenie klienta.

BeginReceiveWywołanie metody jest to, co mówi, gniazdo, co zrobić, gdy odbiera dane od klienta. Dla BeginReceive, trzeba dać mu tablicę bajtów, czyli tam, gdzie będzie to skopiować dane, gdy klient wysyła dane. ReceiveCallbackMetoda będzie się nazywa, co jest jak załatwiamy odbiór danych.

private void ReceiveCallback(IAsyncResult result)
{
  //get our connection from the callback
  xConnection conn = (xConnection)result.AsyncState;
  //catch any errors, we'd better not have any
  try
  {
    //Grab our buffer and count the number of bytes receives
    int bytesRead = conn.socket.EndReceive(result);
    //make sure we've read something, if we haven't it supposadly means that the client disconnected
    if (bytesRead > 0)
    {
      //put whatever you want to do when you receive data here

      //Queue the next receive
      conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
     }
     else
     {
       //Callback run but no data, close the connection
       //supposadly means a disconnect
       //and we still have to close the socket, even though we throw the event later
       conn.socket.Close();
       lock (_sockets)
       {
         _sockets.Remove(conn);
       }
     }
   }
   catch (SocketException e)
   {
     //Something went terribly wrong
     //which shouldn't have happened
     if (conn.socket != null)
     {
       conn.socket.Close();
       lock (_sockets)
       {
         _sockets.Remove(conn);
       }
     }
   }
 }

EDYCJA: W tym wzorze zapomniałem wspomnieć, że w tym obszarze kodu:

//put whatever you want to do when you receive data here

//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);

Generalnie robiłbym w kodzie, jaki chcesz, to ponowne składanie pakietów w wiadomości, a następnie tworzenie ich jako zadań w puli wątków. W ten sposób BeginReceive następnego bloku od klienta nie jest opóźnione podczas działania dowolnego kodu przetwarzania komunikatów.

Akceptacja wywołania zwrotnego kończy odczytywanie gniazda danych przez wywołanie zakończenia odbioru. Wypełnia to bufor dostarczony w funkcji rozpoczęcia odbioru. Gdy zrobisz cokolwiek chcesz w miejscu, w którym zostawiłem komentarz, wywołujemy następną BeginReceivemetodę, która uruchomi wywołanie zwrotne ponownie, jeśli klient wyśle ​​więcej danych. Teraz jest naprawdę trudna część, kiedy klient wysyła dane, twoje wywołanie zwrotne może zostać wywołane tylko z częścią wiadomości. Ponowny montaż może stać się bardzo skomplikowany. Użyłem swojej własnej metody i stworzyłem w tym celu rodzaj zastrzeżonego protokołu. Pominąłem to, ale jeśli poprosisz, mogę to dodać. Ten program obsługi był właściwie najbardziej skomplikowanym fragmentem kodu, jaki kiedykolwiek napisałem.

public bool Send(byte[] message, xConnection conn)
{
  if (conn != null && conn.socket.Connected)
  {
    lock (conn.socket)
    {
    //we use a blocking mode send, no async on the outgoing
    //since this is primarily a multithreaded application, shouldn't cause problems to send in blocking mode
       conn.socket.Send(bytes, bytes.Length, SocketFlags.None);
     }
   }
   else
     return false;
   return true;
 }

Powyższa metoda wysyłania w rzeczywistości wykorzystuje Sendwywołanie synchroniczne , co dla mnie było w porządku ze względu na rozmiary wiadomości i wielowątkowość mojej aplikacji. Jeśli chcesz wysłać wiadomość do każdego klienta, wystarczy przejrzeć listę _sockets.

Klasa xConnection, o której mowa powyżej, jest w zasadzie prostym opakowaniem dla gniazda, które zawiera bufor bajtowy, aw mojej implementacji kilka dodatków.

public class xConnection : xBase
{
  public byte[] buffer;
  public System.Net.Sockets.Socket socket;
}

Również w celach informacyjnych są tutaj usingzałączone przeze mnie elementy, ponieważ zawsze denerwuję się, gdy ich nie ma.

using System.Net.Sockets;

Mam nadzieję, że to pomocne, może nie jest to najczystszy kod, ale działa. Istnieje również kilka niuansów w kodzie, których zmianą powinieneś być zmęczony. Po pierwsze, możesz mieć tylko jedno połączenie BeginAcceptw dowolnym momencie. Wokół tego występował bardzo irytujący błąd .net, który miał miejsce lata temu, więc nie pamiętam szczegółów.

Ponadto w ReceiveCallbackkodzie przetwarzamy wszystko otrzymane z gniazda przed umieszczeniem w kolejce następnego odbioru. Oznacza to, że w przypadku pojedynczego gniazda jesteśmy w rzeczywistości tylko ReceiveCallbackraz w dowolnym momencie i nie musimy używać synchronizacji wątków. Jeśli jednak zmienisz kolejność, aby wywołać następny odbiór natychmiast po pobraniu danych, co może być trochę szybsze, musisz upewnić się, że poprawnie zsynchronizowałeś wątki.

Poza tym hackowałem dużo swojego kodu, ale pozostawiłem istotę tego, co się dzieje. To powinien być dobry początek dla twojego projektu. Zostaw komentarz, jeśli masz więcej pytań na ten temat.


1
To dobra odpowiedź, Kevin ... wygląda na to, że jesteś na dobrej drodze do zdobycia nagrody. :)
Erik Funkenbusch

6
Nie wiem, dlaczego jest to najwyższa głosowana odpowiedź. Begin * End * nie jest najszybszym sposobem tworzenia sieci w języku C # ani najbardziej skalowalnym. JEST szybszy niż synchroniczny, ale jest wiele operacji wykonywanych pod maską w systemie Windows, które naprawdę spowalniają tę ścieżkę sieciową.
esac

6
Pamiętaj, co napisał esac w poprzednim komentarzu. Wzorzec początek-koniec prawdopodobnie będzie działał dla Ciebie do pewnego momentu, do cholery, mój kod obecnie używa funkcji begin-end, ale są ulepszenia jego ograniczeń w .net 3.5. Nie obchodzi mnie nagroda, ale polecam przeczytanie linku w mojej odpowiedzi, nawet jeśli zastosujesz to podejście. „Ulepszenia wydajności gniazda w wersji 3.5”
jvanderh

1
Chciałem tylko dorzucić ich, ponieważ być może nie byłem wystarczająco jasny, to jest kod ery .net 2.0, w którym wierzę, że był to bardzo realny wzorzec. Jednak odpowiedź esac wydaje się być nieco nowocześniejsza, jeśli celuję w .net 3.5, jedyny dziób, jaki mam, to rzucanie zdarzeń :), ale można to łatwo zmienić. Przeprowadziłem również testy przepustowości z tym kodem i na dwurdzeniowym opteronie 2Ghz był w stanie maksymalnie przekroczyć 100 Mb / s ethernet, co dodało warstwę szyfrowania na wierzchu tego kodu.
Kevin Nisbet

1
@KevinNisbet Wiem, że jest już dość późno, ale dla każdego, kto używa tej odpowiedzi do projektowania własnych serwerów - wysyłanie powinno być również asynchroniczne, bo inaczej narażasz się na możliwość zakleszczenia. Jeśli obie strony zapiszą dane, które wypełniają ich odpowiednie bufory, Sendmetody będą blokować na czas nieokreślony po obu stronach, ponieważ nikt nie czyta danych wejściowych.
Luaan

83

Istnieje wiele sposobów wykonywania operacji sieciowych w języku C #. Wszystkie z nich używają różnych mechanizmów pod maską, a zatem mają poważne problemy z wydajnością przy wysokiej współbieżności. Operacje typu Begin * to jedna z tych, które wiele osób często myli za najszybszy / najszybszy sposób tworzenia sieci.

Aby rozwiązać te problemy, wprowadzili zestaw metod * Async: Z MSDN http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

Klasa SocketAsyncEventArgs jest częścią zestawu ulepszeń klasy System.Net.Sockets .. ::. Socket, które udostępniają alternatywny wzorzec asynchroniczny, który może być używany przez wyspecjalizowane aplikacje gniazd o wysokiej wydajności. Ta klasa została specjalnie zaprojektowana dla aplikacji serwerów sieciowych, które wymagają wysokiej wydajności. Aplikacja może używać ulepszonego wzorca asynchronicznego wyłącznie lub tylko w docelowych obszarach aktywnych (na przykład podczas odbierania dużych ilości danych).

Główną cechą tych ulepszeń jest uniknięcie powtarzającego się przydzielania i synchronizacji obiektów podczas asynchronicznych operacji we / wy o dużej objętości. Wzorzec projektowy Begin / End aktualnie zaimplementowany przez klasę System.Net.Sockets .. ::. Socket wymaga przydzielenia obiektu System .. ::. IAsyncResult dla każdej asynchronicznej operacji gniazda.

Pod okładkami * Async API wykorzystuje porty zakończenia we / wy, które są najszybszym sposobem wykonywania operacji sieciowych, zobacz http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

Aby Ci pomóc, dołączam kod źródłowy serwera telnet, który napisałem przy użyciu * Async API. Uwzględniam tylko odpowiednie porcje. Należy również zauważyć, że zamiast przetwarzać dane w tekście, zamiast tego decyduję się na wypchnięcie ich do kolejki bez blokady (bez oczekiwania), która jest przetwarzana w osobnym wątku. Zwróć uwagę, że nie uwzględniam odpowiedniej klasy Pool, która jest po prostu prostą pulą, która utworzy nowy obiekt, jeśli jest pusta, oraz klasą Buffer, która jest tylko samorozszerzającym się buforem, który nie jest naprawdę potrzebny, chyba że otrzymujesz indeterministyczny ilość danych. Jeśli chcesz uzyskać więcej informacji, wyślij mi PM.

 public class Telnet
{
    private readonly Pool<SocketAsyncEventArgs> m_EventArgsPool;
    private Socket m_ListenSocket;

    /// <summary>
    /// This event fires when a connection has been established.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> Connected;

    /// <summary>
    /// This event fires when a connection has been shutdown.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> Disconnected;

    /// <summary>
    /// This event fires when data is received on the socket.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> DataReceived;

    /// <summary>
    /// This event fires when data is finished sending on the socket.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> DataSent;

    /// <summary>
    /// This event fires when a line has been received.
    /// </summary>
    public event EventHandler<LineReceivedEventArgs> LineReceived;

    /// <summary>
    /// Specifies the port to listen on.
    /// </summary>
    [DefaultValue(23)]
    public int ListenPort { get; set; }

    /// <summary>
    /// Constructor for Telnet class.
    /// </summary>
    public Telnet()
    {           
        m_EventArgsPool = new Pool<SocketAsyncEventArgs>();
        ListenPort = 23;
    }

    /// <summary>
    /// Starts the telnet server listening and accepting data.
    /// </summary>
    public void Start()
    {
        IPEndPoint endpoint = new IPEndPoint(0, ListenPort);
        m_ListenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        m_ListenSocket.Bind(endpoint);
        m_ListenSocket.Listen(100);

        //
        // Post Accept
        //
        StartAccept(null);
    }

    /// <summary>
    /// Not Yet Implemented. Should shutdown all connections gracefully.
    /// </summary>
    public void Stop()
    {
        //throw (new NotImplementedException());
    }

    //
    // ACCEPT
    //

    /// <summary>
    /// Posts a requests for Accepting a connection. If it is being called from the completion of
    /// an AcceptAsync call, then the AcceptSocket is cleared since it will create a new one for
    /// the new user.
    /// </summary>
    /// <param name="e">null if posted from startup, otherwise a <b>SocketAsyncEventArgs</b> for reuse.</param>
    private void StartAccept(SocketAsyncEventArgs e)
    {
        if (e == null)
        {
            e = m_EventArgsPool.Pop();
            e.Completed += Accept_Completed;
        }
        else
        {
            e.AcceptSocket = null;
        }

        if (m_ListenSocket.AcceptAsync(e) == false)
        {
            Accept_Completed(this, e);
        }
    }

    /// <summary>
    /// Completion callback routine for the AcceptAsync post. This will verify that the Accept occured
    /// and then setup a Receive chain to begin receiving data.
    /// </summary>
    /// <param name="sender">object which posted the AcceptAsync</param>
    /// <param name="e">Information about the Accept call.</param>
    private void Accept_Completed(object sender, SocketAsyncEventArgs e)
    {
        //
        // Socket Options
        //
        e.AcceptSocket.NoDelay = true;

        //
        // Create and setup a new connection object for this user
        //
        Connection connection = new Connection(this, e.AcceptSocket);

        //
        // Tell the client that we will be echo'ing data sent
        //
        DisableEcho(connection);

        //
        // Post the first receive
        //
        SocketAsyncEventArgs args = m_EventArgsPool.Pop();
        args.UserToken = connection;

        //
        // Connect Event
        //
        if (Connected != null)
        {
            Connected(this, args);
        }

        args.Completed += Receive_Completed;
        PostReceive(args);

        //
        // Post another accept
        //
        StartAccept(e);
    }

    //
    // RECEIVE
    //    

    /// <summary>
    /// Post an asynchronous receive on the socket.
    /// </summary>
    /// <param name="e">Used to store information about the Receive call.</param>
    private void PostReceive(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection != null)
        {
            connection.ReceiveBuffer.EnsureCapacity(64);
            e.SetBuffer(connection.ReceiveBuffer.DataBuffer, connection.ReceiveBuffer.Count, connection.ReceiveBuffer.Remaining);

            if (connection.Socket.ReceiveAsync(e) == false)
            {
                Receive_Completed(this, e);
            }              
        }
    }

    /// <summary>
    /// Receive completion callback. Should verify the connection, and then notify any event listeners
    /// that data has been received. For now it is always expected that the data will be handled by the
    /// listeners and thus the buffer is cleared after every call.
    /// </summary>
    /// <param name="sender">object which posted the ReceiveAsync</param>
    /// <param name="e">Information about the Receive call.</param>
    private void Receive_Completed(object sender, SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (e.BytesTransferred == 0 || e.SocketError != SocketError.Success || connection == null)
        {
            Disconnect(e);
            return;
        }

        connection.ReceiveBuffer.UpdateCount(e.BytesTransferred);

        OnDataReceived(e);

        HandleCommand(e);
        Echo(e);

        OnLineReceived(connection);

        PostReceive(e);
    }

    /// <summary>
    /// Handles Event of Data being Received.
    /// </summary>
    /// <param name="e">Information about the received data.</param>
    protected void OnDataReceived(SocketAsyncEventArgs e)
    {
        if (DataReceived != null)
        {                
            DataReceived(this, e);
        }
    }

    /// <summary>
    /// Handles Event of a Line being Received.
    /// </summary>
    /// <param name="connection">User connection.</param>
    protected void OnLineReceived(Connection connection)
    {
        if (LineReceived != null)
        {
            int index = 0;
            int start = 0;

            while ((index = connection.ReceiveBuffer.IndexOf('\n', index)) != -1)
            {
                string s = connection.ReceiveBuffer.GetString(start, index - start - 1);
                s = s.Backspace();

                LineReceivedEventArgs args = new LineReceivedEventArgs(connection, s);
                Delegate[] delegates = LineReceived.GetInvocationList();

                foreach (Delegate d in delegates)
                {
                    d.DynamicInvoke(new object[] { this, args });

                    if (args.Handled == true)
                    {
                        break;
                    }
                }

                if (args.Handled == false)
                {
                    connection.CommandBuffer.Enqueue(s);
                }

                start = index;
                index++;
            }

            if (start > 0)
            {
                connection.ReceiveBuffer.Reset(0, start + 1);
            }
        }
    }

    //
    // SEND
    //

    /// <summary>
    /// Overloaded. Sends a string over the telnet socket.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="s">Data to send.</param>
    /// <returns>true if the data was sent successfully.</returns>
    public bool Send(Connection connection, string s)
    {
        if (String.IsNullOrEmpty(s) == false)
        {
            return Send(connection, Encoding.Default.GetBytes(s));
        }

        return false;
    }

    /// <summary>
    /// Overloaded. Sends an array of data to the client.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="data">Data to send.</param>
    /// <returns>true if the data was sent successfully.</returns>
    public bool Send(Connection connection, byte[] data)
    {
        return Send(connection, data, 0, data.Length);
    }

    public bool Send(Connection connection, char c)
    {
        return Send(connection, new byte[] { (byte)c }, 0, 1);
    }

    /// <summary>
    /// Sends an array of data to the client.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="data">Data to send.</param>
    /// <param name="offset">Starting offset of date in the buffer.</param>
    /// <param name="length">Amount of data in bytes to send.</param>
    /// <returns></returns>
    public bool Send(Connection connection, byte[] data, int offset, int length)
    {
        bool status = true;

        if (connection.Socket == null || connection.Socket.Connected == false)
        {
            return false;
        }

        SocketAsyncEventArgs args = m_EventArgsPool.Pop();
        args.UserToken = connection;
        args.Completed += Send_Completed;
        args.SetBuffer(data, offset, length);

        try
        {
            if (connection.Socket.SendAsync(args) == false)
            {
                Send_Completed(this, args);
            }
        }
        catch (ObjectDisposedException)
        {                
            //
            // return the SocketAsyncEventArgs back to the pool and return as the
            // socket has been shutdown and disposed of
            //
            m_EventArgsPool.Push(args);
            status = false;
        }

        return status;
    }

    /// <summary>
    /// Sends a command telling the client that the server WILL echo data.
    /// </summary>
    /// <param name="connection">Connection to disable echo on.</param>
    public void DisableEcho(Connection connection)
    {
        byte[] b = new byte[] { 255, 251, 1 };
        Send(connection, b);
    }

    /// <summary>
    /// Completion callback for SendAsync.
    /// </summary>
    /// <param name="sender">object which initiated the SendAsync</param>
    /// <param name="e">Information about the SendAsync call.</param>
    private void Send_Completed(object sender, SocketAsyncEventArgs e)
    {
        e.Completed -= Send_Completed;              
        m_EventArgsPool.Push(e);
    }        

    /// <summary>
    /// Handles a Telnet command.
    /// </summary>
    /// <param name="e">Information about the data received.</param>
    private void HandleCommand(SocketAsyncEventArgs e)
    {
        Connection c = e.UserToken as Connection;

        if (c == null || e.BytesTransferred < 3)
        {
            return;
        }

        for (int i = 0; i < e.BytesTransferred; i += 3)
        {
            if (e.BytesTransferred - i < 3)
            {
                break;
            }

            if (e.Buffer[i] == (int)TelnetCommand.IAC)
            {
                TelnetCommand command = (TelnetCommand)e.Buffer[i + 1];
                TelnetOption option = (TelnetOption)e.Buffer[i + 2];

                switch (command)
                {
                    case TelnetCommand.DO:
                        if (option == TelnetOption.Echo)
                        {
                            // ECHO
                        }
                        break;
                    case TelnetCommand.WILL:
                        if (option == TelnetOption.Echo)
                        {
                            // ECHO
                        }
                        break;
                }

                c.ReceiveBuffer.Remove(i, 3);
            }
        }          
    }

    /// <summary>
    /// Echoes data back to the client.
    /// </summary>
    /// <param name="e">Information about the received data to be echoed.</param>
    private void Echo(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection == null)
        {
            return;
        }

        //
        // backspacing would cause the cursor to proceed beyond the beginning of the input line
        // so prevent this
        //
        string bs = connection.ReceiveBuffer.ToString();

        if (bs.CountAfterBackspace() < 0)
        {
            return;
        }

        //
        // find the starting offset (first non-backspace character)
        //
        int i = 0;

        for (i = 0; i < connection.ReceiveBuffer.Count; i++)
        {
            if (connection.ReceiveBuffer[i] != '\b')
            {
                break;
            }
        }

        string s = Encoding.Default.GetString(e.Buffer, Math.Max(e.Offset, i), e.BytesTransferred);

        if (connection.Secure)
        {
            s = s.ReplaceNot("\r\n\b".ToCharArray(), '*');
        }

        s = s.Replace("\b", "\b \b");

        Send(connection, s);
    }

    //
    // DISCONNECT
    //

    /// <summary>
    /// Disconnects a socket.
    /// </summary>
    /// <remarks>
    /// It is expected that this disconnect is always posted by a failed receive call. Calling the public
    /// version of this method will cause the next posted receive to fail and this will cleanup properly.
    /// It is not advised to call this method directly.
    /// </remarks>
    /// <param name="e">Information about the socket to be disconnected.</param>
    private void Disconnect(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection == null)
        {
            throw (new ArgumentNullException("e.UserToken"));
        }

        try
        {
            connection.Socket.Shutdown(SocketShutdown.Both);
        }
        catch
        {
        }

        connection.Socket.Close();

        if (Disconnected != null)
        {
            Disconnected(this, e);
        }

        e.Completed -= Receive_Completed;
        m_EventArgsPool.Push(e);
    }

    /// <summary>
    /// Marks a specific connection for graceful shutdown. The next receive or send to be posted
    /// will fail and close the connection.
    /// </summary>
    /// <param name="connection"></param>
    public void Disconnect(Connection connection)
    {
        try
        {
            connection.Socket.Shutdown(SocketShutdown.Both);
        }
        catch (Exception)
        {
        }            
    }

    /// <summary>
    /// Telnet command codes.
    /// </summary>
    internal enum TelnetCommand
    {
        SE = 240,
        NOP = 241,
        DM = 242,
        BRK = 243,
        IP = 244,
        AO = 245,
        AYT = 246,
        EC = 247,
        EL = 248,
        GA = 249,
        SB = 250,
        WILL = 251,
        WONT = 252,
        DO = 253,
        DONT = 254,
        IAC = 255
    }

    /// <summary>
    /// Telnet command options.
    /// </summary>
    internal enum TelnetOption
    {
        Echo = 1,
        SuppressGoAhead = 3,
        Status = 5,
        TimingMark = 6,
        TerminalType = 24,
        WindowSize = 31,
        TerminalSpeed = 32,
        RemoteFlowControl = 33,
        LineMode = 34,
        EnvironmentVariables = 36
    }
}

To jest całkiem proste i prosty przykład. Dzięki. Będę musiał ocenić zalety i wady każdej metody.
Erik Funkenbusch

Nie miałem okazji tego przetestować, ale z jakiegoś powodu mam tu niejasne uczucie wyścigu. Po pierwsze, jeśli otrzymujesz dużo wiadomości, nie wiem, czy zdarzenia będą przetwarzane w kolejności (może nie być ważne dla aplikacji użytkowników, ale należy to odnotować) lub mogę się mylić i zdarzenia będą przetwarzane w kolejności. Po drugie, czy mogłem to przeoczyć, ale czy nie ma ryzyka, że ​​bufor zostanie zastąpiony wyczyszczony, gdy DataReceived nadal działa, jeśli zajmuje to dużo czasu? Jeśli te potencjalnie nieuzasadnione obawy zostaną rozwiązane, myślę, że jest to bardzo dobre, nowoczesne rozwiązanie.
Kevin Nisbet

1
W moim przypadku dla mojego serwera telnet 100% TAK są w porządku. Kluczem jest ustawienie właściwej metody wywołania zwrotnego przed wywołaniem AcceptAsync, ReceiveAsync itp. W moim przypadku SendAsync wykonuję w osobnym wątku, więc jeśli zostanie to zmodyfikowane w celu wykonania wzorca Accept / Send / Receive / Send / Receive / Disconnect, to będzie musiał zostać zmodyfikowany.
esac

1
Punkt # 2 to również coś, co musisz wziąć pod uwagę. Przechowuję mój obiekt „Connection” w kontekście SocketAsyncEventArgs. Oznacza to, że mam tylko jeden bufor odbioru na połączenie. Nie publikuję innego odbioru z tym SocketAsyncEventArgs, dopóki DataReceived nie zostanie ukończona, więc nie można odczytać dalszych danych, dopóki nie zostanie ukończona. RADZĘ, aby nie wykonywać długich operacji na tych danych. Właściwie przenoszę cały bufor wszystkich otrzymanych danych do kolejki bez blokad, a następnie przetwarzam go w osobnym wątku. Zapewnia to małe opóźnienia w części sieciowej.
esac

1
Na marginesie napisałem testy jednostkowe i testy obciążenia dla tego kodu, a ponieważ zwiększyłem obciążenie użytkownika z 1 użytkownika do 250 użytkowników (na jednym systemie dwurdzeniowym, 4 GB pamięci RAM), czas odpowiedzi na 100 bajtów (1 pakiet) i 10000 bajtów (3 pakiety) pozostały takie same przez całą krzywą obciążenia użytkownika.
esac

46

Kiedyś była naprawdę dobra dyskusja na temat skalowalnego TCP / IP przy użyciu .NET, napisana przez Chrisa Mullinsa z Coversant, niestety wygląda na to, że jego blog zniknął z poprzedniego miejsca, więc spróbuję zebrać jego rady z pamięci (kilka przydatnych komentarzy o jego pojawieniu się w tym wątku: C ++ vs. C #: Opracowanie wysoce skalowalnego serwera IOCP )

Przede wszystkim należy zauważyć, że zarówno używanie, jak Begin/Endi Asyncmetody w Socketklasie wykorzystują porty IO Completion Port (IOCP) w celu zapewnienia skalowalności. To powoduje znacznie większą różnicę (przy prawidłowym użyciu; patrz poniżej) w skalowalności niż ta, którą z dwóch metod faktycznie wybierzesz do wdrożenia rozwiązania.

Posty Chrisa Mullinsa były oparte na używaniu Begin/End, z którym osobiście mam doświadczenie. Zauważ, że Chris stworzył oparte na tym rozwiązanie, które przeskalowało do 10 000 jednoczesnych połączeń klientów na 32-bitowej maszynie z 2 GB pamięci i do 100 000 na platformie 64-bitowej z wystarczającą ilością pamięci. Z własnego doświadczenia z tą techniką (chociaż nigdzie w pobliżu tego rodzaju obciążenia) nie mam powodu, aby wątpić w te orientacyjne liczby.

IOCP a prymitywy typu wątek na połączenie lub „wybierz”

Powodem, dla którego chcesz użyć mechanizmu, który korzysta z IOCP pod maską, jest to, że używa on bardzo niskiego poziomu puli wątków systemu Windows, która nie budzi żadnych wątków, dopóki nie będą rzeczywiste dane w kanale IO, z którego próbujesz odczytać ( zwróć uwagę, że IOCP może być również używany do plików IO). Zaletą tego jest to, że system Windows nie musi przełączać się do wątku tylko po to, aby stwierdzić, że i tak nie ma jeszcze danych, więc zmniejsza to liczbę przełączeń kontekstu, które będzie musiał wykonać serwer do niezbędnego minimum.

Przełączniki kontekstu z pewnością zabiją mechanizm „wątek na połączenie”, chociaż jest to realne rozwiązanie, jeśli masz do czynienia tylko z kilkoma tuzinami połączeń. Ten mechanizm nie jest jednak w żadnym wypadku „skalowalny”.

Ważne uwagi dotyczące korzystania z IOCP

Pamięć

Przede wszystkim ważne jest, aby zrozumieć, że IOCP może łatwo spowodować problemy z pamięcią w .NET, jeśli Twoja implementacja jest zbyt naiwna. Każde BeginReceivewywołanie IOCP spowoduje „przypięcie” bufora, do którego czytasz. Aby uzyskać dobre wyjaśnienie, dlaczego jest to problem, zobacz: Blog Yun Jina: OutOfMemoryException and Pinning .

Na szczęście tego problemu można uniknąć, ale wymaga to trochę kompromisu. Sugerowanym rozwiązaniem jest przydzielenie dużego byte[]bufora podczas uruchamiania aplikacji (lub jego zamknięcia), o wielkości co najmniej 90 KB (od .NET 2 wymagany rozmiar może być większy w późniejszych wersjach). Powodem tego jest to, że duże alokacje pamięci automatycznie kończą się w segmencie pamięci niekompaktowanym (The Large Object Heap), który jest skutecznie automatycznie przypinany. Przydzielając jeden duży bufor podczas uruchamiania, upewniasz się, że ten blok pamięci nieporuszalnej ma stosunkowo „niski adres”, gdzie nie będzie przeszkadzał i nie spowoduje fragmentacji.

Następnie możesz użyć przesunięć, aby podzielić ten jeden duży bufor na oddzielne obszary dla każdego połączenia, które musi odczytać niektóre dane. Tutaj pojawia się kompromis; ponieważ ten bufor musi być wstępnie przydzielony, będziesz musiał zdecydować, ile przestrzeni bufora potrzebujesz na połączenie i jaki górny limit chcesz ustawić na liczbę połączeń, do których chcesz skalować (lub możesz zaimplementować abstrakcję które mogą przydzielić dodatkowe przypięte bufory, gdy będą potrzebne).

Najprostszym rozwiązaniem byłoby przypisanie każdemu połączeniu jednego bajtu z unikalnym przesunięciem w tym buforze. Następnie możesz BeginReceivezażądać odczytania pojedynczego bajtu, a resztę wykonać w wyniku otrzymanego wywołania zwrotnego.

Przetwarzanie

Kiedy otrzymasz wywołanie zwrotne z Beginwywołania, które wykonałeś, bardzo ważne jest, aby zdać sobie sprawę, że kod w wywołaniu zwrotnym zostanie wykonany w wątku IOCP niskiego poziomu. Jest absolutnie konieczne , aby unikać długotrwałych operacji w tym wywołaniu zwrotnym. Używanie tych wątków do złożonego przetwarzania zabije skalowalność tak samo efektywnie, jak użycie „wątku na połączenie”.

Sugerowanym rozwiązaniem jest użycie wywołania zwrotnego tylko w celu kolejkowania elementu roboczego w celu przetworzenia danych przychodzących, które zostaną wykonane w innym wątku. Unikaj wszelkich potencjalnie blokujących operacji wewnątrz wywołania zwrotnego, aby wątek IOCP mógł jak najszybciej wrócić do swojej puli. W .NET 4.0 sugerowałbym, że najłatwiejszym rozwiązaniem jest utworzenie pliku Task, podając mu odniesienie do gniazda klienta i kopię pierwszego bajtu, który został już odczytany przez BeginReceivewywołanie. To zadanie jest następnie odpowiedzialne za odczytanie wszystkich danych z gniazda, które reprezentują przetwarzane żądanie, wykonanie go, a następnie BeginReceiveponowne wywołanie kolejki gniazda dla IOCP. W wersji starszej niż .NET 4.0 można użyć puli wątków lub utworzyć własną implementację kolejki roboczej z wątkami.

Podsumowanie

Zasadniczo sugerowałbym użycie przykładowego kodu Kevina dla tego rozwiązania z następującymi dodatkowymi ostrzeżeniami:

  • Upewnij się, że bufor, do którego przechodzisz, BeginReceivejest już „przypięty”
  • Upewnij się, że wywołanie zwrotne, do którego przekazujesz, BeginReceivenie robi nic więcej niż kolejkowanie zadania w celu obsługi faktycznego przetwarzania przychodzących danych

Gdy to zrobisz, nie wątpię, że możesz powtórzyć wyniki Chrisa, skalując do potencjalnie setek tysięcy jednoczesnych klientów (biorąc pod uwagę odpowiedni sprzęt i wydajną implementację własnego kodu przetwarzania;)


1
Aby przypiąć mniejszy blok pamięci, można użyć metody Alloc obiektu GCHandle do przypięcia bufora. Po wykonaniu tej czynności UnsafeAddrOfPinnedArrayElement obiektu Marshal może służyć do uzyskiwania wskaźnika do buforu. Na przykład: GCHandle gchTheCards = GCHandle.Alloc (TheData, GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement (TheData, 0); (sbyte *) pTheData = (sbyte *) pAddr.ToPointer ();
Bob Bryan,

@BobBryan O ile nie przegapię subtelnego punktu, który próbujesz zrobić, to podejście w rzeczywistości nie pomaga w rozwiązaniu problemu, który moje rozwiązanie próbuje rozwiązać, przydzielając duże bloki, a mianowicie potencjał do dramatycznej fragmentacji pamięci nieodłącznie związanej z wielokrotnym przydzielaniem małych przypiętych bloków pamięciowy.
jerryjvl

Cóż, chodzi o to, że nie musisz przydzielać dużego bloku, aby był przypięty do pamięci. Możesz przydzielić mniejsze bloki i użyć powyższej techniki, aby przypiąć je do pamięci, aby uniknąć przenoszenia ich przez gc. Możesz zachować odniesienie do każdego z mniejszych bloków, podobnie jak odniesienie do jednego większego bloku i użyć ich ponownie w razie potrzeby. Oba podejście jest poprawne - właśnie wskazywałem, że nie musisz używać bardzo dużego bufora. Ale powiedziawszy, że czasami użycie bardzo dużego bufora jest najlepszym rozwiązaniem, ponieważ gc będzie traktować go wydajniej.
Bob Bryan

@BobBryan, ponieważ przypinanie bufora następuje automatycznie, gdy wywołujesz BeginReceive, przypinanie nie jest tutaj tak naprawdę istotnym punktem; wydajność była;) ... i jest to szczególnie istotne przy próbie napisania skalowalnego serwera, stąd potrzeba przydzielenia dużych bloków do wykorzystania jako przestrzeń buforowa.
jerryjvl

@jerryjvl Przykro mi, że zadałem naprawdę stare pytanie, jednak niedawno odkryłem dokładnie ten problem z metodami asynchronicznymi BeginXXX / EndXXX. To świetny post, ale znalezienie go wymagało dużo czasu. Podoba mi się twoje sugerowane rozwiązanie, ale nie rozumiem części z niego: „Następnie możesz wykonać wywołanie BeginReceive w celu odczytania jednego bajtu, a resztę wykonać w wyniku otrzymanego wywołania zwrotnego”. Co masz na myśli, mówiąc, że wykonujesz resztę przygotowań w wyniku otrzymanego oddzwonienia?
Mausimo

22

Większość odpowiedzi otrzymałeś już za pomocą powyższych przykładów kodu. Korzystanie z asynchronicznych operacji we / wy jest absolutnie właściwą drogą. Async IO to sposób, w jaki Win32 jest projektowany wewnętrznie w celu skalowania. Najlepszą możliwą wydajność, jaką można uzyskać, uzyskuje się za pomocą portów ukończenia, wiążących gniazda z portami zakończenia i puli wątków oczekujących na zakończenie portu zakończenia. Powszechnie uważa się, że na ukończenie czeka od 2 do 4 wątków na procesor (rdzeń). Gorąco polecam przejrzenie tych trzech artykułów autorstwa Ricka Vicika z zespołu ds. Wydajności systemu Windows:

  1. Projektowanie aplikacji pod kątem wydajności - część 1
  2. Projektowanie aplikacji pod kątem wydajności - część 2
  3. Projektowanie aplikacji pod kątem wydajności - część 3

Artykuły te dotyczą głównie natywnego interfejsu API systemu Windows, ale są one obowiązkową lekturą dla każdego, kto próbuje pojąć skalowalność i wydajność. Mają też kilka majtek na temat zarządzania.

Drugą rzeczą, którą musisz zrobić, jest przejrzenie książki Poprawianie wydajności i skalowalności aplikacji .NET , która jest dostępna online. Odpowiednie i ważne porady dotyczące używania wątków, wywołań asynchronicznych i blokad znajdziesz w rozdziale 5. Ale prawdziwe perełki znajdują się w rozdziale 17, gdzie znajdziesz takie gadżety, jak praktyczne wskazówki dotyczące strojenia puli wątków. Moje aplikacje miały poważne problemy, dopóki nie dostosowałem maxIothreads / maxWorkerThreads zgodnie z zaleceniami w tym rozdziale.

Mówisz, że chcesz stworzyć czysty serwer TCP, więc mój następny punkt jest fałszywy. Jednakże , jeśli znajdziesz się opanowany i używać WebRequest klasę i jego pochodne, powinien być ostrzeżony, że jest smok strzeże tych drzwi: the ServicePointManager . Jest to klasa konfiguracji, która ma jeden cel w życiu: zrujnować wydajność. Upewnij się, że uwolnisz swój serwer od sztucznie narzuconego ServicePoint.ConnectionLimit, bo inaczej Twoja aplikacja nigdy się nie skaluje (pozwolę Ci odkryć, jaka jest domyślna wartość ...). Możesz również ponownie rozważyć domyślną zasadę wysyłania nagłówka Expect100Continue w żądaniach http.

Teraz, jeśli chodzi o interfejs API zarządzanego przez gniazdo rdzenia, po stronie wysyłania są dość proste, ale są znacznie bardziej złożone po stronie odbioru. Aby osiągnąć wysoką przepustowość i skalowalność, należy upewnić się, że gniazdo nie jest sterowane przepływem, ponieważ nie ma bufora wysłanego do odbioru. Idealnie, aby uzyskać wysoką wydajność, należy wysłać 3-4 bufory z wyprzedzeniem i opublikować nowe, gdy tylko jeden z nich zostanie zwrócony ( przed przetworzeniem tego, który wrócił), aby mieć pewność, że gniazdo zawsze ma gdzie zdeponować dane pochodzące z sieci. Zobaczysz, dlaczego prawdopodobnie nie będziesz w stanie tego wkrótce osiągnąć.

Po zakończeniu zabawy z BeginRead / BeginWrite API i rozpoczęciu poważnej pracy zdasz sobie sprawę, że potrzebujesz bezpieczeństwa w swoim ruchu, tj. Uwierzytelnianie i szyfrowanie ruchu NTLM / Kerberos lub przynajmniej ochrona przed naruszeniem ruchu. Sposób, w jaki to robisz, polega na korzystaniu z wbudowanego System.Net.Security.NegotiateStream (lub SslStream, jeśli chcesz przejść przez różne domeny). Oznacza to, że zamiast polegać na asynchronicznych operacjach gniazda prostego, będziesz polegać na operacjach asynchronicznych AuthenticatedStream. Gdy tylko uzyskasz gniazdo (z połączenia na kliencie lub z akceptacji na serwerze), tworzysz strumień w gnieździe i przesyłasz go do uwierzytelnienia, wywołując BeginAuthenticateAsClient lub BeginAuthenticateAsServer. Po zakończeniu uwierzytelniania (przynajmniej Twojego sejfu z natywnego szaleństwa InitiateSecurityContext / AcceptSecurityContext ...) dokonasz autoryzacji, sprawdzając właściwość RemoteIdentity swojego uwierzytelnionego strumienia i wykonując dowolną weryfikację ACL, którą Twój produkt musi obsługiwać. Następnie wyślesz wiadomości za pomocą BeginWrite i będziesz je otrzymywać za pomocą BeginRead. To jest problem, o którym mówiłem wcześniej, że nie będzie można opublikować wielu buforów odbioru, ponieważ klasy AuthenticateStream tego nie obsługują. Operacja BeginRead zarządza wewnętrznie wszystkimi operacjami we / wy do momentu odebrania całej ramki, w przeciwnym razie nie mogłaby obsłużyć uwierzytelnienia wiadomości (odszyfrować ramkę i sprawdzić podpis na ramce). Chociaż z mojego doświadczenia wynika, że ​​praca wykonana przez klasy AuthenticatedStream jest dość dobra i nie powinna mieć z tym żadnego problemu. To znaczy. powinieneś być w stanie nasycić sieć GB tylko 4-5% CPU. Klasy AuthenticatedStream narzucą również ograniczenia rozmiaru ramki specyficzne dla protokołu (16 kB dla SSL, 12 kB dla Kerberos).

To powinno pomóc Ci zacząć na dobrej drodze. Nie zamierzam tutaj pisać kodu, na MSDN jest doskonały przykład . Zrobiłem wiele takich projektów i bez problemu udało mi się skalować do około 1000 podłączonych użytkowników. Przede wszystkim musisz zmodyfikować klucze rejestru, aby umożliwić jądru dostęp do większej liczby uchwytów gniazd. i upewnij się, że wdrażasz w systemie operacyjnym serwera , czyli W2K3, a nie XP lub Vista (tj. system operacyjny klienta), to robi dużą różnicę.

BTW, upewnij się, że jeśli masz operacje na bazach danych na serwerze lub we / wy pliku, używasz również dla nich smaku asynchronicznego, w przeciwnym razie opróżnisz pulę wątków w mgnieniu oka. W przypadku połączeń SQL Server upewnij się, że do parametrów połączenia dodano „Asyncronous Processing = true”.


Jest tu kilka świetnych informacji. Chciałbym móc przyznać nagrodę wielu osobom. Jednak zagłosowałem za tobą. Dobre rzeczy, dzięki.
Erik Funkenbusch

11

Mam taki serwer działający w niektórych moich rozwiązaniach. Oto bardzo szczegółowe wyjaśnienie różnych sposobów na zrobienie tego w .net: Bliżej sieci dzięki gniazdom o wysokiej wydajności w .NET

Ostatnio szukałem sposobów na ulepszenie naszego kodu i będę się temu przyglądał: „ Ulepszenia wydajności gniazd w wersji 3.5 ”, które zostały dołączone specjalnie „do użytku przez aplikacje, które używają asynchronicznych operacji we / wy sieci w celu uzyskania najwyższej wydajności”.

„Główną cechą tych ulepszeń jest unikanie powtarzania alokacji i synchronizacji obiektów podczas asynchronicznych operacji wejścia / wyjścia gniazda o dużej objętości. Wzorzec projektowy Begin / End obecnie zaimplementowany przez klasę Socket dla asynchronicznych operacji we / wy gniazd wymaga System. Obiekt IAsyncResult zostanie przydzielony dla każdej asynchronicznej operacji gniazda. "

Możesz kontynuować czytanie, podążając za linkiem. Jutro osobiście przetestuję ich przykładowy kod, aby porównać go z tym, co mam.

Edycja: tutaj możesz znaleźć działający kod zarówno dla klienta, jak i serwera, używając nowego 3.5 SocketAsyncEventArgs, dzięki czemu możesz go przetestować w ciągu kilku minut i przejść przez kod. Jest to proste podejście, ale jest podstawą do rozpoczęcia znacznie większej implementacji. Ciekawą lekturą był również ten artykuł sprzed prawie dwóch lat w MSDN Magazine.



9

Czy rozważałeś użycie powiązania TCP netto programu WCF i wzorca publikowania / subskrypcji? WCF pozwoliłoby Ci skupić się [głównie] na Twojej domenie zamiast na hydraulice.

Istnieje wiele przykładów WCF, a nawet struktura publikowania / subskrypcji dostępna w sekcji pobierania IDesign, która może być przydatna: http://www.idesign.net


8

Zastanawiam się nad jedną rzeczą:

Zdecydowanie nie chcę rozpoczynać wątku dla każdego połączenia.

Dlaczego? Windows mógł obsłużyć setki wątków w aplikacji od co najmniej Windows 2000. Zrobiłem to, bardzo łatwo jest pracować, jeśli wątki nie muszą być synchronizowane. Zwłaszcza biorąc pod uwagę, że wykonujesz dużo operacji we / wy (więc nie jesteś związany z procesorem, a wiele wątków zostanie zablokowanych na dysku lub w komunikacji sieciowej), nie rozumiem tego ograniczenia.

Czy przetestowałeś wielowątkowość i zauważyłeś, że czegoś brakuje? Czy zamierzasz również mieć połączenie z bazą danych dla każdego wątku (co zabiłoby serwer bazy danych, więc jest to zły pomysł, ale można go łatwo rozwiązać za pomocą projektu trójwarstwowego). Martwisz się, że zamiast setek będziesz mieć tysiące klientów, a wtedy naprawdę będziesz mieć problemy? (Chociaż spróbuję tysiąca wątków, a nawet dziesięciu tysięcy, gdybym miał ponad 32 GB pamięci RAM - znowu, biorąc pod uwagę, że nie jesteś związany z procesorem, czas przełączania wątków powinien być absolutnie nieistotny.)

Oto kod - aby zobaczyć, jak to wygląda podczas działania, wejdź na http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html i kliknij zdjęcie.

Klasa serwera:

  public class Server
  {
    private static readonly TcpListener listener = new TcpListener(IPAddress.Any, 9999);

    public Server()
    {
      listener.Start();
      Console.WriteLine("Started.");

      while (true)
      {
        Console.WriteLine("Waiting for connection...");

        var client = listener.AcceptTcpClient();
        Console.WriteLine("Connected!");

        // each connection has its own thread
        new Thread(ServeData).Start(client);
      }
    }

    private static void ServeData(object clientSocket)
    {
      Console.WriteLine("Started thread " + Thread.CurrentThread.ManagedThreadId);

      var rnd = new Random();
      try
      {
        var client = (TcpClient) clientSocket;
        var stream = client.GetStream();
        while (true)
        {
          if (rnd.NextDouble() < 0.1)
          {
            var msg = Encoding.ASCII.GetBytes("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
            stream.Write(msg, 0, msg.Length);

            Console.WriteLine("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
          }

          // wait until the next update - I made the wait time so small 'cause I was bored :)
          Thread.Sleep(new TimeSpan(0, 0, rnd.Next(1, 5)));
        }
      }
      catch (SocketException e)
      {
        Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
      }
    }
  }

Główny program serwera:

namespace ManyThreadsServer
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      new Server();
    }
  }
}

Klasa klienta:

  public class Client
  {
    public Client()
    {
      var client = new TcpClient();
      client.Connect(IPAddress.Loopback, 9999);

      var msg = new byte[1024];

      var stream = client.GetStream();
      try
      {
        while (true)
        {
          int i;
          while ((i = stream.Read(msg, 0, msg.Length)) != 0)
          {
            var data = Encoding.ASCII.GetString(msg, 0, i);
            Console.WriteLine("Received: {0}", data);
          }
        }
      }
      catch (SocketException e)
      {
        Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
      }
    }
  }

Główny program klienta:

using System;
using System.Threading;

namespace ManyThreadsClient
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      // first argument is the number of threads
      for (var i = 0; i < Int32.Parse(args[0]); i++)
        new Thread(RunClient).Start();
    }

    private static void RunClient()
    {
      new Client();
    }
  }
}

Windows może obsłużyć wiele wątków, ale .NET nie jest tak naprawdę zaprojektowany do ich obsługi. Każda domena aplikacji .NET ma pulę wątków i nie chcesz wyczerpać tej puli wątków. Nie jestem pewien, czy uruchamiasz wątek ręcznie, czy pochodzi on z puli wątków, czy nie. Mimo to setki wątków nic nie robi przez większość czasu to ogromne marnotrawstwo zasobów.
Erik Funkenbusch

1
Uważam, że masz niewłaściwy pogląd na wątki. Wątki pochodzą z puli wątków tylko wtedy, gdy naprawdę tego chcesz - zwykłe wątki nie. Setki wątków nic nie marnuje dokładnie nic :) (Cóż, trochę pamięci, ale pamięć jest tak tania, że ​​to już nie problem.) Mam zamiar napisać kilka przykładowych aplikacji do tego, opublikuję adres URL to kiedy skończę. W międzyczasie proponuję powtórzyć to, co napisałem powyżej i spróbować odpowiedzieć na moje pytania.
Marcel Popescu

1
Chociaż zgadzam się z komentarzem Marcela dotyczącym poglądu wątków, że utworzone wątki nie pochodzą z puli wątków, reszta stwierdzenia jest nieprawidłowa. Pamięć nie polega na tym, ile jest zainstalowanych na komputerze, wszystkie aplikacje w systemie Windows działają w wirtualnej przestrzeni adresowej i na systemie 32-bitowym, który daje 2 GB danych dla Twojej aplikacji (nie ma znaczenia, ile pamięci RAM jest zainstalowane w pudełku). Nadal muszą być zarządzane przez środowisko wykonawcze. Wykonywanie asynchronicznych operacji we / wy nie wykorzystuje wątku do oczekiwania (używa IOCP, który pozwala na nakładające się operacje we / wy) i jest lepszym rozwiązaniem i DUŻO lepiej skaluje.
Brian ONeil

7
Przy uruchamianiu wielu wątków problemem nie jest pamięć, ale procesor. Przełączanie kontekstu między wątkami jest stosunkowo kosztowną operacją, a im więcej masz aktywnych wątków, tym więcej przełączników kontekstu ma nastąpić. Kilka lat temu przeprowadziłem test na moim komputerze z aplikacją konsolową C # i ok. 500 wątków mój procesor był w 100%, wątki nie robiły nic znaczącego. W przypadku komunikacji sieciowej lepiej jest ograniczyć liczbę wątków.
sipwiz

1
Wybrałbym rozwiązanie zadania lub użycie async / await. Rozwiązanie Task wydaje się prostsze, podczas gdy async / await są prawdopodobnie bardziej skalowalne (były przeznaczone specjalnie do sytuacji związanych z IO).
Marcel Popescu

5

Używanie zintegrowanego Async IO ( BeginReaditp.) .NET jest dobrym pomysłem, jeśli możesz uzyskać prawidłowe wszystkie szczegóły. Kiedy prawidłowo skonfigurujesz uchwyty gniazda / pliku, użyje on podstawowej implementacji IOCP systemu operacyjnego, umożliwiając wykonanie operacji bez użycia żadnych wątków (lub, w najgorszym przypadku, użycie wątku, który moim zdaniem pochodzi z puli wątków IO jądra puli wątków platformy .NET, co pomaga złagodzić przeciążenie puli wątków).

Głównym problemem jest upewnienie się, że otwierasz gniazda / pliki w trybie nieblokującym. Większość domyślnych funkcji wygodnych (takich jak File.OpenRead) tego nie robi, więc musisz napisać własne.

Jednym z innych głównych problemów jest obsługa błędów - prawidłowa obsługa błędów podczas pisania asynchronicznego kodu we / wy jest znacznie, dużo trudniejsza niż robienie tego w kodzie synchronicznym. Bardzo łatwo jest również skończyć z warunkami wyścigu i zakleszczeniem, nawet jeśli nie używasz bezpośrednio wątków, więc musisz być tego świadomy.

Jeśli to możliwe, powinieneś spróbować użyć wygodnej biblioteki, aby ułatwić proces wykonywania skalowalnych asynchronicznych operacji we / wy.

Microsoft Concurrency Coordination Runtime jest jednym z przykładów biblioteki .NET zaprojektowanej w celu ułatwienia wykonywania tego rodzaju programowania. Wygląda świetnie, ale ponieważ go nie używałem, nie mogę komentować, jak dobrze by się skalował.

Dla moich projektów, które trzeba zrobić asynchroniczne sieci lub dysku I / O, używam zestawu narzędzi .NET współbieżności / IO, że został zbudowany w ciągu ostatniego roku, zwany Squared.Task . Jest inspirowany bibliotekami, takimi jak imvu.task i twisted , a w repozytorium umieściłem kilka działających przykładów, które wykonują sieciowe I / O. Użyłem go również w kilku napisanych przeze mnie aplikacjach - największą publicznie wydaną jest NDexer (który używa go do bezgwintowego wejścia / wyjścia dysku). Biblioteka została napisana w oparciu o moje doświadczenia z imvu.task i posiada zestaw dość obszernych testów jednostkowych, więc gorąco zachęcam do jej wypróbowania. Jeśli masz z tym jakieś problemy, chętnie Ci pomogę.

Moim zdaniem, opierając się na moim doświadczeniu, używanie asynchronicznych / bezgwintowych operacji IO zamiast wątków jest opłacalnym przedsięwzięciem na platformie .NET, o ile jesteś gotowy, aby poradzić sobie z krzywą uczenia się. Pozwala uniknąć problemów ze skalowalnością narzucanych przez koszt obiektów Thread, aw wielu przypadkach można całkowicie uniknąć używania blokad i muteksów, ostrożnie wykorzystując prymitywy współbieżności, takie jak Futures / Promises.


Świetna informacja, sprawdzę Twoje referencje i zobaczę, co ma sens.
Erik Funkenbusch

3

Użyłem rozwiązania Kevina, ale on mówi, że w rozwiązaniu brakuje kodu do ponownego składania wiadomości. Programiści mogą użyć tego kodu do ponownego składania wiadomości:

private static void ReceiveCallback(IAsyncResult asyncResult )
{
    ClientInfo cInfo = (ClientInfo)asyncResult.AsyncState;

    cInfo.BytesReceived += cInfo.Soket.EndReceive(asyncResult);
    if (cInfo.RcvBuffer == null)
    {
        // First 2 byte is lenght
        if (cInfo.BytesReceived >= 2)
        {
            //this calculation depends on format which your client use for lenght info
            byte[] len = new byte[ 2 ] ;
            len[0] = cInfo.LengthBuffer[1];
            len[1] = cInfo.LengthBuffer[0];
            UInt16 length = BitConverter.ToUInt16( len , 0);

            // buffering and nulling is very important
            cInfo.RcvBuffer = new byte[length];
            cInfo.BytesReceived = 0;

        }
    }
    else
    {
        if (cInfo.BytesReceived == cInfo.RcvBuffer.Length)
        {
             //Put your code here, use bytes comes from  "cInfo.RcvBuffer"

             //Send Response but don't use async send , otherwise your code will not work ( RcvBuffer will be null prematurely and it will ruin your code)

            int sendLenghts = cInfo.Soket.Send( sendBack, sendBack.Length, SocketFlags.None);

            // buffering and nulling is very important
            //Important , set RcvBuffer to null because code will decide to get data or 2 bte lenght according to RcvBuffer's value(null or initialized)
            cInfo.RcvBuffer = null;
            cInfo.BytesReceived = 0;
        }
    }

    ContinueReading(cInfo);
 }

private static void ContinueReading(ClientInfo cInfo)
{
    try 
    {
        if (cInfo.RcvBuffer != null)
        {
            cInfo.Soket.BeginReceive(cInfo.RcvBuffer, cInfo.BytesReceived, cInfo.RcvBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
        }
        else
        {
            cInfo.Soket.BeginReceive(cInfo.LengthBuffer, cInfo.BytesReceived, cInfo.LengthBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
        }
    }
    catch (SocketException se)
    {
        //Handle exception and  Close socket here, use your own code 
        return;
    }
    catch (Exception ex)
    {
        //Handle exception and  Close socket here, use your own code 
        return;
    }
}

class ClientInfo
{
    private const int BUFSIZE = 1024 ; // Max size of buffer , depends on solution  
    private const int BUFLENSIZE = 2; // lenght of lenght , depends on solution
    public int BytesReceived = 0 ;
    public byte[] RcvBuffer { get; set; }
    public byte[] LengthBuffer { get; set; }

    public Socket Soket { get; set; }

    public ClientInfo(Socket clntSock)
    {
        Soket = clntSock;
        RcvBuffer = null;
        LengthBuffer = new byte[ BUFLENSIZE ];
    }   

}

public static void AcceptCallback(IAsyncResult asyncResult)
{

    Socket servSock = (Socket)asyncResult.AsyncState;
    Socket clntSock = null;

    try
    {

        clntSock = servSock.EndAccept(asyncResult);

        ClientInfo cInfo = new ClientInfo(clntSock);

        Receive( cInfo );

    }
    catch (SocketException se)
    {
        clntSock.Close();
    }
}
private static void Receive(ClientInfo cInfo )
{
    try
    {
        if (cInfo.RcvBuffer == null)
        {
            cInfo.Soket.BeginReceive(cInfo.LengthBuffer, 0, 2, SocketFlags.None, ReceiveCallback, cInfo);

        }
        else
        {
            cInfo.Soket.BeginReceive(cInfo.RcvBuffer, 0, cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);

        }

    }
    catch (SocketException se)
    {
        return;
    }
    catch (Exception ex)
    {
        return;
    }

}


1

Możesz spróbować użyć frameworka o nazwie ACE (Adaptive Communications Environment), który jest ogólnym frameworkiem C ++ dla serwerów sieciowych. Jest to bardzo solidny, dojrzały produkt, zaprojektowany do obsługi aplikacji o wysokiej niezawodności i dużej objętości, aż do klasy telekomunikacyjnej.

Framework obsługuje dość szeroką gamę modeli współbieżności i prawdopodobnie ma jeden odpowiedni dla twojej aplikacji po wyjęciu z pudełka. Powinno to ułatwić debugowanie systemu, ponieważ większość nieprzyjemnych problemów ze współbieżnością została już rozwiązana. Kompromisem jest to, że framework jest napisany w C ++ i nie jest najbardziej ciepłą i puszystą bazą kodu. Z drugiej strony otrzymujesz przetestowaną infrastrukturę sieciową klasy przemysłowej i wysoce skalowalną architekturę po wyjęciu z pudełka.


2
To dobra sugestia, ale z tagów w pytaniu uważam, że OP będzie używać C #
JPCosta

Zauważyłem to; sugestia była taka, że ​​jest to dostępne dla C ++ i nie znam niczego równoważnego dla C #. Debugowanie tego rodzaju systemu nie jest łatwe w najlepszych momentach i możesz uzyskać zwrot z przejścia do tego frameworka, nawet jeśli oznacza to przejście na C ++.
ConcernedOfTunbridgeWells

Tak, to jest C #. Szukam dobrych rozwiązań opartych na .net. Powinienem był być bardziej jasny, ale założyłem, że ludzie będą czytać tagi
Erik Funkenbusch


1

Cóż, gniazda .NET wydają się udostępniać metodę select () - to jest najlepsze do obsługi danych wejściowych. Dla danych wyjściowych miałbym pulę wątków zapisujących gniazdo nasłuchujących w kolejce roboczej, akceptujących deskryptor / obiekt gniazda jako część elementu roboczego, więc nie potrzebujesz wątku na gniazdo.


1

Użyłbym metod AcceptAsync / ConnectAsync / ReceiveAsync / SendAsync, które zostały dodane w .Net 3.5. Zrobiłem test porównawczy i są one około 35% szybsze (czas odpowiedzi i szybkość transmisji), a 100 użytkowników stale wysyła i odbiera dane.


1

aby ludzie kopiowali wklejając zaakceptowaną odpowiedź, możesz przepisać metodę acceptCallback, usuwając wszystkie wywołania _serverSocket.BeginAccept (nowy AsyncCallback (acceptCallback), _serverSocket); i umieść go w klauzuli last {}, w ten sposób:

private void acceptCallback(IAsyncResult result)
    {
       xConnection conn = new xConnection();
       try
       {
         //Finish accepting the connection
         System.Net.Sockets.Socket s = (System.Net.Sockets.Socket)result.AsyncState;
         conn = new xConnection();
         conn.socket = s.EndAccept(result);
         conn.buffer = new byte[_bufferSize];
         lock (_sockets)
         {
           _sockets.Add(conn);
         }
         //Queue recieving of data from the connection
         conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
       }
       catch (SocketException e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
       }
       catch (Exception e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
       }
       finally
       {
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);       
       }
     }

możesz nawet usunąć pierwszy haczyk, ponieważ jego zawartość jest taka sama, ale jest to metoda szablonowa i powinieneś użyć wpisanego wyjątku, aby lepiej obsłużyć wyjątki i zrozumieć, co spowodowało błąd, więc po prostu zaimplementuj te połowy za pomocą przydatnego kodu



-1

Żeby było jasne, szukam rozwiązań opartych na .net (C #, jeśli to możliwe, ale każdy język .net będzie działał)

Nie osiągniesz najwyższego poziomu skalowalności, jeśli zdecydujesz się wyłącznie na .NET. Przerwy w GC mogą zmniejszyć opóźnienie.

Muszę uruchomić co najmniej jeden wątek dla usługi. Rozważam użycie Asynch API (BeginRecieve itp.), Ponieważ nie wiem, ilu klientów będę podłączać w danym momencie (prawdopodobnie setki). Zdecydowanie nie chcę rozpoczynać wątku dla każdego połączenia.

Nakładające się operacje we / wy jest ogólnie uważane za najszybszy interfejs API systemu Windows do komunikacji sieciowej. Nie wiem, czy to jest to samo, co twój Asynch API. Nie używaj funkcji select, ponieważ każde wywołanie musi sprawdzić każde otwarte gniazdo zamiast wywołań zwrotnych w aktywnych gniazdach.


1
Nie rozumiem twojego komentarza pauzy GC. Nigdy nie widziałem systemu z problemami ze skalowalnością, który byłby bezpośrednio związany z GC.
markt

4
O wiele bardziej prawdopodobne jest, że utworzysz aplikację, której nie można skalować z powodu złej architektury, niż z powodu istnienia GC. Ogromne, skalowalne i wydajne systemy zostały zbudowane przy użyciu platformy .NET i Java. W obu linkach, które podałeś, przyczyną nie było bezpośrednio wyrzucanie elementów bezużytecznych .., ale wymiana sterty. Podejrzewam, że to naprawdę problem z architekturą, którego można było uniknąć .. Jeśli pokażesz mi język, w którym nie da się zbudować systemu, który nie da się skalować, chętnie z niego skorzystam;)
markt

1
Nie zgadzam się z tym komentarzem. Nieznane, pytania, do których się odnosisz, dotyczą języka Java i dotyczą w szczególności większych alokacji pamięci i prób ręcznego wymuszenia gc. Tak naprawdę nie zamierzam mieć tutaj ogromnych ilości przydziału pamięci. To po prostu nie jest problem. Ale dzięki. Tak, model programowania asynchronicznego jest zwykle implementowany w górnej części nakładających się operacji we / wy.
Erik Funkenbusch

1
Właściwie najlepszą praktyką nie jest ciągłe ręczne zmuszanie GC do zbierania. Może to bardzo pogorszyć działanie Twojej aplikacji. NET GC to generacyjna lista kontrolna, która dostosowuje się do użycia aplikacji. Jeśli naprawdę myślisz, że musisz ręcznie wywoływać GC.Collect, powiedziałbym, że Twój kod najprawdopodobniej musi zostać napisany w inny sposób ..
markt

1
@markt, to komentarz dla osób, które tak naprawdę nie wiedzą nic o zbieraniu śmieci. Jeśli masz czas bezczynności, nie ma nic złego w ręcznym zbieraniu. Nie pogorszy to twojej aplikacji, kiedy się zakończy. Artykuły naukowe pokazują, że pokoleniowe GC działają, ponieważ jest to przybliżenie czasu życia twoich obiektów. Oczywiście nie jest to idealne przedstawienie. W rzeczywistości istnieje paradoks, w którym „najstarsze” pokolenie często ma najwyższy wskaźnik śmieci, ponieważ nigdy nie są one zbierane.
Nieznany

-1

Możesz użyć platformy Open Source Push Framework do tworzenia wysokowydajnych serwerów. Jest zbudowany na IOCP i nadaje się do scenariuszy wypychania i transmisji wiadomości.

http://www.pushframework.com


1
Ten post został oznaczony C # i .net. Dlaczego zaproponowałeś framework C ++?
Erik Funkenbusch

Prawdopodobnie dlatego, że to napisał. potatosoftware.com/…
quillbreaker

czy pushframework obsługuje wiele instancji serwera? jeśli nie, jak to się skaluje?
esskar
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.