Come scrivere un server basato su Tcp / Ip scalabile


148

Sono in fase di progettazione di scrivere una nuova applicazione del servizio Windows che accetta connessioni TCP / IP per connessioni a esecuzione prolungata (ovvero non è come HTTP dove ci sono molte connessioni brevi, ma piuttosto un client si connette e rimane connesso per ore o giorni o settimane pari).

Sto cercando idee per il modo migliore per progettare l'architettura di rete. Ho bisogno di iniziare almeno un thread per il servizio. Sto pensando di utilizzare l'API Asynch (BeginRecieve, ecc.) Poiché non so quanti client avrò connesso in un determinato momento (forse centinaia). Sicuramente non voglio iniziare un thread per ogni connessione.

I dati verranno principalmente inviati ai client dal mio server, ma a volte verranno inviati alcuni comandi dai client. Questa è principalmente un'applicazione di monitoraggio in cui il mio server invia periodicamente dati di stato ai client.

Qualche suggerimento sul modo migliore per renderlo il più scalabile possibile? Flusso di lavoro di base? Grazie.

EDIT: Per essere chiari, sto cercando soluzioni basate su .net (se possibile, C #, ma funzionerà qualsiasi linguaggio .net)

NOTA SULLA TAGLIA: per ottenere la generosità, mi aspetto più di una semplice risposta. Avrei bisogno di un esempio funzionante di una soluzione, sia come puntatore a qualcosa che potrei scaricare sia come breve esempio in linea. E deve essere basato su .net e Windows (qualsiasi linguaggio .net è accettabile)

EDIT: voglio ringraziare tutti coloro che hanno dato buone risposte. Sfortunatamente, ho potuto accettarne solo uno e ho scelto di accettare il metodo Begin / End più noto. La soluzione di Esac potrebbe anche essere migliore, ma è ancora abbastanza nuova da non sapere con certezza come funzionerà.

Ho valutato tutte le risposte che ho pensato fossero buone, vorrei poter fare di più per voi ragazzi. Grazie ancora.


1
Sei assolutamente sicuro che debba essere una connessione di lunga durata? È difficile dirlo dalle informazioni limitate fornite, ma lo farei solo se assolutamente necessario ..
Markt

Sì, deve essere di lunga durata. I dati devono essere aggiornati in tempo reale, quindi non posso eseguire il polling periodico, i dati devono essere inviati al client non appena si verifica, il che significa una connessione costante.
Erik Funkenbusch,

1
Questo non è un motivo valido. Http supporta bene connessioni a lungo termine. Basta aprire una connessione e attendere una risposta (polling bloccato). Funziona bene con molte app in stile AJAX ecc. Come pensi che funzioni Gmail :-)
TFD

2
Gmail funziona periodicamente eseguendo il polling per la posta elettronica, non mantiene una connessione a lungo in esecuzione. Questo va bene per la posta elettronica, dove non è richiesta una risposta in tempo reale.
Erik Funkenbusch,

2
Polling o pull si ridimensiona bene ma sviluppa rapidamente la latenza. Anche il push non si ridimensiona, ma aiuta a ridurre o eliminare la latenza.
andrewbadera,

Risposte:


92

Ho scritto qualcosa di simile a questo in passato. Dalla mia ricerca di anni fa ho dimostrato che scrivere la propria implementazione di socket era la soluzione migliore, usando i socket asincroni. Ciò significava che i clienti che non facevano davvero nulla richiedevano risorse relativamente ridotte. Tutto ciò che accade viene gestito dal pool di thread .net.

L'ho scritto come una classe che gestisce tutte le connessioni per i server.

Ho semplicemente usato un elenco per contenere tutte le connessioni client, ma se hai bisogno di ricerche più veloci per elenchi più grandi, puoi scriverlo come preferisci.

private List<xConnection> _sockets;

Inoltre è necessario che il socket ascolti effettivamente le connessioni in entrata.

private System.Net.Sockets.Socket _serverSocket;

Il metodo start in realtà avvia il socket del server e inizia ad ascoltare eventuali connessioni in entrata.

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;
 }

Vorrei solo notare che il codice di gestione delle eccezioni sembra male, ma il motivo è che avevo un codice di soppressione delle eccezioni lì in modo che eventuali eccezioni sarebbero state soppresse e restituite falsese fosse stata impostata un'opzione di configurazione, ma volevo rimuoverla per per brevità.

_ServerSocket.BeginAccept (new AsyncCallback (acceptCallback)), _serverSocket) sopra imposta essenzialmente il nostro socket del server per chiamare il metodo acceptCallback ogni volta che un utente si connette. Questo metodo viene eseguito dal pool di thread .Net, che gestisce automaticamente la creazione di thread di lavoro aggiuntivi in ​​caso di numerose operazioni di blocco. Questo dovrebbe gestire in modo ottimale qualsiasi carico sul server.

    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);
       }
     }

Il codice sopra essenzialmente ha appena finito di accettare la connessione in arrivo, le code BeginReceiveche è un callback che verrà eseguito quando il client invia i dati e quindi accoda il successivo acceptCallbackche accetterà la successiva connessione client che arriva.

La BeginReceivechiamata del metodo è ciò che dice al socket cosa fare quando riceve i dati dal client. Per BeginReceive, è necessario dargli un array di byte, che è dove copierà i dati quando il client invia i dati. Il ReceiveCallbackmetodo verrà chiamato, ed è così che gestiamo la ricezione dei dati.

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);
       }
     }
   }
 }

