151 votes

Comment écrire un serveur évolutif basé sur TCP/IP ?

Je suis dans la phase de conception de l'écriture d'une nouvelle application de service Windows qui accepte les connexions TCP/IP pour des connexions de longue durée (c'est-à-dire que ce n'est pas comme HTTP où il y a de nombreuses connexions courtes, mais plutôt un client qui se connecte et reste connecté pendant des heures ou des jours, voire des semaines).

Je cherche des idées sur la meilleure façon de concevoir l'architecture du réseau. Je vais devoir démarrer au moins un thread pour le service. J'envisage d'utiliser l'API Asynch (BeginReceve, etc.) car je ne sais pas combien de clients seront connectés à un moment donné (peut-être des centaines). Je ne veux absolument pas démarrer un thread pour chaque connexion.

Les données seront principalement envoyées aux clients à partir de mon serveur, mais il y aura quelques commandes envoyées par les clients à l'occasion. Il s'agit principalement d'une application de surveillance dans laquelle mon serveur envoie périodiquement des données d'état aux clients.

Quelle est la meilleure façon de rendre ce système aussi évolutif que possible ? Flux de travail de base ?

Pour être clair, je recherche des solutions basées sur .NET (C# si possible, mais tout langage .NET fera l'affaire).

J'aurais besoin d'un exemple fonctionnel de solution, soit sous la forme d'un pointeur vers quelque chose que je pourrais télécharger, soit sous la forme d'un court exemple en ligne. La solution doit être basée sur .NET et Windows (tout langage .NET est acceptable).

2 votes

Êtes-vous absolument sûr qu'il doit s'agir d'une connexion longue durée ? C'est difficile à dire à partir du peu d'informations fournies, mais je ne le ferais que si c'est absolument nécessaire

0 votes

Oui, il faut qu'elle soit de longue durée. Les données doivent être mises à jour en temps réel, donc je ne peux pas faire de sondage périodique, les données doivent être poussées vers le client au fur et à mesure qu'elles se produisent, ce qui signifie une connexion constante.

1 votes

Ce n'est pas une raison valable. Http supporte très bien les connexions de longue durée. Vous ouvrez simplement une connexion et attendez une réponse (stalled poll). Cela fonctionne bien pour de nombreuses applications de style AJAX, etc. Comment pensez-vous que gmail fonctionne :-)

94voto

Kevin Nisbet Points 1589

J'ai déjà écrit quelque chose de similaire dans le passé. Mes recherches d'il y a quelques années m'ont montré qu'écrire votre propre implémentation de socket était la meilleure solution, en utilisant la fonction asynchrone sockets. Cela signifie que les clients qui ne font pas vraiment quelque chose nécessitent relativement peu de ressources. Tout ce qui se passe est géré par le pool de threads de .NET.

Je l'ai écrit comme une classe qui gère toutes les connexions pour les serveurs.

J'ai simplement utilisé une liste pour contenir toutes les connexions du client, mais si vous avez besoin de recherches plus rapides pour des listes plus importantes, vous pouvez l'écrire comme vous le souhaitez.

private List<xConnection> _sockets;

Il faut aussi que le socket écoute réellement les connexions entrantes.

private System.Net.Sockets.Socket _serverSocket;

La méthode start démarre réellement le socket du serveur et commence à écouter les connexions entrantes.

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("An error occurred 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 rear previously, so make sure there is only one
       //BeginAccept running
       _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
    }
    catch (Exception e)
    {
       throw new ApplicationException("An error occurred starting listeners. Check inner exception", e);
    }
    return true;
 }

J'aimerais juste noter que le code de gestion des exceptions a l'air mauvais, mais la raison en est que j'avais un code de suppression des exceptions dans ce code afin que toutes les exceptions soient supprimées et que le retour soit effectué. false si une option de configuration était définie, mais je voulais l'enlever par souci de brièveté.

