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

Je suis en phase de conception pour écrire une nouvelle application de service Windows qui accepte les connexions TCP / IP pour les connexions de longue durée (c.-à-d. même des semaines).

Je cherche des idées pour la meilleure façon de concevoir l’architecture réseau. Je vais devoir 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 je vais avoir connecté à un moment donné (peut-être des centaines). Je ne veux certainement pas démarrer un thread pour chaque connexion.

Les données seront principalement transmises aux clients depuis mon serveur, mais des commandes seront parfois 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.

Des suggestions sur la meilleure façon de rendre cela aussi évolutif que possible? Workflow de base? Merci.

EDIT: Pour être clair, je recherche des solutions basées sur .net (si possible C #, mais toute langue .net fonctionnera)

NOTE BOUNTY: Pour recevoir la prime, je m’attends à plus qu’une réponse simple. J’aurais besoin d’un exemple concret d’une solution, soit en tant que 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 veux remercier tous ceux qui ont donné de bonnes réponses. Malheureusement, je ne pouvais en accepter qu’un, et j’ai choisi d’accepter la méthode Begin / End plus connue. La solution d’Esac pourrait bien être meilleure, mais c’est encore assez nouveau pour que je ne sache pas exactement comment cela fonctionnera.

J’ai relevé toutes les réponses que je pensais être bonnes, j’aimerais pouvoir faire plus pour vous les gars. Merci encore.

J’ai écrit quelque chose de similaire par le passé. Depuis ma recherche, il y a des années, la meilleure solution consistait à écrire votre propre implémentation de socket en utilisant les sockets asynchrones. Cela signifiait que les clients ne faisant pas vraiment quoi que ce soit demandaient 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 importantes, vous pouvez l’écrire comme vous le souhaitez.

private List _sockets; 

En outre, vous avez besoin de la socket pour écouter 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 éventuelles 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 a l’air mauvais, mais la raison en est que j’avais un code de suppression des exceptions pour que toutes les exceptions soient supprimées et renvoyer false si une option de configuration était définie, mais je voulais le supprimer par souci de brièveté.

Le _serverSocket.BeginAccept (nouvel AsyncCallback (acceptCallback)), _serverSocket) ci-dessus définit essentiellement notre socket de 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 plusieurs 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 juste d’accepter la connexion qui entre, met en queue BeginReceive qui est un rappel qui s’exécutera lorsque le client enverra des données, puis acceptCallback queue le prochain acceptCallback qui acceptera la prochaine connexion client qui arrive.

L’ BeginReceive méthode BeginReceive indique à la socket quoi faire quand elle reçoit des données du client. Pour BeginReceive , vous devez lui atsortingbuer un tableau d’octets, où il copie les données lorsque le client envoie des données. La méthode ReceiveCallback sera appelée, ce qui permet de gérer 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 ce domaine 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); 

Ce que je ferais généralement, c’est dans le code que vous voulez, 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 que le code de traitement des messages est en cours d’exécution.

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 réception de début. Une fois que vous faites ce que vous voulez où j’ai laissé le commentaire, nous appelons la prochaine méthode BeginReceive 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 uniquement être appelé avec une partie du message. Le reassembly peut devenir 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’append. Ce gestionnaire était en fait le morceau de code le plus compliqué que j’avais 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 réalité un appel Send synchrone, qui me convenait parfaitement en raison de la taille des messages et de la nature multithread de mon application. Si vous voulez envoyer à chaque client, vous devez simplement parcourir la liste _sockets.

La classe xConnection que vous voyez référencée ci-dessus est fondamentalement un simple wrapper pour un socket pour inclure le tampon d’octets et, dans mon implémentation, des extras.

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

À titre de référence, mentionnons également l’ using j’utilise depuis que je suis toujours agacé 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 ça marche. Il y a aussi quelques nuances au code que vous devriez être fatigué de changer. D’une part, ne disposer que d’un seul BeginAccept à la fois. Il y avait un bogue .net très agaçant autour de cela, il y a des années, donc je ne me souviens pas des détails.

En outre, dans le code ReceiveCallback , nous traitons tout ce qui est reçu du socket avant de mettre en attente la prochaine réception. Cela signifie que pour une seule socket, nous ne sums en réalité qu’une seule fois dans ReceiveCallback , et nous n’avons pas besoin d’utiliser la synchronisation des threads. Cependant, si vous réorganisez cette opération pour recevoir la prochaine réception immédiatement après avoir extrait les données, ce qui pourrait être un peu plus rapide, vous devrez vous assurer de bien synchroniser les threads.