EDIT: In questo schema ho dimenticato di menzionare che in questa area di codice:

//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);

Quello che farei generalmente è nel codice che desideri, è riassemblare i pacchetti in messaggi e quindi crearli come lavori nel pool di thread. In questo modo BeginReceive del blocco successivo dal client non viene ritardato mentre è in esecuzione il codice di elaborazione dei messaggi.

Il callback accetta termina la lettura del socket dei dati chiamando end reception. In questo modo viene riempito il buffer fornito nella funzione di ricezione iniziale. Una volta fatto quello che vuoi dove ho lasciato il commento, chiamiamo il BeginReceivemetodo successivo che eseguirà nuovamente il callback se il client invia altri dati. Ora ecco la parte davvero complicata, quando il client invia dati, il callback di ricezione potrebbe essere chiamato solo con parte del messaggio. Il riassemblaggio può diventare molto complicato. Ho usato il mio metodo e ho creato una sorta di protocollo proprietario per farlo. L'ho lasciato fuori, ma se lo richiedi, posso aggiungerlo. Questo gestore era in realtà il pezzo di codice più complicato che avessi mai scritto.

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;
 }

Il metodo di invio sopra in realtà utilizza una Sendchiamata sincrona , per me andava bene a causa delle dimensioni del messaggio e della natura multithread della mia applicazione. Se desideri inviare a tutti i client, devi semplicemente scorrere l'elenco _socket.

La classe xConnection che vedi sopra citata è fondamentalmente un semplice wrapper per un socket per includere il buffer di byte, e nella mia implementazione alcuni extra.

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

Anche per riferimento qui ci sono le usings che includo poiché mi annoio sempre quando non sono incluse.

using System.Net.Sockets;

Spero che sia utile, potrebbe non essere il codice più pulito, ma funziona. Ci sono anche alcune sfumature nel codice che dovresti essere stanco di cambiare. Per uno, solo un singolo BeginAcceptchiamato alla volta. C'era un bug molto fastidioso attorno a questo, che era anni fa, quindi non ricordo i dettagli.

Inoltre, nel ReceiveCallbackcodice, elaboriamo qualsiasi cosa ricevuta dal socket prima di mettere in coda la ricezione successiva. Questo significa che per un singolo socket, siamo effettivamente sempre e solo ReceiveCallbackuna volta in qualsiasi momento e non abbiamo bisogno di usare la sincronizzazione dei thread. Tuttavia, se riordini questo per chiamare la ricezione successiva immediatamente dopo aver estratto i dati, che potrebbe essere un po 'più veloce, dovrai assicurarti di sincronizzare correttamente i thread.

Inoltre, ho hackerato molto del mio codice, ma ho lasciato l'essenza di ciò che sta accadendo sul posto. Questo dovrebbe essere un buon inizio per il tuo design. Lascia un commento se hai altre domande al riguardo.


1
Questa è una buona risposta Kevin .. sembra che tu sia sulla buona strada per ottenere la taglia. :)
Erik Funkenbusch,

6
Non so perché questa sia la risposta più votata. Begin * End * non è il modo più veloce di fare networking in C #, né il più altamente scalabile. È più veloce del sincrono, ma ci sono molte operazioni che si svolgono sotto il cofano in Windows che rallentano davvero questo percorso di rete.
esac,

6
Ricorda cosa ha scritto esac nel commento precedente. Il modello di inizio probabilmente funzionerà per te fino a un certo punto, diamine il mio codice sta attualmente usando inizio-fine, ma ci sono miglioramenti ai suoi limiti in .net 3.5. Non mi interessa la generosità, ma consiglierei di leggere il link nella mia risposta anche se si implementa questo approccio. "Socket Performance Enhancements in Version 3.5"
jvanderh,

1
Volevo solo inserirli dal momento che forse non ero stato abbastanza chiaro, questo è il codice dell'era .net 2.0 in cui credo che questo fosse un modello molto praticabile. Tuttavia, la risposta di esac sembra essere un po 'più moderna se si rivolge a .net 3.5, l'unico nitpick che ho è il lancio di eventi :) ma che può essere facilmente modificato. Inoltre, ho eseguito i test di throughput con questo codice e su un opteron dual core 2 Ghz è stato in grado di massimizzare Ethernet a 100 Mbps e questo ha aggiunto un livello di crittografia in cima a questo codice.
Kevin Nisbet,

1
@KevinNisbet So che è abbastanza tardi, ma per chiunque utilizzi questa risposta per progettare i propri server - l'invio dovrebbe anche essere asincrono, perché altrimenti ti apri per la possibilità di un deadlock. Se entrambi i lati scrivono dati che riempiono i rispettivi buffer, i Sendmetodi si bloccheranno indefinitamente su entrambi i lati, perché non c'è nessuno che legge i dati di input.
Luaan,

83

Esistono molti modi per eseguire operazioni di rete in C #. Tutti usano meccanismi diversi sotto il cofano e quindi soffrono di problemi di prestazioni importanti con un'alta concorrenza. Le operazioni Begin * sono una di queste che molte persone confondono spesso per essere il modo più veloce / veloce di fare rete.

Per risolvere questi problemi, hanno introdotto il set di metodi * Async: da MSDN http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

La classe SocketAsyncEventArgs fa parte di una serie di miglioramenti alla classe Socket System.Net.Sockets .. ::. Che forniscono un modello asincrono alternativo che può essere utilizzato da applicazioni socket specializzate ad alte prestazioni. Questa classe è stata appositamente progettata per applicazioni server di rete che richiedono prestazioni elevate. Un'applicazione può utilizzare il modello asincrono potenziato esclusivamente o solo in aree attive mirate (ad esempio, quando si ricevono grandi quantità di dati).