La commande _serverSocket.BeginAccept(new AsyncCallback(acceptCallback)), _serverSocket) ci-dessus configure essentiellement notre socket serveur pour qu'il appelle la méthode acceptCallback chaque fois qu'un utilisateur se connecte. Cette méthode s'exécute à partir du pool de threads .NET, qui gère automatiquement la création de threads de travail supplémentaires si vous avez de nombreuses opérations bloquantes. Cela devrait permettre de gérer de manière optimale toute charge sur le serveur.

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

Le code ci-dessus vient essentiellement d'accepter la connexion qui arrive, met en file d'attente BeginReceive qui est un callback qui s'exécute lorsque le client envoie des données, puis met en attente le prochain acceptCallback qui acceptera la prochaine connexion client qui arrivera.

El BeginReceive est ce qui indique au socket ce qu'il doit faire lorsqu'il reçoit des données du client. Pour BeginReceive vous devez lui donner un tableau d'octets, qui est l'endroit où il copiera les données lorsque le client enverra des données. Le site ReceiveCallback sera appelée, c'est ainsi que nous gérons la réception des données.

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 : Dans ce modèle, j'ai oublié de mentionner que dans cette zone du code :

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

En général, dans le code que vous voulez, je ferais le réassemblage des paquets en messages, puis je les créerais comme des tâches sur le pool de threads. De cette façon, le BeginReceive du bloc suivant du client n'est pas retardé pendant que le code de traitement des messages s'exécute.

Le callback accept termine la lecture du socket de données en appelant end receive. Cela remplit le tampon fourni par la fonction begin receive. Une fois que vous avez fait ce que vous vouliez à l'endroit où j'ai laissé le commentaire, nous appelons la fonction suivante BeginReceive qui exécutera à nouveau le callback si le client envoie d'autres données.

Maintenant, voici la partie vraiment délicate : Lorsque le client envoie des données, votre callback de réception peut n'être appelé qu'avec une partie du message. Le réassemblage peut devenir très très compliqué. J'ai utilisé ma propre méthode et créé une sorte de protocole propriétaire pour faire cela. Je l'ai laissé de côté, mais si vous le demandez, je peux l'ajouter. Ce gestionnaire était en fait le morceau de code le plus compliqué que j'aie jamais écrit.

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

La méthode d'envoi ci-dessus utilise en fait une méthode synchrone Send appel. Pour moi, cela convenait parfaitement en raison de la taille des messages et de la nature multithread de mon application. Si vous voulez envoyer des messages à tous les clients, il vous suffit de parcourir en boucle la liste _sockets.

La classe xConnection que vous voyez référencée ci-dessus est essentiellement une simple enveloppe pour une socket afin d'inclure le tampon d'octets, et dans mon implémentation quelques extras.

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

Pour référence, voici également les using que j'inclus, car je suis toujours contrarié quand ils ne sont pas inclus.

using System.Net.Sockets;

J'espère que c'est utile. Ce n'est peut-être pas le code le plus propre, mais il fonctionne. Il y a également quelques nuances dans le code que vous devriez être prudent de changer. Par exemple, il n'y a qu'un seul BeginAccept appelé à un moment donné. Il y avait un bogue .NET très ennuyeux à ce sujet, il y a des années, donc je ne me souviens pas des détails.

De même, dans le ReceiveCallback nous traitons tout ce qui est reçu du socket avant de mettre en file d'attente la réception suivante. Cela signifie que pour une seule socket, nous ne sommes jamais en fait que dans le mode ReceiveCallback une fois à tout moment, et nous n'avons pas besoin d'utiliser la synchronisation des threads. Cependant, si vous réorganisez cette opération pour appeler la réception suivante immédiatement après avoir extrait les données, ce qui pourrait être un peu plus rapide, vous devrez vous assurer que vous synchronisez correctement les threads.