En outre, j’ai piraté beaucoup de mon code, mais j’ai laissé l’essentiel en place. Cela devrait être un bon début pour votre conception. Laissez un commentaire si vous avez d’autres questions à ce sujet.

Il existe plusieurs façons de faire des opérations réseau en C #. Tous utilisent des mécanismes différents 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 personnes confondent souvent pour être le moyen le plus rapide et 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 apscopes à la classe System.Net.Sockets .. ::. Socket qui fournissent un autre modèle asynchrone pouvant ê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 nécessitant de hautes performances. Une application peut utiliser le modèle asynchrone amélioré uniquement ou uniquement dans des zones actives 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’objects lors des E / S de socket asynchrones à grand volume. Le modèle de conception Begin / End actuellement implémenté par la classe System.Net.Sockets .. ::. Socket nécessite qu’un object System.. ::. IAsyncResult soit alloué pour chaque opération de socket asynchrone.

Sous les couvertures, l’API Async utilise les ports d’achèvement IO, qui constituent le moyen le plus rapide d’effectuer des opérations réseau, voir http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

Et pour vous aider, j’inclus le code source d’un serveur telnet que j’ai écrit en utilisant l’API * Async. Je n’inclus que les portions pertinentes. De plus, au lieu de traiter les données en ligne, je choisis plutôt de les transférer dans une queue libre (sans attente) qui est traitée sur un thread distinct. Notez que je n’inclus pas la classe Pool correspondante qui est juste un pool simple qui créera un nouvel object s’il est vide, et la classe Buffer qui est juste un tampon auto-expansible 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 MP.

  public class Telnet { private readonly Pool m_EventArgsPool; private Socket m_ListenSocket; ///  /// This event fires when a connection has been established. ///  public event EventHandler Connected; ///  /// This event fires when a connection has been shutdown. ///  public event EventHandler Disconnected; ///  /// This event fires when data is received on the socket. ///  public event EventHandler DataReceived; ///  /// This event fires when data is finished sending on the socket. ///  public event EventHandler DataSent; ///  /// This event fires when a line has been received. ///  public event EventHandler LineReceived; ///  /// Specifies the port to listen on. ///  [DefaultValue(23)] public int ListenPort { get; set; } ///  /// Constructor for Telnet class. ///  public Telnet() { m_EventArgsPool = new Pool(); ListenPort = 23; } ///  /// Starts the telnet server listening and accepting data. ///  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); } ///  /// Not Yet Implemented. Should shutdown all connections gracefully. ///  public void Stop() { //throw (new NotImplementedException()); } // // ACCEPT // ///  /// 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. ///  /// null if posted from startup, otherwise a SocketAsyncEventArgs for reuse. 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); } } ///  /// Completion callback routine for the AcceptAsync post. This will verify that the Accept occured /// and then setup a Receive chain to begin receiving data. ///  /// object which posted the AcceptAsync /// Information about the Accept call. 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 // ///  /// Post an asynchronous receive on the socket. ///  /// Used to store information about the Receive call. 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); } } } ///  /// 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. ///  /// object which posted the ReceiveAsync /// Information about the Receive call. 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); } ///  /// Handles Event of Data being Received. ///  /// Information about the received data. protected void OnDataReceived(SocketAsyncEventArgs e) { if (DataReceived != null) { DataReceived(this, e); } } ///  /// Handles Event of a Line being Received. ///  /// User connection. protected void OnLineReceived(Connection connection) { if (LineReceived != null) { int index = 0; int start = 0; while ((index = connection.ReceiveBuffer.IndexOf('\n', index)) != -1) { ssortingng s = connection.ReceiveBuffer.GetSsortingng(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 // ///  /// Overloaded. Sends a ssortingng over the telnet socket. ///  /// Connection to send data on. /// Data to send. /// true if the data was sent successfully. public bool Send(Connection connection, ssortingng s) { if (Ssortingng.IsNullOrEmpty(s) == false) { return Send(connection, Encoding.Default.GetBytes(s)); } return false; } ///  /// Overloaded. Sends an array of data to the client. ///  /// Connection to send data on. /// Data to send. /// true if the data was sent successfully. 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); } ///  /// Sends an array of data to the client. ///  /// Connection to send data on. /// Data to send. /// Starting offset of date in the buffer. /// Amount of data in bytes to send. ///  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; } ///  /// Sends a command telling the client that the server WILL echo data. ///  /// Connection to disable echo on. public void DisableEcho(Connection connection) { byte[] b = new byte[] { 255, 251, 1 }; Send(connection, b); } ///  /// Completion callback for SendAsync. ///  /// object which initiated the SendAsync /// Information about the SendAsync call. private void Send_Completed(object sender, SocketAsyncEventArgs e) { e.Completed -= Send_Completed; m_EventArgsPool.Push(e); } ///  /// Handles a Telnet command. ///  /// Information about the data received. 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); } } } ///  /// Echoes data back to the client. ///  /// Information about the received data to be echoed. 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 // ssortingng bs = connection.ReceiveBuffer.ToSsortingng(); 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 // ///  /// Disconnects a socket. ///  ///  /// 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. ///  /// Information about the socket to be disconnected. 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); } ///  /// Marks a specific connection for graceful shutdown. The next receive or send to be posted /// will fail and close the connection. ///  ///  public void Disconnect(Connection connection) { try { connection.Socket.Shutdown(SocketShutdown.Both); } catch (Exception) { } } ///  /// Telnet command codes. ///  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 } ///  /// Telnet command options. ///  internal enum TelnetOption { Echo = 1, SuppressGoAhead = 3, Status = 5, TimingMark = 6, TerminalType = 24, WindowSize = 31, TerminalSpeed = 32, RemoteFlowControl = 33, LineMode = 34, EnvironmentVariables = 36 } } 

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

Avant tout, notez que les deux méthodes, Begin/End et Async sur la classe Socket , utilisent des ports IOC Completion (IOCP) pour assurer l’évolutivité. Cela fait une différence bien plus grande (lorsqu’elle est utilisée correctement, voir ci-dessous) que l’évolutivité des deux méthodes que vous choisissez pour implémenter votre solution.

Les publications de Chris Mullins étaient basées sur l’utilisation de 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 permis de mettre à niveau jusqu’à 10 000 connexions client simultanées sur une machine 32 bits avec 2 Go de mémoire et 100 000 sur une plate-forme 64 bits avec une mémoire suffisante. D’après ma propre expérience de cette technique (quoique loin de ce type de charge), je n’ai aucune raison de douter de ces chiffres indicatifs.

IOCP contre les primitives thread-per-connection ou ‘select’

La raison pour laquelle vous souhaitez utiliser un mécanisme utilisant 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 que le canal IO ne contient pas de données réelles ( notez que IOCP peut également être utilisé pour le fichier IO). L’avantage de ceci est que Windows n’a pas besoin de passer à un thread pour constater qu’il n’y a pas encore de données de toute façon, ce qui réduit le nombre de changements de contexte que votre serveur devra apporter au ssortingct minimum requirejs.

Les changements de contexte sont ce qui va certainement tuer le mécanisme «thread-per-connection», bien que ce soit une solution viable si vous ne traitez que de quelques dizaines de connexions. Ce mécanisme n’est cependant pas une imagination «évolutive».

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

Mémoire

Avant toute chose, il est essentiel de comprendre que IOCP peut facilement entraîner des problèmes de mémoire sous .NET si votre implémentation est trop naïve. Chaque appel IOCP BeginReceive se traduira par “épinglage” du tampon dans BeginReceive vous lisez. Pour une bonne explication de la raison pour laquelle cela pose problème, voir: Weblog de Yun Jin: OutOfMemoryException et Pinning .

Heureusement, ce problème peut être évité, mais cela nécessite un compromis. La solution suggérée consiste à allouer un grand tampon d’ byte[] au démarrage de l’application (ou à proximité), d’au moins 90 Ko (à partir de .NET 2, la taille requirejse peut être plus grande dans les versions ultérieures). La raison en est que les allocations de mémoire volumineuses se terminent automatiquement dans un segment de mémoire non compactant (The Large Object Heap) qui est automatiquement épinglé. En allouant un grand tampon au démarrage, vous vous assurez que ce bloc de mémoire non déplaçable se trouve à une «adresse basse» où il ne gênera pas la fragmentation.

Vous pouvez ensuite utiliser des décalages pour segmenter ce gros tampon en zones distinctes pour chaque connexion devant lire certaines données. C’est là qu’un compromis entre en jeu; Étant donné que ce tampon doit être pré-alloué, vous devrez décider de l’espace tampon dont vous avez besoin par connexion et de la limite supérieure que vous souhaitez définir pour le nombre de connexions à 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 à atsortingbuer à chaque connexion un seul octet à un seul décalage dans ce tampon. Vous pouvez ensuite lancer un appel BeginReceive pour lire un seul octet et effectuer le rest de la lecture en raison du rappel que vous obtenez.

En traitement

Lorsque vous recevez le rappel de l’appel Begin vous avez effectué, il est très important de comprendre que le code dans le rappel s’exécutera sur le thread IOCP de bas niveau. Il est absolument essentiel d’éviter 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 queue un élément de travail pour traiter les données entrantes, qui seront exécutées sur un autre thread. Évitez les opérations potentiellement bloquantes dans le rappel afin que le thread IOCP puisse retourner à son pool aussi rapidement que possible. Dans .NET 4.0, je suggérerais que la solution la plus simple consiste à générer une Task en lui donnant une référence au socket client et une copie du premier octet déjà lu par l’appel BeginReceive . Cette tâche est alors chargée de lire toutes les données du socket représentant la requête que vous BeginReceive , de les exécuter, puis de lancer un nouvel appel BeginReceive pour mettre en queue le socket pour IOCP. Pre .NET 4.0, vous pouvez utiliser le ThreadPool ou créer votre propre implémentation de queue de travail.

Résumé

Fondamentalement, je suggère d’utiliser le code exemple 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 rappel que vous passez à BeginReceive ne fait que mettre en queue une tâche pour gérer le traitement réel des données entrantes

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

You already got the most part of the answer via the code samples above. Using asynchronous IO operation is absolutely the way to go here. Async IO is the way the Win32 is designed internally to scale. The best possible performance you can get is achieved using Completion Ports, binding your sockets to completion ports and have a thread pool waiting for completion port completion. The common wisdom is to have 2-4 threads per CPU(core) waiting for completion. I highly recommend to go over these three articles by Rick Vicik from the Windows Performance team:

  1. Designing Applications for Performance – Part 1
  2. Designing Applications for Performance – Part 2
  3. Designing Applications for Performance – Part 3

The said articles cover mostly the native Windows API, but they are a must read for anyone trying to get a grasp at scalability and performance. They do have some briefs on the managed side of things too.

Second thing you’ll need to do is make sure you go over the Improving .NET Application Performance and Scalability book, that is available online. You will find pertinent and valid advice around the use of threads, asynchronous calls and locks in Chapter 5. But the real gems are in Chapter 17 where you’ll find such goodies as practical guidance on tuning your thread pool. My apps had some serious problems until I adjusted the maxIothreads/maxWorkerThreads as per the recommendations in this chapter.

You say that you want to do a pure TCP server, so my next point is spurious. However , if you find yourself cornered and use the WebRequest class and its derivatives, be warned that there is a dragon guarding that door: the ServicePointManager . This is a configuration class that has one purpose in life: to ruin your performance. Make sure you free your server from the artificial imposed ServicePoint.ConnectionLimit or your application will never scale (I let you discover urself what is the default value…). You may also reconsider the default policy of sending an Expect100Continue header in the http requests.

Now about the core socket managed API things are fairly easy on the Send side, but they are significantly more complex on the Receive side. In order to achieve high throughput and scale you must ensure that the socket is not flow controlled because you do not have a buffer posted for receive. Ideally for high performance you should post ahead 3-4 buffers and post new buffers as soon as you get one back ( before you process the one got back) so you ensure that the socket always has somewhere to deposit the data coming from the network. You’ll see why you probably won’t be able to achieve this shortly.

After you’re done playing with the BeginRead/BeginWrite API and start the serious work you’ll realize that you need security on your traffic, ie. NTLM/Kerberos authentication and traffic encryption, or at least traffic tampering protection. The way you do this is you use the built in System.Net.Security.NegotiateStream (or SslStream if you need to go cross disparate domains). This means that instead of relying on straight socket asynchronous operations you will rely on the AuthenticatedStream asynchronous operations. As soon as you obtain a socket (either from connect on client or from accept on server) you create a stream on the socket and submit it for authentication, by calling either BeginAuthenticateAsClient or BeginAuthenticateAsServer. After the authentication completes (at least your safe from the native InitiateSecurityContext/AcceptSecurityContext madness…) you will do your authorization by checking the RemoteIdentity property of your Authenticated stream and doing whatever ACL verification your product must support. After that you will send messages using the BeginWrite and you’ll be receiving them with BeginRead. This is the problem I was talking before that you won’t be able to post multiple receive buffers, because the AuthenticateStream classes don’t support this. The BeginRead operation manages internally all the IO until you have received an entire frame, otherwise it could not handle the the message authentication (decrypt frame and validate signature on frame). Though in my experience the job done by the AuthenticatedStream classes is fairly good and shouldn’t have any problem with it. C’est à dire. you should be able to saturate GB network with only 4-5% CPU. The AuthenticatedStream classes will also impose on you the protocol specific frame size limitations (16k for SSL, 12k for Kerberos).

This should get you started on the right track. I’m not going to post code here, there is a perfectly good example on MSDN . I’ve done many projects like this and I was able to scale to about 1000 users connected without problems. Above that you’ll need to modify registry keys to allow the kernel for more socket handles. and make sure you deploy on a server OS, that is W2K3 not XP or Vista (ie. client OS), it makes a big difference.

BTW make sure if you have databases operations on the server or file IO you also use the async flavor for them, or you’ll drain the thread pool in no time. For SQL Server connections make sure you add the ‘Asyncronous Processing=true’ to the connection ssortingng.

I’ve got such a server running in some of my solutions. Here is a very detail explanation of the different ways to do it in .net: Get Closer to the Wire with High-Performance Sockets in .NET

Lately I’ve been looking for ways to improve our code and will be looking into this: ” Socket Performance Enhancements in Version 3.5 ” that was included specifically “for use by applications that use asynchronous network I/O to achieve the highest performance”.

“The main feature of these enhancements is the avoidance of the repeated allocation and synchronization of objects during high-volume asynchronous socket I/O. The Begin/End design pattern currently implemented by the Socket class for asynchronous socket I/O requires a System.IAsyncResult object be allocated for each asynchronous socket operation.”

You can keep reading if you follow the link. I personally will be testing their sample code tomorrow to benchmark it against what i’ve got.

Edit: Here you can find working code for both client and server using the new 3.5 SocketAsyncEventArgs so you can test it within a couple minutes and go thru the code. It is a simple approach but is the basis for starting a much larger implementation. Also this article from almost two years ago in MSDN Magazine was a interesting read.

Have you considered just using a WCF net TCP binding and a publish/subscribe pattern ? WCF would allow you to focus [mostly] on your domain instead of plumbing..

There are lots of WCF samples & even a publish/subscribe framework available on IDesign’s download section which may be useful : http://www.idesign.net

I am wondering about one thing:

I definitely do not want to start a thread for each connection.

Pourquoi donc? Windows could handle hundreds of threads in an application since at least Windows 2000. I’ve done it, it’s really easy to work with if the threads don’t need to be synchronized. Especially given that you’re doing a lot of I/O (so you’re not CPU-bound, and a lot of threads would be blocked on either disk or network communication), I don’t understand this ressortingction.

Have you tested the multi-threaded way and found it lacking in something? Do you intend to also have a database connection for each thread (that would kill the database server, so it’s a bad idea, but it’s easily solved with a 3-tier design). Are you worried that you’ll have thousands of clients instead of hundreds, and then you’ll really have problems? (Though I’d try a thousand threads or even ten thousand if I had 32+ GB of RAM – again, given that you’re not CPU bound, thread switch time should be absolutely irrelevant.)

Here is the code – to see how this looks running, go to http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html and click on the picture.

Server class:

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

Server main program:

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

Client class:

  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.GetSsortingng(msg, 0, i); Console.WriteLine("Received: {0}", data); } } } catch (SocketException e) { Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e); } } } 