La caratteristica principale di questi miglioramenti è l'eliminazione dell'allocazione e della sincronizzazione ripetute di oggetti durante l'I / O socket asincrono ad alto volume. Il modello di progettazione Begin / End attualmente implementato dalla classe System.Net.Sockets .. ::. Socket richiede che un oggetto System .. ::. IAsyncResult sia allocato per ogni operazione socket asincrona.

Sotto le copertine, l'API * Async utilizza le porte di completamento IO che è il modo più veloce per eseguire operazioni di rete, vedere http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

E solo per aiutarti, sto includendo il codice sorgente per un server Telnet che ho scritto usando l'API * Async. Includo solo le parti pertinenti. Inoltre, invece di elaborare i dati in linea, scelgo invece di inserirli in una coda senza blocco (attesa gratuita) che viene elaborata su un thread separato. Si noti che non sto includendo la corrispondente classe Pool che è solo un semplice pool che creerà un nuovo oggetto se è vuoto e la classe Buffer che è solo un buffer autoespandibile che non è realmente necessario a meno che non si riceva un indeterministico quantità di dati. Se desideri ulteriori informazioni, non esitare a inviarmi un 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
    }
}

Questo è piuttosto semplice e un semplice esempio. Grazie. Dovrò valutare i pro e i contro di ogni metodo.
Erik Funkenbusch,

