Comment écrire un serveur Tcp / Ip évolutif


148

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

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

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

Avez-vous des suggestions sur la meilleure façon de rendre cela aussi évolutif que possible? Flux de travail de base? Merci.

EDIT: Pour être clair, je recherche des solutions basées sur .net (C # si possible, mais n'importe quel langage .net fonctionnera)

NOTE DE PRIME: Pour recevoir la prime, j'attends plus qu'une simple réponse. J'aurais besoin d'un exemple fonctionnel d'une solution, soit comme un pointeur vers quelque chose que je pourrais télécharger ou un court exemple en ligne. Et il doit être basé sur .net et Windows (toute langue .net est acceptable)

EDIT: Je tiens à remercier tous ceux qui ont donné de bonnes réponses. Malheureusement, je ne pouvais en accepter qu'une seule et j'ai choisi d'accepter la méthode Begin / End, plus connue. La solution d'Esac est peut-être meilleure, mais elle est encore assez récente pour que je ne sache pas avec certitude comment cela fonctionnera.

J'ai voté pour toutes les réponses que je pensais bonnes, j'aimerais pouvoir faire plus pour vous les gars. Merci encore.


1
Êtes-vous absolument sûr qu'il doit s'agir d'une connexion de longue durée? Il est difficile de dire à partir des informations limitées fournies, mais je ne le ferais que si cela est absolument nécessaire ..
Markt

Oui, cela doit durer longtemps. Les données doivent être mises à jour en temps réel, donc je ne peux pas faire d'interrogation périodique, les données doivent être transmises au client au fur et à mesure, ce qui signifie une connexion constante.
Erik Funkenbusch

1
Ce n’est pas une raison valable. Http supporte très bien les connexions de longue durée. Vous venez d'ouvrir une connexion et attendez un repsonse (sondage bloqué). Cela fonctionne très bien pour de nombreuses applications de style AJAX, etc. Comment pensez-vous que gmail fonctionne :-)
TFD

2
Gmail fonctionne en interrogeant périodiquement les e-mails, il ne conserve pas une connexion de longue durée. Cela convient aux e-mails, où une réponse en temps réel n'est pas requise.
Erik Funkenbusch

2
Interroger, ou tirer, évolue bien mais développe rapidement la latence. Le push n'évolue pas aussi bien, mais permet de réduire ou d'éliminer la latence.
andrewbadera

Réponses:


92

J'ai écrit quelque chose de similaire dans le passé. D'après mes recherches d'il y a des années, l'écriture de votre propre implémentation de socket était le meilleur choix, en utilisant les sockets asynchrones. Cela signifiait que les clients qui ne faisaient vraiment rien nécessitaient en fait relativement peu de ressources. Tout ce qui se produit est géré par le pool de threads .net.

Je l'ai écrit en tant que classe qui gère toutes les connexions pour les serveurs.

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

private List<xConnection> _sockets;

Vous avez également besoin de la prise qui écoute les connexions entrantes.

private System.Net.Sockets.Socket _serverSocket;

La méthode start démarre en fait le socket 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("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;
 }

Je voudrais juste noter que le code de gestion des exceptions semble mauvais, mais la raison en est que j'avais un code de suppression d'exception afin que toutes les exceptions soient supprimées et renvoyées falsesi une option de configuration était définie, mais je voulais la supprimer pour souci de brièveté.

Le _serverSocket.BeginAccept (new AsyncCallback (acceptCallback)), _serverSocket) ci-dessus définit essentiellement notre socket serveur pour appeler 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 de blocage. Cela devrait 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 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);
       }
     }

Le code ci-dessus vient de terminer d'accepter la connexion qui entre, des files d'attente BeginReceivequi est un rappel qui s'exécutera lorsque le client envoie des données, puis met en file d'attente la suivante acceptCallbackqui acceptera la prochaine connexion client qui entrera.

L' BeginReceiveappel de méthode est ce qui indique à la socket ce qu'elle doit faire lorsqu'elle reçoit des données du client. Pour BeginReceive, vous devez lui donner un tableau d'octets, où il copiera les données lorsque le client envoie des données. La ReceiveCallbackméthode sera appelée, c'est ainsi que nous gérons la réception de 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 de 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);

Ce que je ferais généralement, c'est dans le code que vous voulez, c'est de réassembler les paquets en messages, puis de les créer en tant que travaux sur le pool de threads. De cette façon, le BeginReceive du bloc suivant du client n'est pas retardé pendant l'exécution du code de traitement des messages.

