Je suis en phase de conception pour l'écriture d'une nouvelle application de service Windows qui accepte les connexions TCP/IP pour les connexions longues même des semaines).
Je cherche des idées sur la meilleure façon de concevoir l'architecture de 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és à un moment donné (éventuellement des centaines). Je ne veux certainement pas commencer un fil pour chaque connexion.
Les données seront principalement transmises aux clients à partir de mon serveur, mais certaines 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? Flux de travail de base? Merci.
EDIT: Pour être clair, je cherche des solutions basées sur .net (C # si possible, mais n'importe quel langage .net fonctionnera)
NOTE DE BOUNTY: Pour recevoir la prime, j'attends plus qu'une réponse simple. J'aurais besoin d'un exemple concret de solution, sous forme de pointeur sur quelque chose que je pourrais télécharger ou d'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, et j’ai choisi d’accepter la méthode Begin/End la plus connue. La solution d'Esac est peut-être meilleure, mais elle est encore suffisamment nouvelle pour que je ne sache pas exactement comment cela fonctionnera.
J'ai voté toutes les réponses que je trouvais bonnes, j'aimerais pouvoir en faire plus pour vous. Merci encore.
J'ai écrit quelque chose de similaire à cela dans le passé. Il ressort de mes recherches il y a de nombreuses années que l'écriture de votre propre implémentation de socket était la meilleure solution, en utilisant les sockets asynchrones. Cela signifiait que les clients ne faisant rien réellement ne demandaient en réalité que 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 les listes plus volumineuses, vous pouvez l'écrire comme vous le souhaitez.
private List<xConnection> _sockets;
De plus, vous avez besoin que le socket écoute réellement pour les connexions entrantes.
private System.Net.Sockets.Socket _serverSocket;
La méthode de démarrage démarre réellement le socket du serveur et commence à écouter toutes 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’exceptions, de sorte que toutes les exceptions seraient supprimées et renverraient false
si une option de configuration était définie, mais Je voulais l'enlever par souci de brièveté.
Le _serverSocket.BeginAccept (nouvel AsyncCallback (acceptCallback)), _serverSocket) définit essentiellement notre socket de serveur pour appeler la méthode acceptCallback chaque fois qu'un utilisateur se connecte. Cette méthode est exécutée à 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 essentiellement d'accepter la connexion entrante. Il met en file d'attente BeginReceive
un rappel qui s'exécutera lorsque le client enverra des données, puis mettra en file d'attente le prochain acceptCallback
qui acceptera le prochain client. connexion qui vient po.
L'appel à la méthode BeginReceive
indique au socket ce qu'il doit faire lorsqu'il reçoit des données du client. Pour BeginReceive
, vous devez lui donner un tableau d'octets, qui est l'endroit où il copiera les données lorsque le client envoie des données. La méthode ReceiveCallback
sera appelée, c'est comment nous traitons 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 en général, c’est que dans le code que vous souhaitiez, c’est réassembler les paquets en messages, puis les créer en tant que travaux sur le pool de threads. Ainsi, la réception du prochain bloc par le client n’est pas retardée tant que le code de traitement du message est en cours d’exécution.
Le rappel accepté termine la lecture du socket de données en appelant la réception finale. Ceci remplit la mémoire tampon fournie dans la fonction begin receive. Une fois que vous avez fait ce que vous voulez là 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 remontage peut devenir très très compliqué. J'ai utilisé ma propre méthode et créé une sorte de protocole propriétaire pour le faire. Je l'ai laissé de côté, mais si vous le souhaitez, je peux l'ajouter. Ce gestionnaire était en fait le morceau de code le plus compliqué que j'ai 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 synchrone Send
, 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 des prises.
La classe xConnection que vous voyez référencée ci-dessus est fondamentalement un wrapper simple pour un socket qui inclut le tampon d'octets et, dans mon implémentation, des extras.
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
Aussi pour référence, voici les using
s que j'inclus depuis que je suis toujours ennuyé 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 dans le code que vous devriez être fatigué de changer. D'une part, un seul BeginAccept
est appelé à la fois. Auparavant, il y avait un bogue .net très ennuyant, il y a des années, donc je ne me souviens pas des détails.
De plus, dans le code ReceiveCallback
, nous traitons tout ce qui est reçu du socket avant de mettre en file d'attente la prochaine réception. Cela signifie que pour un seul socket, nous ne sommes en fait que dans ReceiveCallback
une fois à un moment donné, et nous n'avons pas besoin d'utiliser la synchronisation de threads. Toutefois, si vous réorganisez cette opération pour appeler la prochaine réception immédiatement après l'extraction des données, ce qui pourrait être un peu plus rapide, vous devrez vous assurer de synchroniser correctement les threads.
En outre, j'ai piraté beaucoup de mon code, mais j'ai laissé l'essence de ce qui se passe en place. Cela devrait être un bon début pour votre design. Laissez un commentaire si vous avez d'autres questions à ce sujet.
Il y a beaucoup de 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 simultanéité élevée. Les opérations de début * font partie de celles que beaucoup de gens considèrent souvent comme le moyen le plus rapide de créer des réseaux.
Pour résoudre ces problèmes, ils ont introduit le jeu de méthodes * Async: 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 apportées à la classe System.Net.Sockets.. ::. Socket qui fournit un modèle alternatif 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 de réseau nécessitant des performances élevées. Une application peut utiliser le modèle asynchrone amélioré exclusivement ou uniquement dans des zones sensibles (par exemple, lors de la réception de grandes quantités de données).
La principale caractéristique de ces améliorations consiste à éviter l'allocation et la synchronisation répétées d'objets au cours d'une entrée/sortie de socket asynchrone à haut volume. Le modèle de conception Begin/End actuellement implémenté par la classe System.Net.Sockets.. ::. Socket requiert qu'un objet System .. ::. IAsyncResult soit alloué pour chaque opération de socket asynchrone.
Sous les couvertures, l’API * Async utilise IO ports d’achèvement, qui est le moyen le plus rapide d’effectuer des opérations de 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. Je n'inclus que les parties pertinentes. À noter également, au lieu de traiter les données en ligne, j'ai plutôt opté pour le placer dans une file d'attente sans verrou (attente sans attente) qui est traitée sur un thread séparé. Notez que je n'inclus pas la classe Pool correspondante, qui est simplement un pool qui créera un nouvel objet s'il est vide, et la classe Buffer, qui n'est qu'un tampon auto-extensible qui n'est pas vraiment nécessaire, sauf si vous recevez un message 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<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
}
}
Il y avait autrefois une très bonne discussion sur TCP/IP évolutif avec .NET écrit par Chris Mullins de Coversant. Malheureusement, son blog semble avoir disparu de son emplacement précédent. Je vais donc essayer de reconstituer ses conseils de mémoire (quelques commentaires utiles de son apparaissent dans ce fil de discussion: C++ vs. C #: développement d’un serveur IOCP hautement évolutif )
Tout d’abord, notez que l’utilisation de Begin/End
et les méthodes Async
de la classe Socket
utilisent IO ports d'achèvement (IOCP) pour fournir une évolutivité. Cela fait une différence beaucoup plus grande (lorsque voir ci-dessous) pour l’extensibilité que la méthode que vous choisissez réellement pour implémenter votre solution.
Les messages de Chris Mullins sont basés sur l'utilisation de Begin/End
, qui est celui que j'ai personnellement expérimenté. Notez que Chris a mis au point une solution basée sur cette solution qui a permis l’augmentation de 10 000 connexions client simultanées sur un ordinateur 32 bits doté de 2 Go de mémoire et jusqu’à 100 000 sur une plate-forme 64 bits disposant de suffisamment de mémoire. De par ma propre expérience avec cette technique (même si ce type de charge n’est pas à la hauteur), je n’ai aucune raison de douter de ces chiffres indicatifs.
Si vous souhaitez utiliser un mécanisme qui utilise IOCP sous le capot, c’est qu’il utilise un pool de threads Windows de très bas niveau qui ne réveille aucun thread tant qu’il n’ya pas de données réelles sur le IO canal que vous essayez de lire (notez que IOCP peut être utilisé pour le fichier IO également)). L’avantage de ceci est que Windows n’a pas besoin de passer à un thread pour le trouver. De toute façon, il n’ya pas encore de données, ce qui réduit le nombre de changements de contexte que votre serveur devra effectuer au strict minimum requis.
Le commutateur de contexte est ce qui va définitivement tuer le mécanisme de "thread-per-connection", bien que ce soit une solution viable si vous ne traitez que quelques dizaines de connexions. Ce mécanisme n’est cependant en aucun cas un effort "évolutif".
Mémoire
Avant tout, il est essentiel de comprendre qu'IOCP peut facilement entraîner des problèmes de mémoire sous .NET si votre implémentation est trop naïve. Chaque appel IOCP BeginReceive
se traduira par un "verrouillage" du tampon dans lequel vous lisez. Pour une bonne explication de la cause de ce 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 consiste à allouer un gros byte[]
tampon au démarrage de l’application (ou à proximité de celui-ci), d’au moins 90 Ko (à partir de .NET 2, la taille requise peut être supérieure dans les versions ultérieures). Cela s'explique par le fait que les allocations de mémoire volumineuses aboutissent automatiquement à un segment de mémoire non compactant (le segment d'objet volumineux) effectivement épinglé automatiquement. En allouant une grande mémoire tampon au démarrage, vous vous assurez que ce bloc de mémoire inamovible est à une adresse relativement basse où il ne gênera pas et ne provoquera pas de fragmentation.
Vous pouvez ensuite utiliser des décalages pour segmenter ce grand tampon en zones distinctes pour chaque connexion devant lire certaines données. C'est là qu'intervient un compromis. étant donné que cette mémoire tampon doit être pré-allouée, vous devrez décider de la quantité d'espace de mémoire tampon dont vous avez besoin par connexion et de la limite supérieure que vous souhaitez définir pour le nombre de connexions que vous souhaitez mettre à l'échelle (ou vous pouvez implémenter une abstraction qui peut allouer des tampons supplémentaires épinglés une fois que vous en avez besoin).
La solution la plus simple serait d’attribuer à chaque connexion un seul octet avec un décalage unique dans ce tampon. Ensuite, vous pouvez lancer un appel BeginReceive
pour qu'un seul octet soit lu et effectuer le reste de la lecture à la suite du rappel obtenu.
Traitement
Lorsque vous recevez le rappel de l'appel Begin
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 d'éviter de longues opérations dans ce rappel. L'utilisation de ces threads pour un traitement complexe annulera votre évolutivité 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 afin de traiter les données entrantes, qui seront exécutées sur un autre thread. Évitez toute opération potentiellement bloquante dans le rappel afin que le thread IOCP puisse revenir à son pool le plus rapidement possible. Dans .NET 4.0, la solution la plus simple consiste à créer un 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 ensuite chargée de lire toutes les données du socket qui représentent la demande que vous traitez, de les exécuter, puis de passer un nouvel appel BeginReceive
pour mettre le socket en file d'attente pour IOCP une fois de plus. Avant .NET 4.0, vous pouvez utiliser le ThreadPool ou créer votre propre implémentation de file d’attente de travail threadée.
Fondamentalement, je suggérerais d'utiliser le code exemple de Kevin pour cette solution, avec les avertissements suivants:
BeginReceive
est déjà 'épinglé'BeginReceive
ne 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 le ferez, je ne doute pas que vous pourriez reproduire les résultats de Chris en les adaptant potentiellement à des centaines de milliers de clients simultanés (avec le bon matériel et une mise en œuvre efficace de votre propre code de traitement bien sûr;)
Vous avez déjà obtenu l'essentiel de la réponse via les exemples de code ci-dessus. L’utilisation asynchrone IO est absolument la solution. Async IO est la façon dont Win32 est conçu en interne pour être à l’échelle. Les meilleures performances possibles que vous pouvez obtenir est réalisé à l'aide de Completion Ports, en liant vos sockets à des ports d'achèvement et en disposant d'un pool de threads en attente d'achèvement. Il est généralement conseillé de disposer de 2 à 4 threads par processeur (cœur) en attente d'achèvement. Je recommande vivement de passer par ces trois articles de Rick Vicik de l'équipe Windows Performance:
Ces articles couvrent principalement l'API Windows native, mais ils sont indispensables à quiconque tente de maîtriser l'évolutivité et les performances. Ils ont également des mémoires sur le côté géré.
La deuxième chose à faire est de vérifier 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 de threads, d'appels asynchrones et de verrous au chapitre 5. Toutefois, les véritables atouts se trouvent au chapitre 17, où vous trouverez des informations utiles comme des conseils pratiques sur le réglage de votre pool de threads. Mes applications rencontraient de graves problèmes jusqu'à ce que je modifie maxIothreads/maxWorkerThreads conformément aux recommandations de ce chapitre.
Vous dites que vous voulez créer un serveur pur TCP, mon point suivant est donc parasite. Cependant, si vous vous trouvez acculé et utilisez la classe WebRequest et ses dérivés, soyez averti qu’il ya un dragon qui garde cette porte: le ServicePointManager . C’est une classe de configuration qui a un but dans la vie: ruiner vos performances. Assurez-vous de libérer votre serveur de ServicePoint.ConnectionLimit artificiel, sinon votre application ne sera jamais mise à l’échelle (je vous laisse découvrir quelle est la valeur par défaut ...). Vous pouvez également reconsidérer la politique par défaut consistant à envoyer un en-tête Expect100Continue dans les demandes http.
Maintenant, en ce qui concerne l'API gérée par socket de base, les choses sont assez simples du côté de l'envoi, mais elles sont beaucoup plus complexes du côté de la réception. Pour atteindre un débit et une évolutivité élevés, vous devez vous assurer que le socket n'est pas contrôlé en termes de flux, car vous n'avez pas de tampon enregistré pour la réception. Idéalement, pour des performances optimales, vous devez poster 3-4 tampons et poster de nouveaux tampons dès que vous en récupérez un ( avant vous traitez celui qui a été récupéré). vous assurez ainsi que le socket a toujours un emplacement pour déposer les données provenant du réseau. Vous verrez pourquoi vous ne pourrez probablement pas atteindre cet objectif 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 System.Net.Security.NegotiateStream intégré (ou SslStream si vous devez parcourir plusieurs domaines distincts). Cela signifie qu'au lieu de compter sur des opérations asynchrones à socket direct, vous ferez confiance aux opérations asynchrones AuthenticatedStream. Dès que vous obtenez un socket (soit depuis connect on client, soit depuis accept sur serveur), vous créez un flux sur le socket et soumettez-le 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 effectuerez 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. Ensuite, vous enverrez des messages avec BeginWrite et vous les recevrez avec BeginRead. C'est le problème dont je parlais auparavant que 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 IO jusqu’à ce que vous receviez une trame complète, sinon elle ne pourrait pas gérer l’authentification du message (décrypter la trame et valider la signature sur la trame). Les classes AuthenticatedStream sont assez bonnes et ne devraient poser aucun problème. Vous devriez pouvoir saturer un 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 poster de code ici, il y a un parfaitement bon exemple sur MSDN . J'ai réalisé de nombreux projets comme celui-ci et j'ai pu passer à environ 1 000 utilisateurs connectés sans problèmes. En outre, vous devrez modifier les clés de registre pour permettre au noyau de disposer de davantage de descripteurs de socket. et assurez-vous de déployer sur un serveur , c'est-à-dire W2K3 pas XP ou Vista (c.-à-d. système d'exploitation client)) , Ça 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 variante async. Pour elles, vous viderez le pool de threads en un rien de temps. Pour les connexions SQL Server, assurez-vous vous ajoutez le 'Traitement asynchrone = true' à la chaîne de connexion.
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 manières de le faire dans .net: Approchez-vous du fil avec des sockets hautes performances dans .NET
Dernièrement, j’ai cherché des moyens d’améliorer notre code et j’examinerai ceci: " Améliorations des performances de socket dans la version 3.5 " qui a été spécifiquement inclus "pour une utilisation par les applications qui utilisent des E/S réseau asynchrones pour atteindre la plus haute performance ".
"La principale caractéristique de ces améliorations est d'éviter l'allocation et la synchronisation répétées d'objets lors d'entrées/sorties de sockets asynchrones à haut volume. Le modèle de conception Début/Fin actuellement implémenté par la classe Socket pour les E/S de sockets asynchrones nécessite un système. L'objet IAsyncResult soit alloué pour chaque opération de socket asynchrone. "
Vous pouvez continuer à lire si vous suivez le lien. Personnellement, je testerai leur échantillon de code demain pour le comparer à ce que j'ai.
Edit: Ici vous pouvez trouver le code de travail pour le client et le serveur à l'aide du nouveau 3.5 SocketAsyncEventArgs afin que vous puissiez le tester dans un quelques minutes et allez à travers le code. C'est une approche simple, mais c'est la base pour démarrer une mise en œuvre beaucoup plus grande. Aussi this l'article de MSDN Magazine d'il y a près de deux ans était une lecture intéressante.
Avez-vous envisagé de n'utiliser qu'un lien WCF TCP contraignant 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 cadre de publication/abonnement disponible dans la section de téléchargement d’IDesign qui peut être utile: http://www.idesign.net
Je m'interroge sur une chose:
Je ne veux certainement pas commencer un fil pour chaque connexion.
Pourquoi donc? Windows pouvait gérer des centaines de threads dans une application depuis au moins Windows 2000. Je l'ai fait. Il est très facile de travailler avec les threads si ceux-ci n'ont pas besoin d'être synchronisés. Particulièrement é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 la communication disque ou réseau), je ne comprends pas cette restriction.
Avez-vous testé la méthode multi-thread et avez-vous trouvé qu'il manquait quelque chose? Avez-vous l'intention d'avoir également une connexion de base de données pour chaque thread (cela tuerait le serveur de base de données, donc c'est une mauvaise idée, mais il est facile à résoudre avec une conception à 3 niveaux). Craignez-vous d'avoir des milliers de clients au lieu de centaines, et d'avoir ensuite des problèmes? (Bien que j'essaierais mille ou même dix mille threads si j'avais plus de 32 Go RAM - encore une fois, étant donné que vous n'êtes pas lié au processeur, le temps de commutation des threads devrait être absolument sans importance. )
Voici le code - pour voir à quoi ça ressemble, allez à 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 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();
}
}
}
Utiliser Async intégré .NET IO (BeginRead
, etc.) est une bonne idée si vous pouvez obtenir tous les détails correctement. Lorsque vous aurez correctement configuré votre socket/fichier, il sera traité correctement. utilisez l'implémentation IOCP sous-jacente du système d'exploitation, permettant à vos opérations d'être terminées sans utiliser de threads (ou, dans le pire des cas, en utilisant un thread qui, je crois, provient du pool de threads du noyau IO au lieu de .NET pool de threads, ce qui permet de réduire la congestion de pool de threads.)
L’objectif principal est de s’assurer que vous ouvrez vos sockets/fichiers en mode non bloquant. La plupart des fonctions pratiques par défaut (comme File.OpenRead
) ne faites pas cela, vous devrez donc écrire le vôtre.
L'une des autres préoccupations principales est la gestion des erreurs - la gestion correcte des erreurs lors de l'écriture de code d'E/S asynchrone est beaucoup, beaucoup plus difficile que de le faire dans un code synchrone. Il est également très facile de se retrouver avec des conditions de concurrence et des blocages bien que vous n'utilisiez peut-être pas directement de 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 asynchrone évolutive.
Le Concurrency Coordination Runtime de Microsoft est un exemple de bibliothèque .NET conçue pour faciliter la tâche de ce type de programmation. Ça 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 .NET de concurrence/IO que j'ai construits au cours de la dernière année, appelé Squared.Task . Il s’inspire de bibliothèques telles que imvu.task et twisted , et j’en ai inclus exemples pratiques dans le référentiel qui effectue les E/S réseau. Je l'ai également utilisé dans quelques applications que j'ai écrites - la plus grande version publiée é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 avez des problèmes, je me ferai un plaisir de vous aider.
À mon avis, mon expérience sur la plate-forme .NET, basée sur mon expérience asynchrone/threadless IO), est intéressante, tant que vous êtes prêt à gérer la courbe d'apprentissage. vous permet d'éviter les problèmes 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 précaution les primitives de concurrence telles que Futures/Promises.
J'ai utilisé la solution de Kevin, mais il dit que cette solution manque de code pour le réassemblage des messages. Les développeurs peuvent utiliser ce code pour réassembler 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;
}
}
Vous pouvez trouver un bon aperçu des techniques sur le page du problème C10k .
J'utiliserais les méthodes AcceptAsync/ConnectAsync/ReceiveAsync/SendAsync ajoutées à .Net 3.5. J'ai réalisé un test d'évaluation et ils sont environ 35% plus rapides (temps de réponse et débit) avec 100 utilisateurs qui envoient et reçoivent en permanence des données.
Vous pouvez essayer d'utiliser un framework appelé ACE (Adaptive Communications Environment), qui est un framework C++ générique pour les serveurs de réseau. C'est un produit très solide et mature, conçu pour prendre en charge des applications à haute fiabilité et à fort volume, allant jusqu'au niveau télécom.
Le cadre traite un assez grand nombre de modèles de concurrence et en a probablement un qui convient parfaitement à votre application. Cela devrait rendre le système plus facile à déboguer car la plupart des problèmes de concurrence accablants 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 disposez d'une infrastructure réseau de niveau industriel testée et testée, ainsi que d'une architecture hautement évolutive et prête à l'emploi.
Je voudrais utiliser SEDA ou une bibliothèque de threading légère (erlang ou linux plus récent voir évolutivité NTPL côté serveur ). Le codage asynchrone est très fastidieux si votre communication n’est pas :)
Eh bien, les sockets .NET semblent fournir select () - c’est mieux pour gérer les entrées. Pour la sortie, un groupe de threads de socket-writer écoute sur une file d'attente de travail, acceptant le descripteur/objet de socket comme faisant partie de l'élément de travail, de sorte que vous n'avez pas besoin de thread par socket.
aux personnes qui copient en collant 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 le mettre 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 puisque son contenu est identique, mais qu'il s'agit d'une méthode de modèle et que vous devez utiliser une exception typée pour mieux gérer les exceptions et comprendre la cause de l'erreur. Il vous suffit donc de mettre en œuvre ces captures avec du code utile.
Je recommanderais de lire ces livres sur ACE
pour obtenir des idées sur les modèles vous permettant de créer un serveur efficace.
Bien que ACE soit implémenté en C++, les livres couvrent de nombreux modèles utiles pouvant être utilisés dans n'importe quel langage de programmation.