Non ho avuto la possibilità di provarlo, ma sto provando la vaga sensazione di una condizione di gara qui per qualche motivo. In primo luogo, se ricevi molti messaggi, non so che gli eventi verranno elaborati in ordine (potrebbe non essere importante per l'app degli utenti, ma dovrebbe essere notato) o potrei sbagliarmi e gli eventi verranno elaborati in ordine. In secondo luogo, potrei averlo perso, ma non c'è il rischio che il buffer venga sovrascritto mentre il DataReceived è ancora in esecuzione se impiega molto tempo? Se vengono affrontate queste preoccupazioni forse ingiustificate, penso che questa sia un'ottima soluzione moderna.
Kevin Nisbet,

1
Nel mio caso, per il mio server Telnet, 100%, SÌ sono in ordine. La chiave sta impostando il metodo di callback corretto prima di chiamare AcceptAsync, ReceiveAsync, ecc. Nel mio caso faccio SendAsync su un thread separato, quindi se questo viene modificato per fare un modello Accept / Send / Ricevi / Send / Ricevi / Disconnect dovrà essere modificato.
esac,

1
Anche il punto 2 è qualcosa che dovrai prendere in considerazione. Sto memorizzando il mio oggetto "Connessione" nel contesto SocketAsyncEventArgs. Ciò significa che ho solo un buffer di ricezione per connessione. Non sto pubblicando un'altra ricezione con questo SocketAsyncEventArgs fino al completamento di DataReceived, quindi non è possibile leggere ulteriori dati su questo fino al completamento. CONSIGLIO che non è necessario effettuare lunghe operazioni su questi dati. In realtà sposto l'intero buffer di tutti i dati ricevuti su una coda senza blocco e quindi lo elaboro su un thread separato. Ciò garantisce una bassa latenza sulla porzione di rete.
esac,

1
In una nota a margine, ho scritto test unitari e test di carico per questo codice e mentre aumentavo il carico utente da 1 utente a 250 utenti (su un singolo sistema dual core, 4 GB di RAM), il tempo di risposta per 100 byte (1 pacchetto) e 10000 byte (3 pacchetti) sono rimasti invariati per tutta la curva di carico dell'utente.
esac,

46

C'era una buona discussione sul TCP / IP scalabile usando .NET scritto da Chris Mullins di Coversant, sfortunatamente sembra che il suo blog sia scomparso dalla sua posizione precedente, quindi cercherò di mettere insieme i suoi consigli dalla memoria (alcuni commenti utili dei suoi apparire in questo thread: C ++ vs. C #: sviluppo di un server IOCP altamente scalabile )

Innanzitutto, si noti che entrambi utilizzano Begin/End che i Asyncmetodi della Socketclasse utilizzano porte di completamento IO (IOCP) per fornire scalabilità. Questo fa una differenza molto più grande (se usato correttamente; vedi sotto) per la scalabilità rispetto a quale dei due metodi effettivamente scelti per implementare la soluzione.

I post di Chris Mullins erano basati sull'utilizzo Begin/End, che è quello con cui ho personalmente esperienza. Si noti che Chris ha messo insieme una soluzione basata su questo che ha scalato fino a 10.000 connessioni client simultanee su una macchina a 32 bit con 2 GB di memoria e ben 100.000 su una piattaforma a 64 bit con memoria sufficiente. Dalla mia esperienza con questa tecnica (anche se non vicino a questo tipo di carico) non ho motivo di dubitare di queste cifre indicative.

IOCP contro thread per connessione o primitive "seleziona"

Il motivo per cui si desidera utilizzare un meccanismo che utilizza IOCP sotto il cofano è che utilizza un pool di thread di Windows di livello molto basso che non attiva alcun thread fino a quando non ci sono dati effettivi sul canale IO da cui si sta tentando di leggere ( si noti che IOCP può essere utilizzato anche per il file IO). Il vantaggio di questo è che Windows non deve passare a un thread solo per scoprire che non ci sono ancora dati, quindi ciò riduce il numero di switch di contesto che il server dovrà effettuare al minimo indispensabile.

Il cambio di contesto è ciò che sicuramente ucciderà il meccanismo "thread per connessione", sebbene questa sia una soluzione praticabile se si ha a che fare solo con poche decine di connessioni. Questo meccanismo tuttavia non è affatto "scalabile" dell'immaginazione.

Considerazioni importanti quando si utilizza IOCP

Memoria

Innanzitutto è fondamentale capire che IOCP può facilmente causare problemi di memoria in .NET se l'implementazione è troppo ingenua. Ogni BeginReceivechiamata IOCP comporterà il "blocco" del buffer in cui stai leggendo. Per una buona spiegazione del perché questo è un problema, vedere: Weblog Yun Jin: OutOfMemoryException and Pinning .

Fortunatamente questo problema può essere evitato, ma richiede un po 'di compromesso. La soluzione suggerita è quella di allocare un grandebyte[] buffer all'avvio dell'applicazione (o vicino ad esso), di almeno 90 KB circa (a partire da .NET 2, la dimensione richiesta potrebbe essere maggiore nelle versioni successive). La ragione per farlo è che le allocazioni di memoria di grandi dimensioni finiscono automaticamente in un segmento di memoria non compattante (The Large Object Heap) che viene effettivamente bloccato automaticamente. Assegnando un buffer di grandi dimensioni all'avvio, ci si assicura che questo blocco di memoria immobile si trovi a un indirizzo relativamente "basso", dove non si frapporrà e causerà la frammentazione.

È quindi possibile utilizzare gli offset per segmentare questo grande buffer in aree separate per ogni connessione che deve leggere alcuni dati. È qui che entra in gioco un compromesso; poiché questo buffer deve essere pre-allocato, dovrai decidere di quanto spazio buffer hai bisogno per connessione e quale limite superiore vuoi impostare sul numero di connessioni che vuoi ridimensionare (o, puoi implementare un'astrazione che può allocare buffer aggiunti aggiuntivi una volta necessari).

La soluzione più semplice sarebbe quella di assegnare a ogni connessione un singolo byte con un offset univoco all'interno di questo buffer. Quindi è possibile effettuare una BeginReceivechiamata per la lettura di un singolo byte ed eseguire il resto della lettura come risultato della richiamata ricevuta.

in lavorazione

Quando si ottiene il callback dalla Beginchiamata effettuata, è molto importante rendersi conto che il codice nel callback verrà eseguito sul thread IOCP di basso livello. È assolutamente essenziale evitare lunghe operazioni in questo callback. L'uso di questi thread per elaborazioni complesse eliminerà la tua scalabilità con la stessa efficacia dell'utilizzo di "thread per connessione".

La soluzione suggerita è quella di utilizzare il callback solo per mettere in coda un oggetto di lavoro per elaborare i dati in arrivo, che verranno eseguiti su qualche altro thread. Evitare qualsiasi operazione potenzialmente bloccante all'interno del callback in modo che il thread IOCP possa tornare al suo pool il più rapidamente possibile. In .NET 4.0 suggerirei che la soluzione più semplice è quella di generare un Task, dandogli un riferimento al socket client e una copia del primo byte che era già stato letto dalla BeginReceivechiamata. Questa attività è quindi responsabile della lettura di tutti i dati dal socket che rappresentano la richiesta che si sta elaborando, eseguendola e quindi effettuando una nuova BeginReceivechiamata per mettere nuovamente in coda il socket per IOCP. Prima di .NET 4.0, è possibile utilizzare ThreadPool o creare la propria implementazione della coda di lavoro thread.

Sommario

Fondamentalmente, suggerirei di utilizzare il codice di esempio di Kevin per questa soluzione, con i seguenti avvisi aggiunti:

  • Assicurarsi che il buffer a cui si passa BeginReceivesia già "bloccato"
  • Assicurarsi che il callback a cui si passa BeginReceivenon faccia altro che mettere in coda un'attività per gestire l'effettiva elaborazione dei dati in arrivo

Quando lo fai, non ho dubbi che potresti replicare i risultati di Chris aumentando potenzialmente fino a centinaia di migliaia di client simultanei (dato l'hardware giusto e un'implementazione efficiente del tuo codice di elaborazione del corso;)


1
Per aggiungere un blocco di memoria più piccolo, è possibile utilizzare il metodo Alloc dell'oggetto GCHandle per bloccare il buffer. Una volta fatto ciò, UnsafeAddrOfPinnedArrayElement dell'oggetto Marshal può essere usato per ottenere un puntatore al buffer. Ad esempio: GCHandle gchTheCards = GCHandle.Alloc (TheData, GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement (TheData, 0); (sbyte *) pTheData = (sbyte *) pAddr.ToPointer ();
Bob Bryan,

@BobBryan A meno che non mi perda un punto sottile che stai cercando di chiarire, quell'approccio non aiuta in realtà con il problema che la mia soluzione sta cercando di risolvere allocando blocchi di grandi dimensioni, vale a dire il potenziale per una drammatica frammentazione della memoria inerente all'allocazione ripetuta di piccoli blocchi appuntati di memoria.
jerryjvl,

Bene, il punto è che non è necessario allocare un blocco di grandi dimensioni per tenerlo bloccato in memoria. È possibile allocare blocchi più piccoli e utilizzare la tecnica sopra descritta per bloccarli in memoria per evitare che gc li sposti. È possibile mantenere un riferimento a ciascuno dei blocchi più piccoli, proprio come se si mantenga un riferimento a un singolo blocco più grande e riutilizzarli secondo necessità. Entrambi gli approcci sono validi - stavo solo sottolineando che non è necessario utilizzare un buffer molto grande. Ma, detto questo, a volte usare un buffer molto grande è il modo migliore di procedere poiché il GC lo tratterà in modo più efficiente.
Bob Bryan,

@BobBryan poiché il blocco del buffer avviene automaticamente quando si chiama BeginReceive, il blocco non è in realtà il punto saliente qui; l'efficienza era;) ... e questo è particolarmente preoccupante quando si tenta di scrivere un server scalabile, quindi la necessità di allocare blocchi di grandi dimensioni da utilizzare per lo spazio buffer.
jerryjvl,

@jerryjvl Mi dispiace sollevare una domanda molto vecchia, tuttavia ho recentemente scoperto questo esatto problema con i metodi asincroni BeginXXX / EndXXX. Questo è un ottimo post, ma ci sono voluti molti scavi per trovarlo. Mi piace la tua soluzione suggerita ma non ne capisco una parte: "Quindi puoi effettuare una chiamata BeginReceive per un singolo byte da leggere ed eseguire il resto della lettura come risultato del callback che ricevi." Cosa intendi con il resto della preparazione come risultato della richiamata che ricevi?
Mausimo,

22

Hai già ottenuto la maggior parte della risposta tramite gli esempi di codice sopra. L'uso dell'operazione asincrona di I / O è assolutamente la strada da percorrere qui. Async IO è il modo in cui Win32 è progettato internamente per scalare. Le migliori prestazioni possibili che si ottengono si ottengono utilizzando le porte di completamento, associando i socket alle porte di completamento e disponendo di un pool di thread in attesa del completamento della porta di completamento. La saggezza comune è di avere 2-4 thread per CPU (core) in attesa di completamento. Consiglio vivamente di leggere questi tre articoli di Rick Vicik del team Performance di Windows:

  1. Progettazione di applicazioni per prestazioni - Parte 1
  2. Progettazione di applicazioni per prestazioni - Parte 2
  3. Progettazione di applicazioni per prestazioni - Parte 3

Gli articoli citati riguardano principalmente l'API nativa di Windows, ma sono indispensabili per chiunque cerchi di capire la scalabilità e le prestazioni. Hanno anche alcuni brief sul lato gestito delle cose.

La seconda cosa che devi fare è assicurarti di andare oltre Migliorare le prestazioni e la scalabilità delle applicazioni .NET libro , disponibile online. Troverai consigli pertinenti e validi sull'uso di thread, chiamate asincrone e blocchi nel Capitolo 5. Ma le vere gemme sono nel Capitolo 17 dove troverai chicche come una guida pratica sull'ottimizzazione del tuo pool di thread. Le mie app hanno avuto alcuni seri problemi fino a quando non ho modificato maxIothreads / maxWorkerThreads secondo le raccomandazioni di questo capitolo.

Dici che vuoi fare un server TCP puro, quindi il mio prossimo punto è falso. Tuttavia , se ti trovi in ​​difficoltà e usi la classe WebRequest e i suoi derivati, tieni presente che c'è un drago a guardia di quella porta: il ServicePointManager . Questa è una classe di configurazione che ha uno scopo nella vita: rovinare le tue prestazioni. Assicurati di liberare il tuo server dal ServicePoint.ConnectionLimit imposto artificialmente o la tua applicazione non si ridimensionerà mai (ti lascio scoprire qual è il valore predefinito ...). È inoltre possibile riconsiderare la politica predefinita di invio di un'intestazione Expect100Continue nelle richieste http.

Ora per quanto riguarda l'API gestita dal core socket, le cose sono abbastanza facili dal lato Invia, ma sono significativamente più complesse dal lato Ricevi. Per ottenere un throughput e una scalabilità elevati, è necessario assicurarsi che il socket non sia controllato dal flusso perché non si dispone di un buffer registrato per la ricezione. Idealmente per prestazioni elevate, è necessario pubblicare 3-4 buffer in anticipo e pubblicare nuovi buffer non appena si ottiene uno indietro ( prima di elaborare quello restituito) in modo da assicurarsi che il socket abbia sempre un posto dove depositare i dati provenienti dalla rete. Vedrai perché probabilmente non sarai in grado di raggiungere questo obiettivo a breve.

Dopo aver finito di giocare con l'API BeginRead / BeginWrite e aver iniziato il lavoro serio, ti renderai conto che hai bisogno di sicurezza sul tuo traffico, ad es. Autenticazione NTLM / Kerberos e crittografia del traffico, o almeno protezione dalla manomissione del traffico. Il modo in cui lo fai è usare System.Net.Security.NegotiateStream integrato (o SslStream se devi attraversare domini disparati). Ciò significa che invece di fare affidamento su operazioni asincrone con socket diretto, ti affidi alle operazioni asincrone AuthenticatedStream. Non appena si ottiene un socket (da connect sul client o da accetta sul server), si crea uno stream sul socket e lo si invia per l'autenticazione, chiamando BeginAuthenticateAsClient o BeginAuthenticateAsServer. Una volta completata l'autenticazione (almeno al sicuro dalla follia nativa InitiateSecurityContext / AcceptSecurityContext ...) eseguirai l'autorizzazione controllando la proprietà RemoteIdentity del tuo flusso autenticato e facendo qualsiasi verifica ACL che il tuo prodotto deve supportare. Dopodiché invierai messaggi utilizzando BeginWrite e li riceverai con BeginRead. Questo è il problema di cui parlavo prima che non sarai in grado di pubblicare più buffer di ricezione, perché le classi AuthenticateStream non lo supportano. L'operazione BeginRead gestisce internamente tutti gli I / O fino a quando non si riceve un intero frame, altrimenti non è in grado di gestire l'autenticazione del messaggio (decodificare frame e convalidare la firma sul frame). Sebbene nella mia esperienza il lavoro svolto dalle classi AuthenticatedStream sia abbastanza buono e non dovrebbe avere alcun problema. Vale a dire. dovresti essere in grado di saturare la rete GB con solo il 4-5% di CPU. Le classi AuthenticatedStream ti imporranno anche le limitazioni della dimensione dei frame specifiche del protocollo (16k per SSL, 12k per Kerberos).

Questo dovrebbe iniziare sulla strada giusta. Non ho intenzione di pubblicare codice qui, c'è un ottimo esempio su MSDN . Ho fatto molti progetti come questo e sono stato in grado di scalare a circa 1000 utenti connessi senza problemi. Inoltre, dovrai modificare le chiavi di registro per consentire al kernel di gestire più socket. e assicurati di eseguire la distribuzione su un sistema operativo server , ovvero W2K3 non XP o Vista (ovvero client OS), fa una grande differenza.

A proposito, assicurati che se hai operazioni di database sul server o file IO utilizzi anche il sapore asincrono per loro, o svuoti il ​​pool di thread in pochissimo tempo. Per le connessioni di SQL Server assicurarsi di aggiungere "Asyncronous Processing = true" alla stringa di connessione.


Ci sono alcune informazioni fantastiche qui. Vorrei poter assegnare a più persone la generosità. Tuttavia, ti ho votato. Roba buona qui, grazie.
Erik Funkenbusch,

11

Ho un server del genere in esecuzione in alcune delle mie soluzioni. Ecco una spiegazione molto dettagliata dei diversi modi per farlo in .net: avvicinarsi al filo con socket ad alte prestazioni in .NET

Ultimamente sono stato alla ricerca di modi per migliorare il nostro codice e esaminerò questo aspetto: " Socket Performance Enhancements in Version 3.5 " che è stato incluso specificamente "per l'utilizzo da parte di applicazioni che utilizzano l'I / O di rete asincrona per ottenere le massime prestazioni".

"La caratteristica principale di questi miglioramenti è l'eliminazione dell'allocazione e della sincronizzazione ripetute di oggetti durante l'I / O socket asincrono ad alto volume. Il modello di progettazione Begin / End attualmente implementato dalla classe Socket per l'I / O socket asincrono richiede un sistema. L'oggetto IAsyncResult deve essere allocato per ogni operazione socket asincrona. "

Puoi continuare a leggere se segui il link. Personalmente testerò il loro codice di esempio domani per confrontarlo con quello che ho.

Modifica: qui puoi trovare il codice funzionante sia per client che per server usando il nuovo SocketAsyncEventArgs 3.5 in modo da poterlo testare in un paio di minuti e passare attraverso il codice. È un approccio semplice ma è la base per iniziare un'implementazione molto più ampia. Anche questo articolo di quasi due anni fa su MSDN Magazine è stato una lettura interessante.



9

Hai considerato di utilizzare solo un binding TCP netto WCF e un modello di pubblicazione / sottoscrizione? WCF ti permetterebbe di concentrarti [principalmente] sul tuo dominio invece che su quello idraulico.

Ci sono molti esempi WCF e persino un framework di pubblicazione / sottoscrizione disponibile nella sezione download di IDesign che può essere utile: http://www.idesign.net


8

Mi chiedo una cosa:

Sicuramente non voglio iniziare un thread per ogni connessione.

Perché? Windows potrebbe gestire centinaia di thread in un'applicazione almeno da Windows 2000. L'ho fatto, è davvero facile lavorare con i thread se non è necessario sincronizzare. Soprattutto dato che stai eseguendo molti I / O (quindi non sei associato alla CPU e molti thread verrebbero bloccati su disco o comunicazione di rete), non capisco questa limitazione.

Hai provato il multithreading e l'hai trovato privo di qualcosa? Hai intenzione di avere anche una connessione al database per ogni thread (che ucciderebbe il server del database, quindi è una cattiva idea, ma è facilmente risolto con un design a 3 livelli). Sei preoccupato che avrai migliaia di clienti anziché centinaia e poi avrai davvero problemi? (Anche se proverei un migliaio di thread o anche diecimila se avessi 32+ GB di RAM - di nuovo, dato che non sei associato alla CPU, il tempo di cambio thread dovrebbe essere assolutamente irrilevante.)

Ecco il codice: per vedere come funziona, vai su http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html e fai clic sull'immagine.

Classe server:

  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);
      }
    }
  }