Le rappel d'acceptation termine la lecture du socket de données en appelant end receive. Cela remplit le tampon fourni dans la fonction de début de réception. Une fois que vous avez fait ce que vous voulez là où j'ai laissé le commentaire, nous appelons la BeginReceiveméthode suivante qui exécutera à nouveau le rappel si le client envoie plus de données. Maintenant, voici la partie vraiment délicate, lorsque le client envoie des données, votre rappel de réception peut être appelé uniquement avec une partie du message. Le remontage peut devenir très très compliqué. J'ai utilisé ma propre méthode et créé une sorte de protocole propriétaire pour ce faire. 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 un Sendappel synchrone , ce qui me convenait parfaitement en raison de la taille des messages et de la nature multithread de mon application. Si vous souhaitez envoyer à tous les clients, il vous suffit de parcourir la liste _sockets.

La classe xConnection que vous voyez référencée ci-dessus est fondamentalement un simple wrapper pour une socket pour 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;
}

Aussi pour référence, voici les usings que j'inclus car je suis toujours ennuyé quand ils ne sont pas inclus.

using System.Net.Sockets;

J'espère que cela est utile, ce n'est peut-être pas le code le plus propre, mais cela fonctionne. Il y a aussi quelques nuances dans le code que vous devriez vous lasser de changer. D'une part, n'en avoir qu'un seul BeginAcceptappelé à la fois. 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 plus, dans le ReceiveCallbackcode, nous traitons tout ce qui est reçu de la socket avant de mettre en file d'attente la prochaine réception. Cela signifie que pour un seul socket, nous ne sommes en fait jamais connectés qu'une seule ReceiveCallbackfois à un moment donné et nous n'avons pas besoin d'utiliser la synchronisation des threads. Cependant, si vous réorganisez ceci pour appeler la prochaine réception immédiatement après avoir extrait les données, ce qui peut être un peu plus rapide, vous devrez vous assurer que vous synchronisez correctement les threads.

De plus, j'ai piraté 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
C'est une bonne réponse Kevin ... on dirait que vous êtes sur la bonne voie pour obtenir la prime. :)
Erik Funkenbusch

6
Je ne sais pas pourquoi c'est la réponse la plus votée. Begin * End * n'est pas le moyen le plus rapide de faire du réseautage en C #, ni le plus hautement évolutif. C'est plus rapide que synchrone, mais il y a beaucoup d'opérations sous le capot sous Windows qui ralentissent vraiment ce chemin réseau.
esac

6
Gardez à l'esprit ce qu'esac a écrit dans le commentaire précédent. Le modèle de début de fin fonctionnera probablement pour vous jusqu'à un certain point, diable mon code utilise actuellement begin-end, mais il y a des améliorations à ses limitations dans .net 3.5. Je me fiche de la prime mais je vous recommande de lire le lien dans ma réponse même si vous implémentez cette approche. "Améliorations des performances de socket dans la version 3.5"
jvanderh

1
Je voulais juste ajouter leur car je n'ai peut-être pas été assez clair, c'est du code de l'ère .net 2.0 où je crois que c'était un modèle très viable. Cependant, la réponse d'esac semble être un peu plus moderne si vous ciblez .net 3.5, le seul pinailleur que j'ai est le lancement d'événements :) mais cela peut facilement être changé. De plus, j'ai fait des tests de débit avec ce code et sur un opteron double cœur, 2Ghz a pu maximiser 100 Mbps Ethernet, ce qui a ajouté une couche de cryptage au-dessus de ce code.
Kevin Nisbet

1
@KevinNisbet Je sais que c'est assez tard, mais pour quiconque utilise cette réponse pour concevoir ses propres serveurs - l'envoi doit également être asynchrone, car sinon vous vous exposez à une possibilité de blocage. Si les deux côtés écrivent des données qui remplissent leurs tampons respectifs, les Sendméthodes se bloqueront indéfiniment des deux côtés, car personne ne lit les données d'entrée.
Luaan

83

Il existe de nombreuses façons d'effectuer des opérations réseau en C #. Tous utilisent différents mécanismes sous le capot et souffrent donc de problèmes de performances majeurs avec une concurrence élevée. Les opérations Begin * sont l'une de celles que beaucoup de gens confondent souvent comme étant le moyen le plus rapide / le plus rapide de faire du réseautage.

Pour résoudre ces problèmes, ils ont introduit l'ensemble de méthodes * Async: à partir de MSDN http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

La classe SocketAsyncEventArgs fait partie d'un ensemble d'améliorations de la classe System.Net.Sockets .. ::. Socket qui fournissent un autre modèle asynchrone qui peut être utilisé par des applications de socket hautes performances spécialisées. Cette classe a été spécialement conçue pour les applications de serveur réseau qui nécessitent des performances élevées. Une application peut utiliser le modèle asynchrone amélioré exclusivement ou uniquement dans les 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 des objets pendant les E / S de socket asynchrone à volume élevé. Le modèle de conception Begin / End actuellement implémenté par la classe System.Net.Sockets .. ::. Socket nécessite qu'un objet System .. ::. IAsyncResult soit alloué pour chaque opération de socket asynchrone.

Sous les couvertures, l'API * Async utilise les ports d'achèvement d'E / S, qui est le moyen le plus rapide d'effectuer des opérations de mise en réseau, voir http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