J'ai également supprimé une grande partie de mon code, mais j'ai laissé l'essentiel de ce qui se passe en place. Cela devrait être un bon début pour votre conception. Laissez un commentaire si vous avez d'autres questions à ce sujet.

1 votes

C'est une bonne réponse Kevin on dirait que vous êtes sur la bonne voie pour obtenir la prime. :)

6 votes

Je ne sais pas pourquoi c'est la réponse la plus votée. Begin* End* n'est pas la façon la plus rapide de faire du réseautage en C#, ni la plus évolutive. Elle est plus rapide que la méthode synchrone, mais il y a beaucoup d'opérations qui se passent sous le capot de Windows et qui ralentissent vraiment ce chemin de réseau.

6 votes

Gardez à l'esprit ce que esac a écrit dans le commentaire précédent. Le pattern begin-end fonctionnera probablement pour vous jusqu'à un certain point, d'ailleurs mon code utilise actuellement begin-end, mais il y a des améliorations à ses limitations dans .net 3.5. Je ne me soucie pas de la prime mais je vous recommande de lire le lien dans ma réponse même si vous mettez en œuvre cette approche. "Amélioration des performances des sockets dans la version 3.5".

84voto

esac Points 6283

Il existe de nombreuses façons d'effectuer des opérations réseau en C#. Toutes utilisent des mécanismes différents sous le capot, et souffrent donc de problèmes de performances majeurs en cas de forte concurrence. Les opérations Begin* sont l'une d'entre elles, que beaucoup de gens considèrent à tort comme le moyen le plus rapide d'effectuer des opérations réseau.

Pour résoudre ces problèmes, ils ont introduit le Ensemble de méthodes asynchrones : De MSDN, Classe SocketAsyncEventArgs -

La classe SocketAsyncEventArgs fait partie d'un ensemble d'améliorations apportées à la classe System.Net.Sockets..: :.Socket qui fournit un modèle asynchrone alternatif pouvant être utilisé par des applications de socket spécialisées à haute performance. Cette classe a été spécifiquement conçue pour les applications de serveur de réseau qui nécessitent des performances élevées. Une application peut utiliser le modèle asynchrone amélioré exclusivement ou seulement dans des zones sensibles ciblées (par exemple, lors de la réception de grandes quantités de données).

La principale caractéristique de ces améliorations est d'éviter l'allocation et la synchronisation répétées d'objets pendant les E/S de socket asynchrones à fort volume. Le modèle de conception Begin/End actuellement mis en œuvre par la classe System.Net.Sockets..: :.Socket exige qu'un objet System..: :.IAsyncResult soit alloué pour chaque opération de socket asynchrone.

Sous la couverture, l'API *Async utilise les ports d'achèvement d'E/S, ce qui est le moyen le plus rapide d'effectuer des opérations de mise en réseau, cf. Windows Sockets 2.0 : Écrire des applications Winsock évolutives à l'aide de ports de complétion

Pour vous aider, j'ai inclus le code source d'un serveur telnet que j'ai écrit en utilisant l'API *Async. Je n'inclus que les parties pertinentes. Il faut également noter qu'au lieu de traiter les données en ligne, j'ai choisi de les pousser dans une file d'attente sans verrou (sans attente) qui est traitée sur un thread séparé. Notez que je n'inclus pas la classe Pool correspondante qui n'est qu'un simple pool qui créera un nouvel objet s'il est vide, et la classe Buffer qui n'est qu'un tampon auto-extensible qui n'est pas vraiment nécessaire à moins que vous ne receviez une quantité indéterminée de données.

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

0 votes

Il s'agit d'un exemple simple et direct. Merci. Je vais devoir évaluer les avantages et les inconvénients de chaque méthode.

0 votes