Programma principale del server:

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

Classe cliente:

  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);
      }
    }
  }

Programma principale del cliente:

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 può gestire molti thread, ma .NET non è progettato per gestirli. Ogni appdomain .NET ha un pool di thread e non si desidera esaurire quel pool di thread. Non sono sicuro se avvii manualmente una discussione se proviene dal threadpool o no. Tuttavia, centinaia di thread che non fanno nulla per la maggior parte del tempo sono un enorme spreco di risorse.
Erik Funkenbusch,

1
Credo che tu abbia una visione errata dei thread. I thread provengono dal pool di thread solo se lo desideri effettivamente, i thread normali no. Centinaia di thread che non fanno nulla non sprecano esattamente nulla :) (Beh, un po 'di memoria, ma la memoria è così economica che non è più un problema.) Scriverò un paio di app di esempio per questo, posterò un URL su una volta che ho finito. Nel frattempo, ti consiglio di rivedere ciò che ho scritto sopra e provare a rispondere alle mie domande.
Marcel Popescu,

1
Mentre sono d'accordo con il commento di Marcel sulla vista dei thread in cui i thread creati non provengono dal threadpool, il resto dell'affermazione non è corretto. La memoria non riguarda la quantità installata in una macchina, tutte le applicazioni su Windows girano nello spazio di indirizzi virtuale e su un sistema a 32 bit che ti dà 2 GB di dati per la tua app (non importa quanto ram sia installato sulla scatola). Devono comunque essere gestiti dal runtime. Fare l'IO asincrono non usa un thread per aspettare (usa IOCP che consente l'IO sovrapposto) ed è una soluzione migliore e ridimensionerà MOLTO meglio.
Brian ONeil,