Client main program:

 using System; using System.Threading; namespace ManyThreadsClient { internal class Program { private static void Main(ssortingng[] 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(); } } } 

Using .NET’s integrated Async IO ( BeginRead , etc) is a good idea if you can get all the details right. When you properly set up your socket/file handles it will use the OS’s underlying IOCP implementation, allowing your operations to complete without using any threads (or, in the worst case, using a thread that I believe comes from the kernel’s IO thread pool instead of .NET’s thread pool, which helps alleviate threadpool congestion.)

The main gotcha is to make sure that you open your sockets/files in non-blocking mode. Most of the default convenience functions (like File.OpenRead ) don’t do this, so you’ll need to write your own.

One of the other main concerns is error handling – properly handling errors when writing asynchronous I/O code is much, much harder than doing it in synchronous code. It’s also very easy to end up with race conditions and deadlocks even though you may not be using threads directly, so you need to be aware of this.

If possible, you should try and use a convenience library to ease the process of doing scalable asynchronous IO.

Microsoft’s Concurrency Coordination Runtime is one example of a .NET library designed to ease the difficulty of doing this kind of programming. It looks great, but as I haven’t used it, I can’t comment on how well it would scale.

For my personal projects that need to do asynchronous network or disk I/O, I use a set of .NET concurrency/IO tools that I’ve built over the past year, called Squared.Task . It’s inspired by libraries like imvu.task and twisted , and I’ve included some working examples in the repository that do network I/O. I also have used it in a few applications I’ve written – the largest publicly released one being NDexer (which uses it for threadless disk I/O). The library was written based on my experience with imvu.task and has a set of fairly comprehensive unit tests, so I strongly encourage you to try it out. If you have any issues with it, I’d be glad to offer you some assistance.