Et juste pour vous aider, j'inclus le code source d'un serveur telnet que j'ai écrit en utilisant l'API * Async. J'inclus uniquement les parties pertinentes. À noter également, au lieu de traiter les données en ligne, j'opte plutôt pour les pousser dans une file d'attente sans verrouillage (attente gratuite) qui est traitée sur un thread séparé. Notez que je n'inclut pas la classe Pool correspondante qui est juste un simple pool qui créera un nouvel objet s'il est vide, et la classe Buffer qui est juste un tampon auto-extensible qui n'est pas vraiment nécessaire sauf si vous recevez un indéterministe quantité de données. Si vous souhaitez plus d'informations, n'hésitez pas à m'envoyer 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
    }
}

C'est assez simple et un exemple simple. Merci. Je vais devoir évaluer les avantages et les inconvénients de chaque méthode.
Erik Funkenbusch

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

1
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. il devra être modifié.
esac

1
Le point n ° 2 est également quelque chose que vous devrez prendre en considération. Je stocke mon objet 'Connection' dans le contexte SocketAsyncEventArgs. Cela signifie que je n'ai qu'un seul tampon de réception par connexion. Je ne publie pas d'autre réception avec ce SocketAsyncEventArgs jusqu'à ce que DataReceived soit terminé, donc aucune autre donnée ne peut être lue à ce sujet jusqu'à ce qu'elle soit complète. JE AVIS AVIS de ne pas effectuer de longues opérations sur ces données. En fait, je déplace tout le tampon de toutes les données reçues sur une file d'attente sans verrouillage, puis je le traite sur un thread séparé. Cela garantit une faible latence sur la partie réseau.
esac

1
Par ailleurs, j'ai écrit des tests unitaires et des tests de charge pour ce code, et en augmentant la charge utilisateur de 1 utilisateur à 250 utilisateurs (sur un système double cœur unique, 4 Go de RAM), le temps de réponse pour 100 octets (1 paquet) et 10000 octets (3 paquets) sont restés les mêmes tout au long de la courbe de charge utilisateur.
esac

46