Je n'ai pas eu l'occasion de le tester mais j'ai la vague impression qu'il s'agit d'une condition de course pour une raison quelconque. Premièrement, si vous recevez beaucoup de messages, je ne sais pas si les événements seront traités dans l'ordre (ce n'est peut-être pas important pour l'application des utilisateurs, mais il faut le noter) ou je pourrais me tromper et les événements seront traités dans l'ordre. Deuxièmement, je l'ai peut-être manqué, mais n'y a-t-il pas un risque d'écrasement du tampon effacé alors que DataReceived est toujours en cours d'exécution si cela prend beaucoup de temps ? Si ces préoccupations peut-être injustifiées sont abordées, je pense que c'est une très bonne solution moderne.

1 votes

Dans mon cas, pour mon serveur telnet, 100%, OUI ils sont en ordre. La clé est de définir la méthode de rappel appropriée avant d'appeler AcceptAsync, ReceiveAsync, etc. Dans mon cas, je fais le SendAsync sur un thread séparé, donc si cela est modifié pour faire un modèle Accept/Send/Receive/Send/Receive/Disconnect, alors cela devra être modifié.

45voto

jerryjvl Points 9310

Chris Mullins, de Coversant, a écrit une excellente discussion sur le TCP/IP évolutif à l'aide de .NET. Malheureusement, il semble que son blog ait disparu de son emplacement précédent. Je vais donc essayer de rassembler ses conseils de mémoire (certains de ses commentaires utiles apparaissent dans ce fil de discussion) : C++ contre C# : Développement d'un serveur IOCP hautement évolutif )

Avant toute chose, notez que l'utilisation de Begin/End et le Async méthodes sur le Socket font appel à Ports d'achèvement des E/S (IOCP) pour assurer l'évolutivité. Cela fait une bien plus grande différence (lorsqu'il est utilisé correctement ; voir ci-dessous) pour l'évolutivité que la méthode que vous choisissez pour mettre en œuvre votre solution.

Les postes de Chris Mullins étaient basés sur l'utilisation de Begin/End qui est celui avec lequel j'ai personnellement de l'expérience. Notez que Chris a mis au point une solution basée sur ce principe qui peut atteindre 10 000 connexions clients simultanées sur une machine 32 bits avec 2 Go de mémoire, et plus de 100 000 sur une plate-forme 64 bits avec suffisamment de mémoire. D'après ma propre expérience de cette technique (bien que loin de ce type de charge), je n'ai aucune raison de douter de ces chiffres indicatifs.

IOCP contre les primitives "thread par connexion" ou "select".