In my opinion, based on my experience using asynchronous/threadless IO instead of threads is a worthwhile endeavor on the .NET platform, as long as you’re ready to deal with the learning curve. It allows you to avoid the scalability hassles imposed by the cost of Thread objects, and in many cases, you can completely avoid the use of locks and mutexes by making careful use of concurrency primitives like Futures/Promises.

You can find a nice overview of techniques at the C10k problem page .

I used Kevin’s solution but he says that solution lacks code for reassembly of messages. Developers can use this code for reassembly of 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; } } 

You could try using a framework called ACE (Adaptive Communications Environment) which is a generic C++ framework for network servers. It’s a very solid, mature product and is designed to support high-reliability, high-volume applications up to telco-grade.

The framework deals with quite a wide range of concurrency models and probably has one suitable for your applciation out of the box. This should make the system easier to debug as most of the nasty concurrency issues have already been sorted out. The trade-off here is that the framework is written in C++ and is not the most warm and fluffy of code bases. On the other hand, you get tested, indussortingal grade network infrastructure and a highly scalable architecture out of the box.

I would use SEDA or a lightweight threading library (erlang or newer linux see NTPL scalability on the server side ). Async coding is very cumbersome if your communication isn’t 🙂

Well, .NET sockets seem to provide select() – that’s best for handling input. For output I’d have a pool of socket-writer threads listening on a work queue, accepting socket descriptor/object as part of the work item, so you don’t need a thread per socket.