Il y avait une très bonne discussion sur le TCP / IP évolutif utilisant .NET écrit par Chris Mullins de Coversant, malheureusement, il semble que son blog ait disparu de son emplacement précédent, donc je vais essayer de rassembler ses conseils de mémoire (quelques commentaires utiles de lui apparaissent dans ce fil: C ++ vs C #: Développement d'un serveur IOCP hautement évolutif )

Tout d'abord, notez que l'utilisation Begin/Endet les Asyncméthodes de la Socketclasse utilisent les ports d'achèvement IO (IOCP) pour fournir l'évolutivité. Cela fait une bien plus grande différence (lorsqu'elle est utilisée correctement; voir ci-dessous) pour l'évolutivité que celle des deux méthodes que vous choisissez réellement pour implémenter votre solution.

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

IOCP versus thread par connexion ou primitives de sélection

La raison pour laquelle vous souhaitez 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 IO que vous essayez de lire ( notez que l'IOCP peut également être utilisé pour les E / S de fichiers). L'avantage de ceci est que Windows n'a pas besoin de basculer vers un thread uniquement 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 requis.

Les changements de contexte sont ce qui tuera définitivement 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 est cependant loin d'être «évolutif».

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

Mémoire

Tout d'abord, il est essentiel de comprendre que l'IOCP peut facilement entraîner des problèmes de mémoire sous .NET si votre implémentation est trop naïve. Chaque BeginReceiveappel IOCP entraînera "l'épinglage" du tampon dans lequel vous lisez. Pour une bonne explication des raisons pour lesquelles il s'agit d'un problème, voir: Weblog de Yun Jin: OutOfMemoryException and Pinning .

Heureusement, ce problème peut être évité, mais il nécessite un peu de compromis. La solution suggérée est d'allouer une grande byte[]mémoire tampon au démarrage de l'application (ou à proximité), d'au moins 90 Ko ou plus (à partir de .NET 2, la taille requise peut être plus grande dans les versions ultérieures). La raison à cela est que les allocations de mémoire volumineuses se retrouvent automatiquement dans un segment de mémoire non compacté (le tas d'objets volumineux) qui est effectivement épinglé automatiquement. En allouant une grande mémoire tampon au démarrage, vous vous assurez que ce bloc de mémoire inamovible se trouve à une «adresse faible» où il ne gênera pas et ne provoquera pas de fragmentation.

Vous pouvez ensuite utiliser des décalages pour segmenter cette grande mémoire tampon en zones distinctes pour chaque connexion qui a besoin de lire certaines données. C'est là 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 souhaitez définir sur le nombre de connexions que vous souhaitez mettre à l'échelle (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 consisterait à affecter à chaque connexion un seul octet à un décalage unique dans ce tampon. Ensuite, vous pouvez faire un BeginReceiveappel pour qu'un seul octet soit lu, et effectuer le reste de la lecture à la suite du rappel que vous obtenez.

En traitement

Lorsque vous obtenez le rappel de l' Beginappel que vous avez effectué, il est très important de réaliser que le code du rappel s'exécutera sur le thread IOCP de bas niveau. Il est absolument essentiel que vous évitiez de longues opérations dans ce rappel. L'utilisation de ces threads pour un traitement complexe tuera votre évolutivité tout aussi efficacement que l'utilisation de 'thread-per-connection'.

La solution suggérée consiste à utiliser le rappel uniquement pour mettre en file d'attente un élément de travail pour traiter les données entrantes, qui seront exécutées sur un autre thread. Évitez toute opération potentiellement bloquante à l'intérieur du rappel afin que le thread IOCP puisse retourner dans son pool le plus rapidement possible. Dans .NET 4.0, je suggérerais que la solution la plus simple consiste à générer un Task, en lui donnant une référence au socket client et une copie du premier octet déjà lu par l' BeginReceiveappel. Cette tâche est ensuite chargée de lire toutes les données du socket qui représentent la demande que vous traitez, de l'exécuter, puis de passer un nouvel BeginReceiveappel pour mettre à nouveau le socket en file d'attente pour IOCP. Avant .NET 4.0, vous pouvez utiliser ThreadPool ou créer votre propre implémentation de file d'attente de travail threadée.

Résumé

Fondamentalement, je suggérerais d'utiliser l'exemple de code de Kevin pour cette solution, avec les avertissements ajoutés suivants:

  • Assurez-vous que le tampon que vous passez BeginReceiveest déjà `` épinglé ''
  • Assurez-vous que le rappel que vous passez BeginReceivene fait rien de plus que mettre en file d'attente une tâche pour gérer le traitement réel des données entrantes

Lorsque vous faites cela, je ne doute pas que vous puissiez répliquer les résultats de Chris en augmentant potentiellement des centaines de milliers de clients simultanés (avec le bon matériel et une implémentation efficace de votre propre code de traitement bien sûr;)


1
Pour épingler un bloc de mémoire plus petit, la méthode Alloc d'objet GCHandle peut être utilisée pour épingler le tampon. Une fois que cela est fait, le 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 ();
Bob Bryan

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

Eh bien, le fait est que vous n'avez pas à allouer un gros bloc pour le garder épinglé en mémoire. Vous pouvez allouer des blocs plus petits et utiliser la technique ci-dessus pour les épingler en mémoire afin d'éviter que le gc les déplace. Vous pouvez conserver une référence à chacun des petits blocs, tout comme vous gardez une référence à un seul bloc plus grand, et les réutiliser si nécessaire. L'une ou l'autre approche est valide - je faisais juste remarquer que vous n'avez pas besoin d'utiliser un très grand tampon. Mais, cela dit, utiliser parfois un très grand tampon est la meilleure façon de procéder, car le gc le traitera plus efficacement.
Bob Bryan

@BobBryan puisque l'épinglage du tampon se produit automatiquement lorsque vous appelez BeginReceive, l'épinglage n'est pas vraiment le point saillant ici; l'efficacité était;) ... et c'est particulièrement un problème lorsque l'on essaie d'écrire un serveur évolutif, d'où la nécessité d'allouer de gros blocs à utiliser pour l'espace tampon.
jerryjvl

@jerryjvl Désolé de soulever une question très ancienne, mais j'ai récemment découvert ce problème exact avec les méthodes asynchrones BeginXXX / EndXXX. C'est un excellent article, mais il a fallu beaucoup de fouilles pour le trouver. J'aime votre solution suggérée mais je n'en comprends pas une partie: "Ensuite, vous pouvez faire un appel BeginReceive pour qu'un seul octet soit lu, et effectuer le reste de la lecture à la suite du rappel que vous obtenez." Qu'entendez-vous par effectuer le reste de la préparation à la suite du rappel que vous recevez?
Mausimo le

22

Vous avez déjà obtenu l'essentiel de la réponse via les exemples de code ci-dessus. L'utilisation d'un fonctionnement d'E / S asynchrone est absolument la voie à suivre ici. Async IO est la façon dont le Win32 est conçu en interne pour évoluer. Les meilleures performances possibles que vous pouvez obtenir sont obtenues en utilisant les ports d'achèvement, en liant vos sockets aux ports d'achèvement et en ayant un pool de threads en attente de l'achèvement du port d'achèvement. La sagesse commune est d'avoir 2 à 4 threads par CPU (cœur) en attente de fin. Je recommande vivement de passer en revue ces trois articles de Rick Vicik de l'équipe Windows Performance:

  1. Concevoir des applications pour la performance - Partie 1
  2. Concevoir des applications pour la performance - Partie 2
  3. Concevoir des applications pour la performance - Partie 3

Lesdits articles couvrent principalement l'API Windows native, mais ils sont indispensables pour quiconque tente de comprendre l'évolutivité et les performances. Ils ont également des mémoires sur le côté géré des choses.

La deuxième chose à faire est de vous assurer de consulter le livre Amélioration des performances et de l'évolutivité des applications .NET , disponible en ligne. Vous trouverez des conseils pertinents et valables sur l'utilisation des threads, des appels asynchrones et des verrous au chapitre 5. Mais les vrais joyaux se trouvent au chapitre 17 où vous trouverez des avantages tels que des conseils pratiques sur le réglage de votre pool de threads. Mes applications ont eu de sérieux problèmes jusqu'à ce que j'aie ajusté les maxIothreads / maxWorkerThreads selon les recommandations de ce chapitre.

Vous dites que vous voulez faire un serveur TCP pur, donc mon prochain point est faux. Cependant , si vous vous trouvez coincé et utilisez la classe WebRequest et ses dérivés, sachez qu'un dragon garde cette porte: le 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 du ServicePoint.ConnectionLimit artificiel imposé ou votre application ne sera jamais mise à l'échelle (je vous laisse découvrir par vous-même quelle est la valeur par défaut ...). Vous pouvez également reconsidérer la politique par défaut d'envoi d'un en-tête Expect100Continue dans les requêtes http.

Maintenant, à propos de l'API gérée par socket de base, les choses sont assez faciles 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 mise à l'é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 devez publier 3-4 tampons et publier de nouveaux tampons dès que vous en récupérez un ( avant de traiter celui qui est revenu) afin de vous assurer que le socket a toujours un endroit où déposer les données provenant du réseau. Vous verrez pourquoi vous ne pourrez probablement pas y parvenir sous peu.

Une fois que vous avez fini de jouer avec l'API BeginRead / BeginWrite et que vous commencez le travail sérieux, vous vous rendrez compte que vous avez besoin de sécurité sur votre trafic, c'est-à-dire. Authentification NTLM / Kerberos et chiffrement du trafic, ou au moins protection contre la falsification du trafic. Pour ce faire, vous utilisez le System.Net.Security.NegotiateStream intégré (ou SslStream si vous avez besoin de traverser des domaines disparates). Cela signifie qu'au lieu de compter sur des opérations asynchrones de socket direct, vous vous ferez aux opérations asynchrones AuthenticatedStream. Dès que vous obtenez un socket (soit de la connexion sur le client, soit de l'acceptation sur le serveur), vous créez un flux sur le socket et le soumettez pour authentification, en appelant BeginAuthenticateAsClient ou BeginAuthenticateAsServer. Une fois l'authentification terminée (au moins votre coffre-fort de la folie native InitiateSecurityContext / AcceptSecurityContext ...), vous ferez votre autorisation en vérifiant la propriété RemoteIdentity de votre flux authentifié et en effectuant la vérification ACL que votre produit doit prendre en charge. Après cela, vous enverrez des messages en utilisant BeginWrite et vous les recevrez avec BeginRead. C'est le problème dont je parlais auparavant: vous ne pourrez pas publier plusieurs tampons de réception, car les classes AuthenticateStream ne le prennent pas en charge. L'opération BeginRead gère en interne tous les E / S jusqu'à ce que vous ayez reçu une trame entière, sinon elle ne pourrait pas gérer l'authentification du message (décrypter la trame et valider 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 avoir de problème avec cela. C'est à dire. vous devriez pouvoir saturer le réseau GB 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 aider à démarrer sur la bonne voie. Je ne vais pas publier de code ici, il y a un très bon exemple sur MSDN . J'ai réalisé de nombreux projets comme celui-ci et j'ai pu passer à environ 1000 utilisateurs connectés sans problème. Au-dessus de cela, vous devrez modifier les clés de registre pour permettre au noyau d'avoir plus de poignées de socket. et assurez-vous de déployer sur un OS serveur , c'est-à-dire W2K3 et non XP ou Vista (c'est-à-dire OS client), cela fait une grande différence.

BTW assurez-vous que si vous avez des opérations de bases de données sur le serveur ou le fichier IO, vous utilisez également la saveur asynchrone pour eux, ou vous viderez le pool de threads en un rien de temps. Pour les connexions SQL Server, assurez-vous d'ajouter le «traitement asynchrone = true» à la chaîne de connexion.


Il y a de bonnes informations ici. J'aimerais pouvoir attribuer la prime à plusieurs personnes. Cependant, je vous ai voté pour. Bon truc ici, merci.
Erik Funkenbusch

11

J'ai un tel serveur en cours d'exécution dans certaines de mes solutions. Voici une explication très détaillée des différentes façons de le faire dans .net: Rapprochez-vous du fil avec des sockets hautes performances dans .NET

Dernièrement, j'ai cherché des moyens d'améliorer notre code et je me pencherai sur ceci: " Améliorations des performances de socket dans la version 3.5 " qui a été inclus spécifiquement "pour une utilisation par les applications qui utilisent des E / S réseau asynchrones pour atteindre les meilleures performances".

«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 asynchrone à volume élevé. Le modèle de conception Début / Fin actuellement implémenté par la classe Socket pour les E / S de socket asynchrone nécessite un système. L’objet IAsyncResult doit être alloué pour chaque opération de socket asynchrone. »

Vous pouvez continuer à lire si vous suivez le lien. Je testerai personnellement leur exemple de code demain pour le comparer à ce que j'ai.

Edit: Ici, vous pouvez trouver du code de travail pour le client et le serveur en utilisant le nouveau 3.5 SocketAsyncEventArgs afin que vous puissiez le tester en quelques minutes et parcourir le code. C'est une approche simple, mais c'est la base pour démarrer une implémentation beaucoup plus large. Aussi cet article d'il y a près de deux ans dans MSDN Magazine est une lecture intéressante.



9

Avez-vous envisagé d'utiliser simplement une liaison TCP réseau WCF et un modèle de publication / abonnement? WCF vous permettrait de vous concentrer [principalement] sur votre domaine plutôt que sur la plomberie.

Il existe de nombreux exemples WCF et même un framework de publication / abonnement disponible dans la section de téléchargement d'IDesign qui peut être utile: http://www.idesign.net


8

Je m'interroge sur une chose:

Je ne veux certainement pas démarrer un fil pour chaque connexion.

Pourquoi donc? Windows pourrait gérer des centaines de threads dans une application depuis au moins Windows 2000. Je l'ai fait, il est vraiment facile de travailler avec si les threads n'ont pas besoin d'être synchronisés. Surtout étant donné que vous faites beaucoup d'E / S (donc vous n'êtes pas lié au processeur, et beaucoup de threads seraient bloqués sur le disque ou la communication réseau), je ne comprends pas cette restriction.

Avez-vous testé la méthode multi-thread et trouvé qu'il manquait quelque chose? Avez-vous l'intention d'avoir également une connexion à la base de données pour chaque thread (cela tuerait le serveur de base de données, c'est donc une mauvaise idée, mais c'est facilement résolu avec une conception à 3 niveaux). Craignez-vous d'avoir des milliers de clients au lieu de centaines, et ensuite vous aurez vraiment des problèmes? (Bien que j'essaye un millier de threads ou même dix mille si j'avais plus de 32 Go de RAM - encore une fois, étant donné que vous n'êtes pas lié au processeur, le temps de changement de thread ne devrait absolument pas être pertinent.)

Voici le code - pour voir à quoi cela ressemble, allez sur http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html et cliquez sur l'image.

Classe de serveur:

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

Programme principal du serveur:

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

Classe de client:

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

Programme principal du client:

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 peut gérer de nombreux threads, mais .NET n'est pas vraiment conçu pour les gérer. Chaque domaine d'application .NET possède un pool de threads et vous ne souhaitez pas épuiser ce pool de threads. Je ne sais pas si vous démarrez un thread manuellement s'il provient ou non du pool de threads. Pourtant, des centaines de threads ne faisant rien la plupart du temps représentent un énorme gaspillage de ressources.
Erik Funkenbusch

1
Je pense que vous avez une vision incorrecte des fils. Les threads ne proviennent du pool de threads que si vous le souhaitez, contrairement aux threads normaux. Des centaines de threads sans rien gaspiller exactement rien :) (Eh bien, un peu de mémoire, mais la mémoire est si bon marché que ce n'est plus vraiment un problème.) Je vais écrire quelques exemples d'applications pour cela, je publierai une URL sur une fois que j'ai terminé. En attendant, je vous recommande de revoir ce que j'ai écrit ci-dessus et d'essayer de répondre à mes questions.
Marcel Popescu

1
Bien que je sois d'accord avec le commentaire de Marcel sur la vue des threads dans lequel les threads créés ne proviennent pas du threadpool, le reste de la déclaration n'est pas correct. La mémoire ne concerne pas la quantité installée sur une machine, toutes les applications sur Windows s'exécutent dans l'espace d'adressage virtuel et sur un système 32 bits qui vous donnent 2 Go de données pour votre application (peu importe la quantité de RAM installée sur la boîte). Ils doivent toujours être gérés par le runtime. Faire l'E / S asynchrone n'utilise pas de thread pour attendre (il utilise IOCP qui permet des E / S superposées) et est une meilleure solution et évoluera BEAUCOUP mieux.
Brian ONeil

7
Lorsque vous exécutez beaucoup de threads, ce n'est pas la mémoire qui pose problème mais le processeur. Le changement de contexte entre les threads est une opération relativement coûteuse et plus vous avez de threads actifs, plus il y aura de changements de contexte. Il y a quelques années, j'ai effectué un test sur mon PC avec une application console C # et avec env. 500 threads mon CPU était à 100%, les threads ne faisaient rien de significatif. Pour les communications réseau, il est préférable de limiter le nombre de threads.
sipwiz

1
J'irais avec une solution de tâche ou utiliserais async / await. La solution de tâche semble plus simple tandis qu'async / await sont probablement plus évolutives (elles étaient spécifiquement destinées aux situations liées aux E / S).
Marcel Popescu

5

Utiliser Async IO ( BeginRead, etc.) intégré à .NET est une bonne idée si vous pouvez obtenir tous les détails correctement. Lorsque vous configurez correctement vos poignées de socket / fichier, il utilisera l'implémentation IOCP sous-jacente du système d'exploitation, permettant à vos opérations de se terminer sans utiliser de threads (ou, dans le pire des cas, en utilisant un thread qui, je pense, provient du pool de threads IO du noyau à la place. du pool de threads de .NET, ce qui permet de réduire la congestion du pool de threads.)

Le principal problème est de vous assurer que vous ouvrez vos sockets / fichiers en mode non bloquant. La plupart des fonctions pratiques par défaut (comme File.OpenRead) ne le font pas, vous devrez donc écrire les vôtres.

L'une des autres principales préoccupations est la gestion des erreurs - gérer correctement les erreurs lors de l'écriture de code d'E / S asynchrone est beaucoup, beaucoup plus difficile que de le faire en code synchrone. Il est également très facile de se retrouver avec des conditions de concurrence et des blocages même si vous n'utilisez peut-être pas directement les threads, vous devez donc en être conscient.

Si possible, vous devriez essayer d'utiliser une bibliothèque pratique pour faciliter le processus d'E / S asynchrones évolutives.

Le runtime de coordination de la concurrence de Microsoft est un exemple de bibliothèque .NET conçue pour soulager la difficulté de faire ce type de programmation. Cela a l'air génial, mais comme je ne l'ai pas utilisé, je ne peux pas dire à quel point il évoluerait.

Pour mes projets personnels nécessitant des E / S réseau ou disque asynchrones, j'utilise un ensemble d'outils de concurrence / E / S .NET que j'ai construits au cours de l'année écoulée, appelé Squared.Task . Il est inspiré par des bibliothèques comme imvu.task et twisted , et j'ai inclus quelques exemples de travail dans le référentiel qui font des E / S réseau. Je l'ai également utilisé dans quelques applications que j'ai écrites - la plus grande publication publique étant NDexer (qui l'utilise pour les E / S de disque sans thread). La bibliothèque a été écrite sur la base de mon expérience avec imvu.task et dispose d'un ensemble de tests unitaires assez complets, je vous encourage donc vivement à l'essayer. Si vous rencontrez des problèmes, je serais ravi de vous aider.

À mon avis, sur la base de mon expérience avec l'utilisation d'E / S asynchrones / sans thread au lieu de threads, c'est une entreprise intéressante sur la plate-forme .NET, tant que vous êtes prêt à gérer la courbe d'apprentissage. Cela vous permet d'éviter les tracas d'évolutivité imposés par le coût des objets Thread, et dans de nombreux cas, vous pouvez complètement éviter l'utilisation de verrous et de mutex en utilisant avec soin les primitives de concurrence comme Futures / Promises.


Excellente info, je vais vérifier vos références et voir ce qui a du sens.
Erik Funkenbusch

3

J'ai utilisé la solution de Kevin mais il dit que la solution manque de code pour le réassemblage des messages. Les développeurs peuvent utiliser ce code pour le réassemblage des messages:

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

Vous pouvez essayer d'utiliser un framework appelé ACE (Adaptive Communications Environment) qui est un framework C ++ générique pour les serveurs réseau. Il s'agit d'un produit très solide et mature, conçu pour prendre en charge des applications à haut volume et à haute fiabilité jusqu'à la qualité des télécommunications.

Le cadre traite un assez large éventail de modèles de concurrence et en a probablement un adapté à votre application. Cela devrait faciliter le débogage du système car la plupart des problèmes de concurrence désagréables ont déjà été résolus. Le compromis ici est que le framework est écrit en C ++ et n'est pas la base de code la plus chaleureuse et la plus moelleuse. D'autre part, vous bénéficiez d'une infrastructure réseau testée de qualité industrielle et d'une architecture hautement évolutive prête à l'emploi.


2
C'est une bonne suggestion, mais d'après les balises de la question, je pense que l'OP utilisera C #
JPCosta

J'ai remarqué ça; la suggestion était que cela est disponible pour C ++ et je ne connais rien d'équivalent pour C #. Déboguer ce type de système n'est pas facile dans le meilleur des cas et vous pouvez obtenir un retour en allant vers ce framework même si cela signifie passer au C ++.
ConcernedOfTunbridgeWells

Oui, c'est C #. Je recherche de bonnes solutions basées sur .net. J'aurais dû être plus clair, mais je supposais que les gens
liraient


1

Eh bien, les sockets .NET semblent fournir select () - c'est le meilleur pour gérer les entrées. Pour la sortie, j'aurais un pool de threads d'écriture de socket écoutant sur une file d'attente de travail, acceptant le descripteur / objet de socket dans le cadre de l'élément de travail, vous n'avez donc pas besoin d'un thread par socket.


1

J'utiliserais les méthodes AcceptAsync / ConnectAsync / ReceiveAsync / SendAsync qui ont été ajoutées dans .Net 3.5. J'ai fait un benchmark et ils sont environ 35% plus rapides (temps de réponse et débit) avec 100 utilisateurs qui envoient et reçoivent constamment des données.


1

aux personnes qui copient la réponse acceptée, vous pouvez réécrire la méthode acceptCallback, en supprimant tous les appels de _serverSocket.BeginAccept (new AsyncCallback (acceptCallback), _serverSocket); et placez-le dans une clause finally {}, de cette façon:

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

vous pouvez même supprimer la première capture car son contenu est le même mais c'est une méthode de modèle et vous devez utiliser une exception typée pour mieux gérer les exceptions et comprendre ce qui a causé l'erreur, alors implémentez simplement ces captures avec du code utile



-1

Pour être clair, je recherche des solutions basées sur .net (C # si possible, mais n'importe quel langage .net fonctionnera)

Vous n'obtiendrez pas le plus haut niveau d'évolutivité si vous optez uniquement pour .NET. Les pauses GC peuvent entraver la latence.

Je vais avoir besoin de démarrer au moins un thread pour le service. J'envisage d'utiliser l'API Asynch (BeginRecieve, etc.) car je ne sais pas combien de clients j'aurai connectés à un moment donné (peut-être des centaines). Je ne veux certainement pas démarrer un fil pour chaque connexion.

Les E / S superposées sont généralement considérées comme l'API la plus rapide de Windows pour la communication réseau. Je ne sais pas si c'est la même chose que votre API Asynch. N'utilisez pas select car chaque appel doit vérifier chaque socket ouvert au lieu d'avoir des rappels sur les sockets actifs.


1
Je ne comprends pas votre commentaire de pause GC. Je n'ai jamais vu un système avec des problèmes d'évolutivité directement liés à GC.
markt

4
Il est beaucoup plus probable que vous construisiez une application qui ne puisse pas évoluer en raison d'une architecture médiocre que parce que GC existe. D'énormes systèmes évolutifs et performants ont été construits avec .NET et Java. Dans les deux liens que vous avez donnés, la cause n'était pas directement le garbage collection .. mais lié à l'échange de tas. Je soupçonne que c'est vraiment un problème d'architecture qui aurait pu être évité. Si vous pouvez me montrer un langage qu'il n'est pas possible de construire un système qui ne peut pas évoluer, je l'utiliserai volontiers;)
markt

1
Je ne suis pas d'accord avec ce commentaire. Inconnu, les questions auxquelles vous faites référence sont Java, et elles traitent spécifiquement d'allocations de mémoire plus importantes et essaient de forcer manuellement gc. Je ne vais pas vraiment avoir d'énormes allocations de mémoire ici. Ce n'est tout simplement pas un problème. Mais merci. Oui, le modèle de programmation asynchrone est généralement implémenté au-dessus des E / S superposées.
Erik Funkenbusch

1
En fait, la meilleure pratique consiste à ne pas forcer constamment manuellement le GC à collecter. Cela pourrait très bien aggraver les performances de votre application. Le .NET GC est un GC générationnel qui s'accordera à l'utilisation de votre application. Si vous pensez vraiment que vous devez appeler manuellement GC.Collect, je dirais que votre code doit probablement être écrit d'une autre manière ..
markt

1
@markt, c'est un commentaire pour les personnes qui ne connaissent vraiment rien au ramassage des ordures. Si vous avez du temps d'inactivité, il n'y a rien de mal à faire une collecte manuelle. Cela n'aggravera pas votre application une fois terminée. Les articles académiques montrent que les CG générationnels fonctionnent parce que c'est une approximation de la durée de vie de vos objets. Ce n'est évidemment pas une représentation parfaite. En fait, il existe un paradoxe où la génération «la plus ancienne» a souvent le taux de déchets le plus élevé car elle n'est jamais ramassée.
Inconnu

-1

Vous pouvez utiliser l'infrastructure open source Push Framework pour le développement de serveurs hautes performances. Il est construit sur IOCP et convient aux scénarios push et à la diffusion de messages.

http://www.pushframework.com


1
Ce message a été étiqueté C # et .net. Pourquoi avez-vous suggéré un framework C ++?
Erik Funkenbusch

Probablement parce qu'il l'a écrit. potatosoftware.com/…
quillbreaker

Pushframework prend-il en charge plusieurs instances de serveur? sinon, comment évolue-t-il?
esskar
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.