Comment écrire un serveur TCP/Ip évolutif
Je suis dans la phase de conception d'écrire une nouvelle application de service Windows qui accepte les connexions TCP/IP pour les connexions longues (c'est-à-dire que ce N'est pas comme HTTP où il y a beaucoup de connexions courtes, mais plutôt un client se connecte et reste connecté pendant des heures ou des jours ou même des semaines).
Je cherche des idées pour la meilleure façon de concevoir l'architecture 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..) puisque je ne sais pas combien de clients j'aurai connecté à un moment donné (peut-être des centaines). Je ne veux certainement pas démarrer un thread pour chaque connexion.
Les données seront principalement envoyées aux clients à partir de mon serveur, mais il y aura des commandes envoyées par les clients à l'occasion. Il s'agit principalement d'une application de surveillance dans laquelle mon serveur envoie périodiquement des données d'État aux clients.
Des suggestions sur la meilleure façon de rendre cela aussi évolutif que possible? Flux de production de base? Grâce.
EDIT: pour être clair, je cherche des solutions basées sur. Net (C# si possible, mais n'importe quel langage. net fonctionnera)
Bounty NOTE: pour recevoir la prime, je m'attends à plus qu'une simple réponse. J'aurais besoin d'un exemple fonctionnel d'une solution, soit comme un pointeur vers quelque chose que je pourrais télécharger ou un court exemple en ligne. Et il doit être basé sur. net et Windows (n'importe quel langage. net est acceptable)
EDIT: je tiens à 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 elle est encore assez nouvelle pour que je ne sache pas avec certitude comment cela va fonctionner.
J'ai upvoted toutes les réponses que je pensais être bonnes, j'aimerais pouvoir faire plus pour vous les gars. Encore une fois, merci.
18 réponses
J'ai écrit quelque chose de similaire dans le passé. De mes recherches il y a des années ont montré que l'écriture de votre propre implémentation de socket était le meilleur pari, en utilisant les sockets asynchrones. Cela signifiait que les clients qui ne faisaient rien nécessitaient relativement peu de ressources. Tout ce qui se produit est géré par le pool de threads.net.
Je l'ai écrit comme une classe qui gère toutes les connexions pour les serveurs.
J'ai simplement utilisé une liste pour contenir toutes les connexions client, mais si vous avez besoin de recherches plus rapides pour des listes plus grandes, Vous pouvez l'écrire comme vous le souhaitez.
private List<xConnection> _sockets;
Vous avez également besoin du socket qui écoute réellement les connexions entrantes.
private System.Net.Sockets.Socket _serverSocket;
La méthode start démarre réellement le socket du serveur et commence à écouter 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 des exceptions pour que toutes les exceptions soient supprimées et return false
si une option de configuration était définie, mais je voulais la supprimer par souci de concision.
Le _serverSocket.BeginAccept(new 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 de nombreuses opérations de blocage. Cela devrait gérer de manière optimale toute charge sur le serveur.
private void acceptCallback(IAsyncResult result)
{
xConnection conn = new xConnection();
try
{
//Finish accepting the connection
System.Net.Sockets.Socket s = (System.Net.Sockets.Socket)result.AsyncState;
conn = new xConnection();
conn.socket = s.EndAccept(result);
conn.buffer = new byte[_bufferSize];
lock (_sockets)
{
_sockets.Add(conn);
}
//Queue recieving of data from the connection
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
//Queue the accept of the next incomming connection
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (SocketException e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
//Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (Exception e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
//Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
}
Le code ci-dessus vient essentiellement de terminer d'accepter la connexion qui entre, les files d'attente BeginReceive
qui est un rappel qui s'exécutera lorsque le client enverra des données, puis les files d'attente acceptCallback
qui accepteront la prochaine connexion client qui entre.
L'appel de 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, ce qui est la façon dont nous gérons la réception des données.
private void ReceiveCallback(IAsyncResult result)
{
//get our connection from the callback
xConnection conn = (xConnection)result.AsyncState;
//catch any errors, we'd better not have any
try
{
//Grab our buffer and count the number of bytes receives
int bytesRead = conn.socket.EndReceive(result);
//make sure we've read something, if we haven't it supposadly means that the client disconnected
if (bytesRead > 0)
{
//put whatever you want to do when you receive data here
//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
}
else
{
//Callback run but no data, close the connection
//supposadly means a disconnect
//and we still have to close the socket, even though we throw the event later
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
catch (SocketException e)
{
//Something went terribly wrong
//which shouldn't have happened
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
}
EDIT: Dans ce modèle, j'ai oublié de mentionner que dans cette zone de code:
//put whatever you want to do when you receive data here
//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
Ce que je ferais généralement, c'est dans le code ce que vous voulez, c'est réassembler les paquets en messages, puis 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 accept termine la lecture du socket de données en appelant end receive. Cela remplit le tampon fourni dans la fonction de réception. 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 ne peut être appelé qu'avec une partie du message. Le remontage peut devenir très très compliqué. J'ai utilisé ma propre méthode et créé une sorte de protocole propriétaire pour ce faire. Je l'ai laissée, mais si vous le demandez, je peux l'ajouter dans. 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 fait un appel synchrone Send
, pour moi, c'était bien en raison de la taille des messages et de la nature multithread de mon application. Si vous souhaitez envoyer à chaque client, il vous suffit de parcourir la liste _sockets.
La classe xConnection que vous voyez référencée ci-dessus est fondamentalement, un wrapper simple pour un socket pour inclure le tampon d'octets, et dans mon implémentation quelques extras.
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
Aussi pour référence, voici les using
que j'inclus car 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 au code que vous devriez être fatigué de changer. Pour un, n'avoir qu'un seul BeginAccept
appelé à un moment donné. Il y avait un très ennuyeux . Net bug autour de cela, qui était 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 une seule socket, nous ne sommes réellement jamais dans ReceiveCallback
qu'une fois à tout moment, et nous n'avons pas besoin d'utiliser la synchronisation des threads. Cependant, si vous réorganisez ceci pour appeler la prochaine réception immédiatement après avoir extrait les données, ce qui pourrait être un peu plus rapide, vous devrez vous assurer que vous synchroniser correctement les fils.
Aussi, 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 conception. Laissez un commentaire si vous avez d'autres questions à ce sujet.
Il y a plusieurs façons de faire des opérations réseau en C#. Tous utilisent différents mécanismes sous le capot, et souffrent ainsi de problèmes de performance majeurs avec une concurrence élevée. Les opérations Begin* sont l'une de ces opérations que beaucoup de gens confondent souvent pour être le moyen le plus rapide/le plus rapide de faire du réseautage.
Pour résoudre ces problèmes, ils ont introduit l'ensemble de méthodes * Async: From 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 fournissent un modèle asynchrone alternatif qui peut être utilisé par des applications socket haute performance spécialisées. Cette classe a été spécialement conçue pour les applications de serveur réseau nécessitant des performances élevées. Une application peut utiliser le modèle asynchrone amélioré exclusivement ou seulement dans des zones chaudes 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 répétée et la synchronisation des objets pendant les e / s de socket asynchrones à volume élevé. le modèle de conception Begin/End actuellement implémenté par la classe System.Net.Sockets..::.Socket nécessite un système..::.Objet IAsyncResult être alloué pour chaque opération de socket asynchrone.
Sous les couvertures, L'API * Async utilise des ports d'achèvement D'E / S qui sont le moyen le plus rapide d'effectuer des opérations 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 ne suis qu', y compris les parties pertinentes. A noter également, au lieu de traiter les données en ligne, j'opte plutôt pour les pousser sur une file d'attente Sans verrou (wait free) qui est traitée sur un thread séparé. Notez que j' Je n'inclut pas la classe Pool correspondante qui est juste un pool simple qui va créer un nouvel objet 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 une quantité indéterminée de données. Si vous souhaitez plus d'informations, n'hésitez pas à m'envoyer un PM.
public class Telnet
{
private readonly Pool<SocketAsyncEventArgs> m_EventArgsPool;
private Socket m_ListenSocket;
/// <summary>
/// This event fires when a connection has been established.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> Connected;
/// <summary>
/// This event fires when a connection has been shutdown.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> Disconnected;
/// <summary>
/// This event fires when data is received on the socket.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> DataReceived;
/// <summary>
/// This event fires when data is finished sending on the socket.
/// </summary>
public event EventHandler<SocketAsyncEventArgs> DataSent;
/// <summary>
/// This event fires when a line has been received.
/// </summary>
public event EventHandler<LineReceivedEventArgs> LineReceived;
/// <summary>
/// Specifies the port to listen on.
/// </summary>
[DefaultValue(23)]
public int ListenPort { get; set; }
/// <summary>
/// Constructor for Telnet class.
/// </summary>
public Telnet()
{
m_EventArgsPool = new Pool<SocketAsyncEventArgs>();
ListenPort = 23;
}
/// <summary>
/// Starts the telnet server listening and accepting data.
/// </summary>
public void Start()
{
IPEndPoint endpoint = new IPEndPoint(0, ListenPort);
m_ListenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
m_ListenSocket.Bind(endpoint);
m_ListenSocket.Listen(100);
//
// Post Accept
//
StartAccept(null);
}
/// <summary>
/// Not Yet Implemented. Should shutdown all connections gracefully.
/// </summary>
public void Stop()
{
//throw (new NotImplementedException());
}
//
// ACCEPT
//
/// <summary>
/// Posts a requests for Accepting a connection. If it is being called from the completion of
/// an AcceptAsync call, then the AcceptSocket is cleared since it will create a new one for
/// the new user.
/// </summary>
/// <param name="e">null if posted from startup, otherwise a <b>SocketAsyncEventArgs</b> for reuse.</param>
private void StartAccept(SocketAsyncEventArgs e)
{
if (e == null)
{
e = m_EventArgsPool.Pop();
e.Completed += Accept_Completed;
}
else
{
e.AcceptSocket = null;
}
if (m_ListenSocket.AcceptAsync(e) == false)
{
Accept_Completed(this, e);
}
}
/// <summary>
/// Completion callback routine for the AcceptAsync post. This will verify that the Accept occured
/// and then setup a Receive chain to begin receiving data.
/// </summary>
/// <param name="sender">object which posted the AcceptAsync</param>
/// <param name="e">Information about the Accept call.</param>
private void Accept_Completed(object sender, SocketAsyncEventArgs e)
{
//
// Socket Options
//
e.AcceptSocket.NoDelay = true;
//
// Create and setup a new connection object for this user
//
Connection connection = new Connection(this, e.AcceptSocket);
//
// Tell the client that we will be echo'ing data sent
//
DisableEcho(connection);
//
// Post the first receive
//
SocketAsyncEventArgs args = m_EventArgsPool.Pop();
args.UserToken = connection;
//
// Connect Event
//
if (Connected != null)
{
Connected(this, args);
}
args.Completed += Receive_Completed;
PostReceive(args);
//
// Post another accept
//
StartAccept(e);
}
//
// RECEIVE
//
/// <summary>
/// Post an asynchronous receive on the socket.
/// </summary>
/// <param name="e">Used to store information about the Receive call.</param>
private void PostReceive(SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;
if (connection != null)
{
connection.ReceiveBuffer.EnsureCapacity(64);
e.SetBuffer(connection.ReceiveBuffer.DataBuffer, connection.ReceiveBuffer.Count, connection.ReceiveBuffer.Remaining);
if (connection.Socket.ReceiveAsync(e) == false)
{
Receive_Completed(this, e);
}
}
}
/// <summary>
/// Receive completion callback. Should verify the connection, and then notify any event listeners
/// that data has been received. For now it is always expected that the data will be handled by the
/// listeners and thus the buffer is cleared after every call.
/// </summary>
/// <param name="sender">object which posted the ReceiveAsync</param>
/// <param name="e">Information about the Receive call.</param>
private void Receive_Completed(object sender, SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;
if (e.BytesTransferred == 0 || e.SocketError != SocketError.Success || connection == null)
{
Disconnect(e);
return;
}
connection.ReceiveBuffer.UpdateCount(e.BytesTransferred);
OnDataReceived(e);
HandleCommand(e);
Echo(e);
OnLineReceived(connection);
PostReceive(e);
}
/// <summary>
/// Handles Event of Data being Received.
/// </summary>
/// <param name="e">Information about the received data.</param>
protected void OnDataReceived(SocketAsyncEventArgs e)
{
if (DataReceived != null)
{
DataReceived(this, e);
}
}
/// <summary>
/// Handles Event of a Line being Received.
/// </summary>
/// <param name="connection">User connection.</param>
protected void OnLineReceived(Connection connection)
{
if (LineReceived != null)
{
int index = 0;
int start = 0;
while ((index = connection.ReceiveBuffer.IndexOf('\n', index)) != -1)
{
string s = connection.ReceiveBuffer.GetString(start, index - start - 1);
s = s.Backspace();
LineReceivedEventArgs args = new LineReceivedEventArgs(connection, s);
Delegate[] delegates = LineReceived.GetInvocationList();
foreach (Delegate d in delegates)
{
d.DynamicInvoke(new object[] { this, args });
if (args.Handled == true)
{
break;
}
}
if (args.Handled == false)
{
connection.CommandBuffer.Enqueue(s);
}
start = index;
index++;
}
if (start > 0)
{
connection.ReceiveBuffer.Reset(0, start + 1);
}
}
}
//
// SEND
//
/// <summary>
/// Overloaded. Sends a string over the telnet socket.
/// </summary>
/// <param name="connection">Connection to send data on.</param>
/// <param name="s">Data to send.</param>
/// <returns>true if the data was sent successfully.</returns>
public bool Send(Connection connection, string s)
{
if (String.IsNullOrEmpty(s) == false)
{
return Send(connection, Encoding.Default.GetBytes(s));
}
return false;
}
/// <summary>
/// Overloaded. Sends an array of data to the client.
/// </summary>
/// <param name="connection">Connection to send data on.</param>
/// <param name="data">Data to send.</param>
/// <returns>true if the data was sent successfully.</returns>
public bool Send(Connection connection, byte[] data)
{
return Send(connection, data, 0, data.Length);
}
public bool Send(Connection connection, char c)
{
return Send(connection, new byte[] { (byte)c }, 0, 1);
}
/// <summary>
/// Sends an array of data to the client.
/// </summary>
/// <param name="connection">Connection to send data on.</param>
/// <param name="data">Data to send.</param>
/// <param name="offset">Starting offset of date in the buffer.</param>
/// <param name="length">Amount of data in bytes to send.</param>
/// <returns></returns>
public bool Send(Connection connection, byte[] data, int offset, int length)
{
bool status = true;
if (connection.Socket == null || connection.Socket.Connected == false)
{
return false;
}
SocketAsyncEventArgs args = m_EventArgsPool.Pop();
args.UserToken = connection;
args.Completed += Send_Completed;
args.SetBuffer(data, offset, length);
try
{
if (connection.Socket.SendAsync(args) == false)
{
Send_Completed(this, args);
}
}
catch (ObjectDisposedException)
{
//
// return the SocketAsyncEventArgs back to the pool and return as the
// socket has been shutdown and disposed of
//
m_EventArgsPool.Push(args);
status = false;
}
return status;
}
/// <summary>
/// Sends a command telling the client that the server WILL echo data.
/// </summary>
/// <param name="connection">Connection to disable echo on.</param>
public void DisableEcho(Connection connection)
{
byte[] b = new byte[] { 255, 251, 1 };
Send(connection, b);
}
/// <summary>
/// Completion callback for SendAsync.
/// </summary>
/// <param name="sender">object which initiated the SendAsync</param>
/// <param name="e">Information about the SendAsync call.</param>
private void Send_Completed(object sender, SocketAsyncEventArgs e)
{
e.Completed -= Send_Completed;
m_EventArgsPool.Push(e);
}
/// <summary>
/// Handles a Telnet command.
/// </summary>
/// <param name="e">Information about the data received.</param>
private void HandleCommand(SocketAsyncEventArgs e)
{
Connection c = e.UserToken as Connection;
if (c == null || e.BytesTransferred < 3)
{
return;
}
for (int i = 0; i < e.BytesTransferred; i += 3)
{
if (e.BytesTransferred - i < 3)
{
break;
}
if (e.Buffer[i] == (int)TelnetCommand.IAC)
{
TelnetCommand command = (TelnetCommand)e.Buffer[i + 1];
TelnetOption option = (TelnetOption)e.Buffer[i + 2];
switch (command)
{
case TelnetCommand.DO:
if (option == TelnetOption.Echo)
{
// ECHO
}
break;
case TelnetCommand.WILL:
if (option == TelnetOption.Echo)
{
// ECHO
}
break;
}
c.ReceiveBuffer.Remove(i, 3);
}
}
}
/// <summary>
/// Echoes data back to the client.
/// </summary>
/// <param name="e">Information about the received data to be echoed.</param>
private void Echo(SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;
if (connection == null)
{
return;
}
//
// backspacing would cause the cursor to proceed beyond the beginning of the input line
// so prevent this
//
string bs = connection.ReceiveBuffer.ToString();
if (bs.CountAfterBackspace() < 0)
{
return;
}
//
// find the starting offset (first non-backspace character)
//
int i = 0;
for (i = 0; i < connection.ReceiveBuffer.Count; i++)
{
if (connection.ReceiveBuffer[i] != '\b')
{
break;
}
}
string s = Encoding.Default.GetString(e.Buffer, Math.Max(e.Offset, i), e.BytesTransferred);
if (connection.Secure)
{
s = s.ReplaceNot("\r\n\b".ToCharArray(), '*');
}
s = s.Replace("\b", "\b \b");
Send(connection, s);
}
//
// DISCONNECT
//
/// <summary>
/// Disconnects a socket.
/// </summary>
/// <remarks>
/// It is expected that this disconnect is always posted by a failed receive call. Calling the public
/// version of this method will cause the next posted receive to fail and this will cleanup properly.
/// It is not advised to call this method directly.
/// </remarks>
/// <param name="e">Information about the socket to be disconnected.</param>
private void Disconnect(SocketAsyncEventArgs e)
{
Connection connection = e.UserToken as Connection;
if (connection == null)
{
throw (new ArgumentNullException("e.UserToken"));
}
try
{
connection.Socket.Shutdown(SocketShutdown.Both);
}
catch
{
}
connection.Socket.Close();
if (Disconnected != null)
{
Disconnected(this, e);
}
e.Completed -= Receive_Completed;
m_EventArgsPool.Push(e);
}
/// <summary>
/// Marks a specific connection for graceful shutdown. The next receive or send to be posted
/// will fail and close the connection.
/// </summary>
/// <param name="connection"></param>
public void Disconnect(Connection connection)
{
try
{
connection.Socket.Shutdown(SocketShutdown.Both);
}
catch (Exception)
{
}
}
/// <summary>
/// Telnet command codes.
/// </summary>
internal enum TelnetCommand
{
SE = 240,
NOP = 241,
DM = 242,
BRK = 243,
IP = 244,
AO = 245,
AYT = 246,
EC = 247,
EL = 248,
GA = 249,
SB = 250,
WILL = 251,
WONT = 252,
DO = 253,
DONT = 254,
IAC = 255
}
/// <summary>
/// Telnet command options.
/// </summary>
internal enum TelnetOption
{
Echo = 1,
SuppressGoAhead = 3,
Status = 5,
TimingMark = 6,
TerminalType = 24,
WindowSize = 31,
TerminalSpeed = 32,
RemoteFlowControl = 33,
LineMode = 34,
EnvironmentVariables = 36
}
}
Il y avait une très bonne discussion sur TCP/IP évolutif en utilisant. net écrit par Chris Mullins de Coversant, malheureusement, il semble que son blog a disparu de son emplacement précédent, donc je vais essayer de rassembler ses conseils de la mémoire (quelques commentaires utiles de son apparaissent dans ce fil: C++ vs C#: développer un serveur IOCP hautement évolutif )
Tout d'abord, notez que les méthodes Begin/End
et Async
de la classe Socket
utilisent toutes deux L'achèvement des E / S Ports (IOCP) pour fournir l'évolutivité. Cela fait une différence beaucoup plus grande (lorsqu'il est utilisé correctement; voir ci-dessous) pour l'évolutivité que celle des deux méthodes que vous choisissez réellement pour implémenter votre solution.
Les messages de Chris Mullins étaient basés sur l'utilisation de Begin/End
, qui est celui avec lequel j'ai personnellement de l'expérience. Notez que Chris a mis en place une solution basée sur ceci qui a mis à l'échelle 10 000 s de connexions client simultanées sur une machine 32 bits avec 2 Go de mémoire, et bien dans 100 000 s sur un 64 bits plate-forme avec une mémoire suffisante. De ma propre expérience avec cette technique (bien que loin de ce type de charge), je n'ai aucune raison de douter de ces chiffres indicatifs.
IOCP contre thread par connexion ou "sélectionner" primitives
La raison pour laquelle vous voulez utiliser un mécanisme qui utilise IOCP sous le capot est qu'il utilise un pool de threads Windows de très bas niveau qui ne réveille aucun thread tant qu'il n'y a pas de données réelles sur le canal D'E / S que vous essayez de lire (notez que IOCP peut être utilisé pour le fichier IO ainsi). L'avantage de ceci est que Windows n'a pas besoin de passer à un thread seulement pour constater qu'il n'y a pas encore de données, donc cela réduit le nombre de commutateurs de contexte que votre serveur devra faire au strict minimum requis.
Les commutateurs de contexte sont ce qui va certainement tuer le mécanisme' thread-per-connection', bien que ce soit une solution viable si vous n'avez affaire qu'à quelques dizaines de connexions. Ce mécanisme n'est cependant pas extensible l'imagination évolutive.
Considérations importantes lors de L'utilisation D'IOCP
Mémoire
Tout d'abord, 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
entraînera un "épinglage" du tampon dans lequel vous lisez. Pour une bonne explication de pourquoi c'est un problème, voir: Weblog de Yun Jin: OutOfMemoryException et épingler.
Heureusement ce problème peut être évité, mais il nécessite un peu de compromis. La solution suggérée consiste à allouer un grand tampon byte[]
au démarrage de l'application (ou à proximité), d'au moins 90 Ko ou-so (à partir de.Net 2, la taille requise peut être plus grande dans les versions ultérieures). La raison pour cela est que les allocations de mémoire volumineuses se retrouvent automatiquement dans un segment de mémoire non compactant (le tas D'objets volumineux)qui est automatiquement épinglé. En allouant un grand tampon au démarrage, vous vous assurez que ce bloc de la mémoire non amovible est à une adresse relativement "basse" où elle 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 qui a besoin de lire des données. C'est là qu'un compromis entre en jeu; puisque ce tampon doit être pré-alloué, vous devrez décider combien d'espace tampon vous avez besoin par connexion, et quelle limite supérieure vous voulez définir sur le nombre de connexions que vous voulez 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 serait d'attribuer à chaque connexion un seul octet à un décalage unique dans ce tampon. Ensuite, vous pouvez faire un appel BeginReceive
pour un seul octet à lire, et effectuer le reste de la lecture à la suite du rappel que vous obtenez.
Traitement
Lorsque vous obtenez le rappel de l'appel Begin
que vous avez fait, il est très important de réaliser 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 va tuer votre évolutivité tout aussi efficacement que l'utilisation de 'thread-per-connection'.
La solution suggérée consiste à utiliser le rappel uniquement pour mettre en file d'attente un élément de travail pour traiter les données entrantes, qui seront exécutées sur un autre thread. Évitez les opérations potentiellement bloquantes à l'intérieur du rappel afin que le thread IOCP puisse revenir à son pool le plus rapidement possible. Dans. NET 4.0, je suggère que la solution la plus simple consiste à générer un Task
, en lui donnant une référence au socket client et une copie du premier octet qui a déjà été lu par l'appel BeginReceive
. Cette tâche est alors chargée de lire toutes les données du socket qui représentent la requête que vous traitez, de l'Exécuter, puis de faire un nouvel appel BeginReceive
pour mettre en file d'attente le socket pour IOCP une fois de plus. Pré. net 4.0, vous pouvez utiliser le ThreadPool, ou créez votre propre implémentation de file d'attente de travail threadée.
Résumé
Fondamentalement, je suggère d'utiliser L'exemple de code de Kevin pour cette solution, avec les avertissements supplémentaires suivants:
- assurez-vous que le tampon de passer
BeginReceive
est déjà "épinglé' - assurez-vous que le rappel que vous passez à
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
Quand vous faites cela, je n'ai aucun doute que vous pourriez répliquer 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 bien sûr;)
Vous avez déjà obtenu la plus grande partie de la réponse via les exemples de code ci-dessus. L'utilisation d'une opération d'E / S asynchrone est absolument la voie à suivre ici. Async IO est la façon dont Win32 est conçu en interne pour évoluer. La meilleure performance possible que vous pouvez obtenir est obtenue en utilisant les Ports D'achèvement, en liant vos sockets aux ports d'achèvement et en disposant d'un pool de threads en attente de l'achèvement du port d'achèvement. La sagesse commune est d'avoir 2-4 threads par CPU (core) en attente d'achèvement. Je recommande fortement d'aller plus ces trois articles de Rick Vicik de L'équipe de Performance de Windows:
- Conception D'Applications pour la Performance-Partie 1
- Conception D'Applications pour la Performance-Partie 2
- la Conception d'Applications pour des Performances - Partie 3
Ces articles couvrent principalement L'API Windows native, mais ils sont une lecture incontournable pour tous ceux qui essaient d'avoir une compréhension de l'évolutivité et des performances. Ils ont quelques mémoires sur le côté géré de les choses trop.
La deuxième chose que vous devez faire est de vous assurer de parcourir le livre amélioration des performances et de l'évolutivité des applications.net, disponible en ligne. Vous trouverez des conseils pertinents et valables sur l'utilisation des threads, des appels asynchrones et des verrous au chapitre 5. Mais les véritables joyaux sont dans le chapitre 17 où vous trouverez des goodies tels que des conseils pratiques sur le réglage de votre pool de threads. Mes applications ont eu de sérieux problèmes jusqu'à ce que j'ajuste les maxIothreads / maxWorkerThreads comme selon les recommandations de ce chapitre.
Vous dites que vous voulez faire un serveur TCP pur, donc mon point suivant est faux. cependant , si vous vous trouvez coincé et utilisez la classe WebRequest et ses dérivés, soyez averti qu'il y a 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 du point de service imposé artificiel.ConnectionLimit ou 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 stratégie par défaut d'envoi D'un en-tête Expect100Continue dans les requêtes http.
Maintenant, à propos de l'API gérée par socket core, les choses sont assez faciles du côté de L'envoi, mais elles sont beaucoup plus complexes du côté de la réception. Afin d'atteindre un débit et une échelle élevés vous devez vous assurer que le socket n'est pas contrôlé par le flux car vous n'avez pas de tampon affiché pour recevoir. Idéalement, pour des performances élevées, vous devriez publier 3-4 tampons et poster de nouveaux tampons dès que vous en récupérez un ( avant {[30] } vous traitez celui qui est revenu) afin de vous assurer que le socket a toujours un endroit pour déposer les données provenant du réseau. Vous verrez pourquoi vous ne serez probablement pas en mesure d'y parvenir sous peu.
Une fois que vous avez terminé de jouer avec L'API BeginRead / BeginWrite et de commencer 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 cryptage du trafic, ou au moins Protection contre la falsification du trafic. La façon dont vous le faites est que vous utilisez le système intégré. Net. Security.NegotiateStream (ou SslStream si vous devez traverser des domaines disparates). Cela signifie qu'au lieu de compter sur des opérations asynchrones socket droites, vous vous appuierez sur les opérations asynchrones AuthenticatedStream. Dès que vous obtenez un socket (à partir de connect on client ou d'accept on server), vous créez un flux sur le socket et soumettez-le pour l'authentification, en appelant BeginAuthenticateAsClient ou BeginAuthenticateAsServer. Une fois l'authentification terminée (au moins votre coffre-fort de la folie InitiateSecurityContext/AcceptSecurityContext native...) vous ferez votre autorisation en vérifiant la propriété RemoteIdentity de votre flux authentifié et en faisant toute vérification ACL que votre produit doit prendre en charge. Après cela, vous enverrez des messages en utilisant le BeginWrite et vous les recevrez avec BeginRead. C'est le problème que je parlais avant que vous ne puissiez pas poster plusieurs tampons de réception, car les classes AuthenticateStream ne le supportent pas. L'opération BeginRead gère en interne toutes les e / s jusqu'à ce que vous ayez reçu une trame entière, sinon elle ne pourrait pas gérer l'authentification du message (décrypter le cadre et valider la signature sur le cadre). Bien que d'après mon expérience le travail effectué par les classes AuthenticatedStream soit assez bon et ne devrait pas avoir de problème avec il. IE. vous devriez être en mesure de saturer le réseau GB avec seulement 4-5% CPU. Les classes AuthenticatedStream vous imposeront également les limitations de taille d'image 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 fait beaucoup de projets comme celui-ci et j'ai pu évoluer vers environ 1000 utilisateurs connectés sans problèmes. Ci dessus vous devrez modifier le registre touches pour permettre au noyau pour plus de poignées de socket. et assurez-vous de déployer sur un système d'exploitation server, C'est-à-dire W2K3 pas XP ou Vista (ie. OS client), Cela fait une grande différence.
Assurez-vous que si vous avez des opérations de bases de données sur le serveur ou le fichier IO, vous utilisez également la saveur asynchrone pour eux, ou vous viderez le pool de threads en un rien de temps. Pour les connexions SQL Server, assurez-vous d'ajouter le 'Asyncronous Processing=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 façons de le faire dans. NET: rapprochez-vous du fil avec des Sockets haute Performance dans. NET
Dernièrement, j'ai cherché des moyens d'améliorer notre code et je vais examiner ceci: "améliorations des performances des sockets dans la Version 3.5 "qui a été inclus spécifiquement" pour une utilisation par les applications qui utilisent des E / S réseau asynchrones pour atteindre le plus haut performance".
"la principale caractéristique de ces améliorations est d'éviter l'allocation et la synchronisation répétées d'objets pendant les e/s de socket asynchrones à volume élevé.le modèle de conception Begin/End actuellement implémenté par la classe Socket pour les E/S de socket asynchrones nécessite un système.Objet IAsyncResult être alloué pour chaque opération de socket asynchrone."
Vous pouvez continuer à lire si vous suivez le lien. Personnellement, je vais tester leur exemple de code demain la comparer à l'encontre de ce que j'ai.
Modifier: ici Vous pouvez trouver du code de travail pour le client et le serveur en utilisant le nouveau SocketAsyncEventArgs 3.5 afin que vous puissiez le tester en quelques minutes et passer par le code. C'est une approche simple, mais c'est la base pour commencer une mise en œuvre beaucoup plus large. Aussi cet article d'il y a près de deux ans dans le Magazine MSDN était une lecture intéressante.
Avez-vous envisagé d'utiliser une liaison TCP net WCF et un modèle de publication/Abonnement ? WCF vous permettrait de vous concentrer [principalement] sur votre domaine au lieu de plomberie..
Il y a beaucoup D'exemples WCF et même un cadre de publication / Abonnement disponible sur 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 de discussion pour chaque connexion.
Pourquoi est-ce? Windows pourrait gérer des centaines de threads dans une application depuis au moins Windows 2000. Je l'ai fait, c'est vraiment facile de travailler avec si les threads n'ont pas besoin d'être synchronisés. Surtout étant donné que vous faites beaucoup d'E/S (donc vous n'êtes pas lié au processeur, et beaucoup de threads seraient bloqués sur le disque ou la communication réseau), Je ne le fais pas comprenez cette restriction.
Avez-vous testé la manière multi-thread et l'avez-vous trouvé manquant de quelque chose? Avez-vous l'intention d'avoir également une connexion à la base de données pour chaque thread (cela tuerait le serveur de base de données, donc c'est une mauvaise idée, mais elle est facilement résolue avec un design à 3 niveaux). Êtes-vous inquiet que vous aurez des milliers de clients au lieu de centaines, Et alors vous aurez vraiment des problèmes? (Bien que j'essayerais mille threads ou même dix mille si j'avais 32 + Go de RAM-encore une fois, étant donné que vous n'êtes pas lié au processeur, le temps de commutation de thread devrait être absolument hors de propos.)
Voici le code - pour voir comment cela semble fonctionner, allez à http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html {[16] } et cliquez sur l'image.
Classe du 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 les E / S asynchrones intégrées de. Net (BeginRead
, etc.) est une bonne idée si vous pouvez obtenir tous les détails correctement. Lorsque vous configurez correctement vos poignées de socket / file, il utilisera L'implémentation IOCP sous-jacente du système d'exploitation, permettant à vos opérations de se terminer sans utiliser de threads (ou, dans le pire des cas, en utilisant un thread qui, je crois, provient du pool de threads IO du noyau au lieu du pool de threads.net, ce qui aide)
Le principal gotcha est de vous assurer que vous ouvrez vos sockets / fichiers en mode non bloquant. La plupart des fonctions de commodité par défaut (comme File.OpenRead
) ne le font pas, vous devrez donc écrire les vôtres.
L'une des autres préoccupations principales est la gestion des erreurs - gérer correctement les erreurs lors de l'écriture de code d'E/S asynchrone est beaucoup, beaucoup plus difficile que de le faire en code synchrone. Il est également très facile de se retrouver avec des conditions de course et des blocages même si vous n'utilisez pas directement les threads, vous devez donc en être conscient.
Si possible, vous devriez essayer d'utiliser une bibliothèque pratique pour faciliter le processus d'E / S asynchrones évolutives.
Pour mes projets personnels qui doivent effectuer des E / S réseau ou disque asynchrones, j'utilise un ensemble D'outils. net concurrency/IO que j'ai créés au cours de la dernière année, appelé au carré.Tâche . Il est inspiré par des bibliothèques comme imvu.task et twisted , et j'ai inclus quelques exemples de travail dans le référentiel qui font des e/s Réseau Je l'ai également utilisé dans quelques applications que j'ai écrites - la plus grande publiée publiquement étant NDexer (qui l'utilise pour les e/s de disque sans fil). La bibliothèque a été écrite sur la base de mon expérience avec imvu.tâche et a un ensemble de tests unitaires assez complets, donc je nous vous encourageons fortement à l'essayer. Si vous avez des problèmes avec elle, je serais heureux de vous offrir de l'aide.
À mon avis, d'après mon expérience, l'utilisation d'E/S asynchrones / threadless au lieu de threads est une entreprise intéressante sur la plate-forme.net, tant que vous êtes prêt à faire face à la courbe d'apprentissage. Il vous permet d'éviter les tracas d'évolutivité imposés par le coût des objets Thread, et dans de nombreux cas, vous pouvez complètement éviter l'utilisation de verrous et de mutex en faisant un usage prudent des primitives de concurrence comme Futures / Promises.
J'ai utilisé la solution de Kevin mais il dit que la solution manque de code pour le réassemblage des messages. Les développeurs peuvent utiliser ce code pour réassembler les 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 essayer d'utiliser un framework appelé ACE (Adaptive Communications Environment) qui est un framework C++ générique pour les serveurs réseau. C'est un produit très solide et mature et est conçu pour prendre en charge des applications de grande fiabilité et de volume élevé jusqu'à la qualité telco.
Le framework traite d'un large éventail de modèles de concurrence et en a probablement un adapté à votre application. Cela devrait rendre le système plus facile à déboguer car la plupart des problèmes de concurrence désagréables ont déjà été triés. Le compromis ici est que le framework est écrit en C++ et n'est pas le plus chaud et le plus moelleux des bases de code. D'autre part, vous obtenez une infrastructure réseau de qualité industrielle testée et une architecture hautement évolutive prête à l'emploi.
J'utiliserais SEDA {[2] } ou une bibliothèque de threads légère (erlang ou linux plus récent voir l'évolutivité NTPL Côté Serveur). Le codage asynchrone est très lourd si votre communication ne l'est pas:)
Eh bien, les sockets. net semblent fournir select () - c'est mieux pour gérer l'entrée. Pour la sortie, j'aurais un pool de threads socket-writer écoutant sur une file d'attente de travail, acceptant le descripteur/objet socket dans le cadre de l'élément de travail, donc vous n'avez pas besoin d'un thread par socket.
J'utiliserais les méthodes AcceptAsync / ConnectAsync/ReceiveAsync / SendAsync qui ont été ajoutées dans.Net 3.5. J'ai fait un benchmark et ils sont environ 35% plus rapides (temps de réponse et débit binaire) avec 100 utilisateurs envoyant et recevant constamment des données.
Pour copier coller la réponse acceptée, vous pouvez réécrire la méthode acceptCallback, en supprimant tous les appels de _serverSocket.BeginAccept(nouveau AsyncCallback (acceptCallback), _serverSocket); et mettez-le dans une clause finally {}, de cette façon:
private void acceptCallback(IAsyncResult result)
{
xConnection conn = new xConnection();
try
{
//Finish accepting the connection
System.Net.Sockets.Socket s = (System.Net.Sockets.Socket)result.AsyncState;
conn = new xConnection();
conn.socket = s.EndAccept(result);
conn.buffer = new byte[_bufferSize];
lock (_sockets)
{
_sockets.Add(conn);
}
//Queue recieving of data from the connection
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
}
catch (SocketException e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
catch (Exception e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
finally
{
//Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
}
Vous pouvez même supprimer la première capture puisque son contenu est le même mais c'est une méthode de modèle et vous devriez utiliser une exception typée pour mieux gérer les exceptions et comprendre ce qui a causé l'erreur, donc implémentez simplement ces captures avec certains code utile
Je recommande de lire ces livres sur ACE
- programmation réseau c++: maîtriser la complexité en utilisant ACE et Patterns
- programmation réseau c++: réutilisation systématique avec ACE et Frameworks
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 beaucoup de modèles utiles qui peuvent être utilisés dans n'importe quel langage de programmation.
Pour être clair, je cherche des solutions basées sur. Net (C# si possible, mais n'importe quel langage. net fonctionnera)
Vous n'obtiendrez pas le plus haut niveau d'évolutivité si vous allez purement avec .NET. GC les pauses peuvent entraver la latence.
Je vais avoir besoin de démarrer au moins un thread pour le service. J'envisage d'utiliser L'API Asynch (BeginRecieve, etc..) puisque je ne sais pas combien de clients j'aurai connecté à un moment donné (peut-être des centaines). Je certainement ne veulent pas démarrer un thread pour chaque connexion.
Overlapped io est généralement considéré comme L'API la plus rapide de Windows pour la communication réseau. Je ne sais pas si c'est la même chose que votre API Asynch. N'utilisez pas select car chaque appel doit vérifier chaque socket ouvert au lieu d'avoir des rappels sur les sockets actifs.
Vous pouvez utiliser Push Framework open source framework pour le développement de serveurs hautes performances. Il est construit sur IOCP et convient aux scénarios push et à la diffusion de messages.