I would use the AcceptAsync/ConnectAsync/ReceiveAsync/SendAsync methods that were added in .Net 3.5. I have done a benchmark and they are approximately 35% faster (response time and bitrate) with 100 users constantly sending and receiving data.

to people copy pasting the accepted answer, you can rewrite the acceptCallback method, removing all calls of _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket); and put it in a finally{} clause, this way:

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

you could even remove the first catch since its content is the same but it’s a template method and you should use typed exception to better handle the exceptions and understand what caused the error, so just implement those catches with some useful code

I would recommend to read these books on ACE

  • C++ Network Programming: Mastering Complexity Using ACE and Patterns
  • C++ Network Programming: Systematic Reuse with ACE and Frameworks

to get ideas about patterns allowing you to create an efficient server.

Although ACE is implemented in C++ the books cover a lot of useful patterns that can be used in any programming language.

To be clear, i’m looking for .net based solutions (C# if possible, but any .net language will work)

You are not going to get the highest level of scalability if you go purely with .NET. GC pauses can hamper the latency.

I’m going to need to start at least one thread for the service. I am considering using the Asynch API (BeginRecieve, etc..) since I don’t know how many clients I will have connected at any given time (possibly hundreds). I definitely do not want to start a thread for each connection.

Overlapped IO is generally considered to be Windows’ fastest API for network communication. I don’t know if this the same as your Asynch API. Do not use select as each call needs to check every socket that is open instead of having callbacks on active sockets.

You can use Push Framework open source framework for high-performance server development. It is built on IOCP and is suitable for push scenarios and message broadcast.

http://www.pushframework.com