7
Quando si eseguono molti thread non è la memoria il problema, ma la CPU. Il cambio di contesto tra thread è un'operazione relativamente costosa e più thread attivi si hanno più switch di contesto che si verificheranno. Alcuni anni fa ho eseguito un test sul mio PC con un'app console C # e con ca. 500 thread la mia CPU era al 100%, i thread non stavano facendo nulla di significativo. Per le comunicazioni di rete è meglio mantenere basso il numero di thread.
sipwiz,

1
Vorrei andare con una soluzione Task o utilizzare async / waitit. La soluzione Task sembra più semplice mentre async / wait sono probabilmente più scalabili (erano specificamente pensati per situazioni legate all'IO).
Marcel Popescu il

5

Utilizzando Async IO integrato di .NET (BeginRead . Etc) è una buona idea se riesci a ottenere tutti i dettagli nel modo giusto. Quando si configurano correttamente gli handle del file / socket, verrà utilizzata l'implementazione IOCP sottostante del sistema operativo, consentendo il completamento delle operazioni senza utilizzare alcun thread (o, nel peggiore dei casi, utilizzando un thread che credo provenga invece dal pool di thread IO del kernel del pool di thread di .NET, che consente di alleviare la congestione di threadpool.)

Il gotcha principale è assicurarsi di aprire socket / file in modalità non bloccante. La maggior parte delle funzioni comfort predefinite (comeFile.OpenRead ) non lo fanno, quindi dovrai scrivere le tue.

Una delle altre preoccupazioni principali è la gestione degli errori: gestire correttamente gli errori quando si scrive codice I / O asincrono è molto, molto più difficile che farlo in codice sincrono. È anche molto facile finire con le condizioni di gara e deadlock anche se potresti non utilizzare i thread direttamente, quindi devi esserne consapevole.

Se possibile, dovresti provare a utilizzare una libreria di convenienza per facilitare il processo di esecuzione di IO asincroni scalabili.

Il Runtime di coordinamento della concorrenza di Microsoft è un esempio di libreria .NET progettata per facilitare la difficoltà di eseguire questo tipo di programmazione. Sembra fantastico, ma dato che non l'ho usato, non posso commentare quanto bene si ridimensionerebbe.

Per i miei progetti personali che devono eseguire reti I / O asincrone o di rete, utilizzo un set di strumenti di concorrenza / I / O .NET che ho creato lo scorso anno, chiamato Squared.Task . È ispirato a librerie come imvu.task e twisted e ho incluso alcuni esempi funzionanti nel repository che eseguono I / O di rete. L'ho anche usato in alcune applicazioni che ho scritto - la più grande rilasciata pubblicamente è NDexer (che lo utilizza per l'I / O su disco senza ). La biblioteca è stata scritta sulla base della mia esperienza con imvu.task e ha una serie di test unitari abbastanza completi, quindi ti incoraggio vivamente a provarlo. In caso di problemi, saremo lieti di offrirti un po 'di assistenza.