La raison pour laquelle vous voulez utiliser un mécanisme qui utilise IOCP sous le capot est qu'il utilise un pool de threads Windows de très bas niveau qui ne réveille aucun thread tant qu'il n'y a pas de données réelles sur le canal d'E/S que vous essayez de lire (notez qu'IOCP peut également être utilisé pour les E/S de fichiers). L'avantage de cette méthode est que Windows n'a pas besoin de passer à un thread pour constater qu'il n'y a pas encore de données, ce qui réduit le nombre de changements de contexte que votre serveur devra effectuer au strict minimum.

Le changement de contexte est ce qui va définitivement tuer le mécanisme de "thread par connexion", bien que ce soit une solution viable si vous ne traitez que quelques dizaines de connexions. Ce mécanisme n'est cependant pas du tout "évolutif".

Considérations importantes lors de l'utilisation d'IOCP

Mémoire

Avant tout, il est essentiel de comprendre qu'IOCP peut facilement entraîner des problèmes de mémoire sous .NET si votre mise en œuvre est trop naïve. Tous les programmes IOCP BeginReceive aura pour effet de "pinner" le tampon dans lequel vous lisez. Pour une bonne explication des raisons de ce problème, voir : Le Weblog de Yun Jin : OutOfMemoryException et épinglage .

Heureusement, ce problème peut être évité, mais cela demande un certain compromis. La solution proposée consiste à allouer un grand byte[] au démarrage de l'application (ou à proximité), d'au moins 90 Ko environ (à partir de .NET 2, la taille requise peut être supérieure dans les versions ultérieures). La raison pour laquelle il faut procéder ainsi est que les allocations de mémoire importantes se retrouvent automatiquement dans un segment de mémoire non compacté (le segment grand tas d'objets ) qui est effectivement épinglé automatiquement. En allouant un grand tampon au démarrage, vous vous assurez que ce bloc de mémoire inamovible se trouve à une adresse relativement basse, où il ne risque pas de gêner et de provoquer une fragmentation.

Vous pouvez ensuite utiliser les décalages pour segmenter ce grand tampon en zones distinctes pour chaque connexion qui doit lire des données. C'est ici qu'un compromis entre en jeu ; puisque ce tampon doit être pré-alloué, vous devrez décider de la quantité d'espace tampon dont vous avez besoin par connexion, et de la limite supérieure que vous voulez fixer au nombre de connexions que vous voulez faire évoluer (ou, vous pouvez implémenter une abstraction qui peut allouer des tampons épinglés supplémentaires une fois que vous en avez besoin).

La solution la plus simple serait d'attribuer à chaque connexion un seul octet à un décalage unique dans ce tampon. Vous pouvez alors créer un BeginReceive pour un seul octet à lire, et effectuer le reste de la lecture en fonction du rappel que vous obtenez.

Traitement

Lorsque vous obtenez le rappel de la Begin que vous avez fait, il est très important de réaliser que le code dans le callback sera exécuté sur le thread IOCP de bas niveau. Il est absolument essentiel que vous évitiez les opérations longues dans ce callback. L'utilisation de ces threads pour des traitements complexes nuira à votre évolutivité tout autant que l'utilisation de "threads par connexion".

La solution proposée est d'utiliser le rappel uniquement pour mettre en file d'attente un élément de travail pour traiter les données entrantes, qui sera exécuté sur un autre thread. Évitez toute opération potentiellement bloquante dans le callback afin que le thread IOCP puisse retourner à son pool aussi vite que possible. Dans .NET 4.0, je pense que la solution la plus simple est de créer un thread Task en lui donnant une référence à la socket du client et une copie du premier octet qui a déjà été lu par l'interface de l'utilisateur. BeginReceive appel. Cette tâche est ensuite chargée de lire toutes les données du socket qui représentent la demande que vous traitez, de les exécuter, puis de créer un nouvel appel de type BeginReceive pour mettre en file d'attente la socket pour IOCP une fois de plus. Avant .NET 4.0, vous pouvez utiliser le ThreadPool ou créer votre propre implémentation de file d'attente.

Résumé

En gros, je suggère d'utiliser L'exemple de code de Kevin pour cette solution, avec les avertissements supplémentaires suivants :

  • Assurez-vous que le tampon que vous passez à BeginReceive est déjà "épinglé".
  • Assurez-vous que le callback que vous passez à BeginReceive ne fait rien de plus que de mettre en file d'attente une tâche qui se chargera du traitement effectif des données entrantes.

Lorsque vous ferez cela, je ne doute pas que vous puissiez reproduire les résultats de Chris en passant à l'échelle à des centaines de milliers de clients simultanés (avec le bon matériel et une mise en œuvre efficace de votre propre code de traitement, bien sûr ;).

1 votes

Pour épingler un bloc de mémoire plus petit, la méthode Alloc de l'objet GCHandle peut être utilisée pour épingler le tampon. Une fois cette opération effectuée, l'élément UnsafeAddrOfPinnedArrayElement de l'objet Marshal peut être utilisé pour obtenir un pointeur vers le tampon. Par exemple : GCHandle gchTheCards = GCHandle.Alloc(TheData, GCHandleType.Pinned) ; IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement(TheData, 0) ; (sbyte*)pTheData = (sbyte*)pAddr.ToPointer() ;

0 votes

@BobBryan À moins que je ne manque un point subtil que vous essayez de faire, cette approche n'aide pas réellement à résoudre le problème que ma solution essaie de résoudre en allouant de grands blocs, à savoir le potentiel de fragmentation dramatique de la mémoire inhérent à l'allocation répétée de petits blocs de mémoire épinglés.

0 votes

Le fait est que vous n'avez pas besoin d'allouer un gros bloc pour le garder en mémoire. Vous pouvez allouer des blocs plus petits et utiliser la technique ci-dessus pour les fixer en mémoire afin d'éviter que le gc ne les déplace. Vous pouvez conserver une référence à chacun des petits blocs, tout comme vous conservez une référence à un seul grand bloc, et les réutiliser si nécessaire. Les deux approches sont valables - je soulignais simplement que vous n'avez pas besoin d'utiliser un très grand tampon. Mais, ceci dit, parfois l'utilisation d'un très grand tampon est la meilleure façon de procéder puisque le gc le traitera plus efficacement.

22voto

Remus Rusanu Points 159382

Vous avez déjà obtenu la majeure partie de la réponse via les exemples de code ci-dessus. L'utilisation d'opérations d'E/S asynchrones est absolument nécessaire. L'E/S asynchrone est la façon dont le Win32 est conçu en interne pour évoluer. La meilleure performance que vous pouvez obtenir est obtenue en utilisant ports d'achèvement Pour cela, vous pouvez lier vos sockets aux ports d'achèvement et avoir un pool de threads attendant l'achèvement du port d'achèvement. La sagesse commune est d'avoir 2-4 threads par CPU (core) en attente d'achèvement. Je vous recommande vivement de lire ces trois articles de Rick Vicik, de l'équipe chargée des performances de Windows :

  1. Conception d'applications pour la performance - Partie 1
  2. Conception d'applications pour la performance - Partie 2
  3. Conception d'applications pour la performance - Partie 3

Ces articles couvrent principalement l'API native de Windows, mais ils sont incontournables pour quiconque tente d'appréhender l'évolutivité et les performances. Ils contiennent également quelques brèves sur le côté géré des choses.

La deuxième chose que vous devez faire est de vous assurer que vous passez en revue les Amélioration des performances et de l'évolutivité des applications .NET qui est disponible en ligne. Vous trouverez des conseils pertinents et valables concernant l'utilisation des threads, des appels asynchrones et des verrous au chapitre 5. Mais les vraies perles se trouvent au chapitre 17, où vous trouverez des conseils pratiques sur le réglage de votre pool de threads. Mes applications ont connu de sérieux problèmes jusqu'à ce que j'ajuste les paramètres maxIothreads/maxWorkerThreads selon les recommandations de ce chapitre.

Vous dites que vous voulez faire un serveur TCP pur, donc mon point suivant est fallacieux. Cependant Si vous vous retrouvez acculé et que vous utilisez la classe WebRequest et ses dérivés, sachez qu'un dragon garde la porte : la classe WebRequest. ServicePointManager . Il s'agit d'une classe de configuration qui n'a qu'un seul but dans la vie : ruiner vos performances. Assurez-vous de libérer votre serveur de l'imposition artificielle de ServicePoint.ConnectionLimit ou votre application ne passera jamais à l'échelle (je vous laisse découvrir vous-même quelle est la valeur par défaut...). Vous pouvez également reconsidérer la politique par défaut d'envoyer un header Expect100Continue dans les requêtes HTTP.

En ce qui concerne l'API de base gérée par les sockets, les choses sont assez simples du côté de l'envoi, mais elles sont beaucoup plus complexes du côté de la réception. Afin d'obtenir un débit et une échelle élevés, vous devez vous assurer que le socket n'est pas contrôlé par le flux, car vous n'avez pas de tampon posté pour la réception. Idéalement, pour des performances élevées, vous devriez poster en avance 3-4 tampons et poster de nouveaux tampons dès que vous en recevez un en retour ( avant vous traitez celui qui vous est retourné), vous vous assurez ainsi que le socket a toujours un endroit où déposer les données provenant du réseau. Vous verrez bientôt pourquoi vous ne pourrez probablement pas y parvenir.

Lorsque vous aurez fini de jouer avec l'API BeginRead/BeginWrite et que vous commencerez à travailler sérieusement, vous réaliserez que vous avez besoin de sécuriser votre trafic, c'est-à-dire d'une authentification NTLM/Kerberos et d'un cryptage du trafic, ou au moins d'une protection contre la falsification du trafic. Pour ce faire, vous utilisez l'outil intégré System.Net.Security.NegotiateStream (ou SslStream si vous devez traverser des domaines disparates). Cela signifie qu'au lieu de s'appuyer sur des opérations asynchrones de type socket, vous vous appuierez sur des opérations asynchrones de type AuthenticatedStream. Dès que vous obtenez un socket (soit par connect sur le client, soit par accept sur le serveur), vous créez un flux sur le socket et le soumettez à l'authentification, en appelant BeginAuthenticateAsClient ou BeginAuthenticateAsServer. Une fois l'authentification terminée (au moins vous êtes à l'abri de la folie native de InitiateSecurityContext/AcceptSecurityContext...), vous ferez votre autorisation en vérifiant la propriété RemoteIdentity de votre flux authentifié et en faisant ce qui suit ACL vérification que votre produit doit prendre en charge.

Ensuite, vous enverrez des messages en utilisant le BeginWrite et vous les recevrez avec le BeginRead. C'est le problème dont je parlais précédemment, à savoir que vous ne pourrez pas afficher plusieurs tampons de réception, car les classes AuthenticateStream ne le supportent pas. L'opération BeginRead gère toutes les E/S en interne jusqu'à ce que vous ayez reçu une trame entière. Sinon, elle ne pourrait pas gérer l'authentification du message (décryptage de la trame et validation de la signature sur la trame). Cependant, d'après mon expérience, le travail effectué par les classes AuthenticatedStream est assez bon et ne devrait pas poser de problème. C'est-à-dire que vous devriez être capable de saturer un réseau de 1 Gbit/s avec seulement 4-5% de CPU. Les classes AuthenticatedStream vous imposeront également les limitations de taille de trame spécifiques au protocole (16k pour SSL, 12k pour Kerberos).

Cela devrait vous mettre sur la bonne voie. Je ne vais pas poster de code ici, et il y a une Un très bon exemple sur MSDN . J'ai réalisé de nombreux projets de ce type et j'ai pu aller jusqu'à environ 1000 utilisateurs connectés sans problème. Au-dessus de cela, vous devrez modifier les clés de registre pour permettre au noyau de gérer plus de sockets. Et assurez-vous que vous déployez sur un serveur OS, c'est-à-dire, Windows Server 2003 et non Windows XP ou Windows Vista (c'est-à-dire le système d'exploitation du client), cela fait une grande différence.

Par ailleurs, si vous effectuez des opérations de bases de données sur le serveur ou des entrées/sorties de fichiers, assurez-vous d'utiliser la version asynchrone pour ces opérations, sinon vous allez vider le pool de threads en un rien de temps. Pour les connexions SQL Server, assurez-vous d'ajouter l'option 'Asyncronous Processing=true' à la chaîne de connexion.

0 votes

Il y a de très bonnes informations ici. J'aimerais pouvoir attribuer la prime à plusieurs personnes. Cependant, je vous ai upvoted. De bonnes choses ici, merci.

9voto

markt Points 3716

Envisagez d'utiliser simplement un WCF net TCP binding et un modèle de publication/abonnement. WCF vous permettrait de vous concentrer (principalement) sur votre domaine au lieu de la plomberie...

De nombreux échantillons WCF et même un cadre de publication/abonnement sont disponibles dans la section de téléchargement d'IDesign, ce qui peut être utile : http://www.idesign.net

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X