A mio avviso, in base alla mia esperienza con l'utilizzo di IO asincrono / threadless anziché thread è uno sforzo utile sulla piattaforma .NET, purché tu sia pronto a gestire la curva di apprendimento. Ti consente di evitare i problemi di scalabilità imposti dal costo degli oggetti Thread e, in molti casi, puoi evitare completamente l'uso di blocchi e mutex facendo un uso attento dei primitivi di concorrenza come Futures / Promises.


Informazioni fantastiche, controllerò i tuoi riferimenti e vedrò cosa ha senso.
Erik Funkenbusch,

3

Ho usato la soluzione di Kevin, ma dice che la soluzione non ha il codice per il riassemblaggio dei messaggi. Gli sviluppatori possono utilizzare questo codice per il riassemblaggio dei messaggi:

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

È possibile provare a utilizzare un framework chiamato ACE (Adaptive Communications Environment) che è un framework C ++ generico per server di rete. È un prodotto molto solido, maturo ed è progettato per supportare applicazioni ad alta affidabilità e volumi elevati fino al telco-grade.

Il framework si occupa di una vasta gamma di modelli di concorrenza e probabilmente ne ha uno adatto per la tua applicazione immediatamente. Ciò dovrebbe facilitare il debug del sistema poiché la maggior parte dei problemi di concorrenza è già stata risolta. Il compromesso qui è che il framework è scritto in C ++ e non è la base di codice più calda e soffice. D'altra parte, si ottiene subito un'infrastruttura di rete di livello industriale testata e un'architettura altamente scalabile.


2
Questo è un buon suggerimento, ma dai tag della domanda credo che l'OP utilizzerà C #
JPCosta,

Ho notato che; il suggerimento era che questo è disponibile per C ++ e non sono a conoscenza di qualcosa di equivalente per C #. Il debug di questo tipo di sistema non è facile nel migliore dei casi e potresti ottenere un ritorno dall'andare a questo framework anche se significa passare a C ++.
Preoccupato di TunbridgeWells

Sì, questo è C #. Sto cercando buone soluzioni basate su .net. Avrei dovuto essere più chiaro, ma immaginavo che le persone avrebbero letto i tag
Erik Funkenbusch,


1

Bene, i socket .NET sembrano fornire select () - è la cosa migliore per gestire l'input. Per l'output avrei un pool di thread di socket writer in ascolto su una coda di lavoro, accettando il descrittore / oggetto socket come parte dell'elemento di lavoro, quindi non è necessario un thread per socket.


1

Vorrei utilizzare i metodi AcceptAsync / ConnectAsync / ReceiveAsync / SendAsync che sono stati aggiunti in .Net 3.5. Ho fatto un benchmark e sono circa il 35% più veloci (tempo di risposta e bitrate) con 100 utenti che inviano e ricevono costantemente dati.


1

alle persone che copiano incollando la risposta accettata, è possibile riscrivere il metodo acceptCallback, rimuovendo tutte le chiamate di _serverSocket.BeginAccept (new AsyncCallback (acceptCallback), _serverSocket); e inseriscilo in una clausola finalmente {}, in questo modo:

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);       
       }
     }

potresti persino rimuovere la prima cattura poiché il suo contenuto è lo stesso ma è un metodo modello e dovresti usare l'eccezione tipizzata per gestire meglio le eccezioni e capire cosa ha causato l'errore, quindi implementa quelle catture con un codice utile



-1

Per essere chiari, sto cercando soluzioni basate su .net (se possibile, C #, ma funzionerà qualsiasi linguaggio .net)

Non otterrai il massimo livello di scalabilità se utilizzi esclusivamente .NET. Le pause GC possono ostacolare la latenza.

Ho bisogno di iniziare almeno un thread per il servizio. Sto pensando di utilizzare l'API Asynch (BeginRecieve, ecc.) Poiché non so quanti client avrò connesso in un determinato momento (forse centinaia). Sicuramente non voglio iniziare un thread per ogni connessione.

L'IO sovrapposto è generalmente considerata l'API più veloce di Windows per le comunicazioni di rete. Non so se sia uguale all'API Asynch. Non utilizzare select in quanto ogni chiamata deve controllare ogni socket aperto anziché disporre di callback su socket attivi.


1
Non capisco il tuo commento di pausa GC. Non ho mai visto un sistema con problemi di scalabilità direttamente correlato a GC.
markt,

4
È molto più probabile che tu crei un'app che non può essere ridimensionata a causa della scarsa architettura rispetto al fatto che GC esiste. Enormi sistemi scalabili + performanti sono stati creati con .NET e Java. In entrambi i collegamenti forniti, la causa non era direttamente la garbage collection .. ma era correlata allo scambio di heap. Sospetto che sia davvero un problema con l'architettura che avrebbe potuto essere evitato. Se mi puoi mostrare un linguaggio che non è possibile costruire un sistema che non può essere scalato, lo userò volentieri;)
markt

1
Non sono d'accordo con questo commento. Sconosciuto, le domande a cui si fa riferimento sono Java e si occupano specificamente di allocazioni di memoria più grandi e provano a forzare manualmente gc. Non avrò davvero enormi quantità di allocazione di memoria in corso qui. Questo non è solo un problema. Ma grazie. Sì, il modello di programmazione asincrona è in genere implementato su IO sovrapposto.
Erik Funkenbusch,

1
In realtà, la migliore pratica non è quella di forzare costantemente la raccolta manuale del GC. Questo potrebbe benissimo peggiorare la tua app. .NET GC è un GC generazionale che si sintonizzerà sull'utilizzo della tua app. Se pensi davvero di dover chiamare manualmente GC.Collect, direi che molto probabilmente il tuo codice deve essere scritto in un altro modo ...
markt

1
@markt, questo è un commento per le persone che non sanno davvero nulla della raccolta dei rifiuti. Se hai tempo di inattività, non c'è niente di sbagliato nel fare una raccolta manuale. Non peggiorerà la tua applicazione al termine. I documenti accademici mostrano che i GC generazionali funzionano perché è un'approssimazione della durata dei tuoi oggetti. Ovviamente questa non è una rappresentazione perfetta. In effetti, esiste un paradosso in cui la generazione "più vecchia" ha spesso il più alto rapporto di immondizia perché non viene mai raccolta.
Sconosciuto

-1

È possibile utilizzare il framework open source Push Framework per lo sviluppo di server ad alte prestazioni. È basato su IOCP ed è adatto per scenari push e trasmissione di messaggi.

http://www.pushframework.com


1
Questo post è stato taggato C # e .net. Perché hai suggerito un framework C ++?
Erik Funkenbusch,

Probabilmente perché l'ha scritto. potatosoftware.com/…
quillbreaker

pushframework supporta più istanze del server? in caso contrario, come si ridimensiona?
Esskar,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.