J'ai écrit quelque chose de similaire dans le passé. D'après mes recherches d'il y a des années, l'écriture de votre propre implémentation de socket était le meilleur choix, en utilisant les sockets asynchrones. Cela signifiait que les clients qui ne faisaient vraiment rien nécessitaient en fait relativement peu de ressources. Tout ce qui se produit est géré par le pool de threads .net.
Je l'ai écrit en tant que classe qui gère toutes les connexions pour les serveurs.
J'ai simplement utilisé une liste pour contenir toutes les connexions client, mais si vous avez besoin de recherches plus rapides pour des listes plus grandes, vous pouvez l'écrire comme vous le souhaitez.
private List<xConnection> _sockets;
Vous avez également besoin de la prise qui écoute les connexions entrantes.
private System.Net.Sockets.Socket _serverSocket;
La méthode start démarre en fait le socket serveur et commence à écouter les connexions entrantes.
public bool Start()
{
System.Net.IPHostEntry localhost = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
System.Net.IPEndPoint serverEndPoint;
try
{
serverEndPoint = new System.Net.IPEndPoint(localhost.AddressList[0], _port);
}
catch (System.ArgumentOutOfRangeException e)
{
throw new ArgumentOutOfRangeException("Port number entered would seem to be invalid, should be between 1024 and 65000", e);
}
try
{
_serverSocket = new System.Net.Sockets.Socket(serverEndPoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
}
catch (System.Net.Sockets.SocketException e)
{
throw new ApplicationException("Could not create socket, check to make sure not duplicating port", e);
}
try
{
_serverSocket.Bind(serverEndPoint);
_serverSocket.Listen(_backlog);
}
catch (Exception e)
{
throw new ApplicationException("Error occured while binding socket, check inner exception", e);
}
try
{
//warning, only call this once, this is a bug in .net 2.0 that breaks if
// you're running multiple asynch accepts, this bug may be fixed, but
// it was a major pain in the ass previously, so make sure there is only one
//BeginAccept running
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (Exception e)
{
throw new ApplicationException("Error occured starting listeners, check inner exception", e);
}
return true;
}
Je voudrais juste noter que le code de gestion des exceptions semble mauvais, mais la raison en est que j'avais un code de suppression d'exception afin que toutes les exceptions soient supprimées et renvoyées false
si une option de configuration était définie, mais je voulais la supprimer pour souci de brièveté.
Le _serverSocket.BeginAccept (new AsyncCallback (acceptCallback)), _serverSocket) ci-dessus définit essentiellement notre socket serveur pour appeler la méthode acceptCallback chaque fois qu'un utilisateur se connecte. Cette méthode s'exécute à partir du pool de threads .Net, qui gère automatiquement la création de threads de travail supplémentaires si vous avez de nombreuses opérations de blocage. Cela devrait gérer de manière optimale toute charge sur le serveur.
private void acceptCallback(IAsyncResult result)
{
xConnection conn = new xConnection();
try
{
//Finish accepting the connection
System.Net.Sockets.Socket s = (System.Net.Sockets.Socket)result.AsyncState;
conn = new xConnection();
conn.socket = s.EndAccept(result);
conn.buffer = new byte[_bufferSize];
lock (_sockets)
{
_sockets.Add(conn);
}
//Queue recieving of data from the connection
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
//Queue the accept of the next incomming connection
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (SocketException e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
//Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (Exception e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
//Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
}
Le code ci-dessus vient de terminer d'accepter la connexion qui entre, des files d'attente BeginReceive
qui est un rappel qui s'exécutera lorsque le client envoie des données, puis met en file d'attente la suivante acceptCallback
qui acceptera la prochaine connexion client qui entrera.
L' BeginReceive
appel de méthode est ce qui indique à la socket ce qu'elle doit faire lorsqu'elle reçoit des données du client. Pour BeginReceive
, vous devez lui donner un tableau d'octets, où il copiera les données lorsque le client envoie des données. La ReceiveCallback
méthode sera appelée, c'est ainsi que nous gérons la réception de données.
private void ReceiveCallback(IAsyncResult result)
{
//get our connection from the callback
xConnection conn = (xConnection)result.AsyncState;
//catch any errors, we'd better not have any
try
{
//Grab our buffer and count the number of bytes receives
int bytesRead = conn.socket.EndReceive(result);
//make sure we've read something, if we haven't it supposadly means that the client disconnected
if (bytesRead > 0)
{
//put whatever you want to do when you receive data here
//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
}
else
{
//Callback run but no data, close the connection
//supposadly means a disconnect
//and we still have to close the socket, even though we throw the event later
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
catch (SocketException e)
{
//Something went terribly wrong
//which shouldn't have happened
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
}
EDIT: Dans ce modèle, j'ai oublié de mentionner que dans cette zone de code:
//put whatever you want to do when you receive data here
//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
Ce que je ferais généralement, c'est dans le code que vous voulez, c'est de réassembler les paquets en messages, puis de les créer en tant que travaux sur le pool de threads. De cette façon, le BeginReceive du bloc suivant du client n'est pas retardé pendant l'exécution du code de traitement des messages.
Le rappel d'acceptation termine la lecture du socket de données en appelant end receive. Cela remplit le tampon fourni dans la fonction de début de réception. Une fois que vous avez fait ce que vous voulez là où j'ai laissé le commentaire, nous appelons la BeginReceive
méthode suivante qui exécutera à nouveau le rappel si le client envoie plus de données. Maintenant, voici la partie vraiment délicate, lorsque le client envoie des données, votre rappel de réception peut être appelé uniquement avec une partie du message. Le remontage peut devenir très très compliqué. J'ai utilisé ma propre méthode et créé une sorte de protocole propriétaire pour ce faire. Je l'ai laissé de côté, mais si vous le demandez, je peux l'ajouter. Ce gestionnaire était en fait le morceau de code le plus compliqué que j'aie jamais écrit.
public bool Send(byte[] message, xConnection conn)
{
if (conn != null && conn.socket.Connected)
{
lock (conn.socket)
{
//we use a blocking mode send, no async on the outgoing
//since this is primarily a multithreaded application, shouldn't cause problems to send in blocking mode
conn.socket.Send(bytes, bytes.Length, SocketFlags.None);
}
}
else
return false;
return true;
}
La méthode d'envoi ci-dessus utilise en fait un Send
appel synchrone , ce qui me convenait parfaitement en raison de la taille des messages et de la nature multithread de mon application. Si vous souhaitez envoyer à tous les clients, il vous suffit de parcourir la liste _sockets.
La classe xConnection que vous voyez référencée ci-dessus est fondamentalement un simple wrapper pour une socket pour inclure le tampon d'octets, et dans mon implémentation, quelques extras.
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
Aussi pour référence, voici les using
s que j'inclus car je suis toujours ennuyé quand ils ne sont pas inclus.
using System.Net.Sockets;
J'espère que cela est utile, ce n'est peut-être pas le code le plus propre, mais cela fonctionne. Il y a aussi quelques nuances dans le code que vous devriez vous lasser de changer. D'une part, n'en avoir qu'un seul BeginAccept
appelé à la fois. Il y avait un bogue .net très ennuyeux à ce sujet, il y a des années, donc je ne me souviens pas des détails.
De plus, dans le ReceiveCallback
code, nous traitons tout ce qui est reçu de la socket avant de mettre en file d'attente la prochaine réception. Cela signifie que pour un seul socket, nous ne sommes en fait jamais connectés qu'une seule ReceiveCallback
fois à un moment donné et nous n'avons pas besoin d'utiliser la synchronisation des threads. Cependant, si vous réorganisez ceci pour appeler la prochaine réception immédiatement après avoir extrait les données, ce qui peut être un peu plus rapide, vous devrez vous assurer que vous synchronisez correctement les threads.
De plus, j'ai piraté une grande partie de mon code, mais j'ai laissé l'essentiel de ce qui se passe en place. Cela devrait être un bon début pour votre conception. Laissez un commentaire si vous avez d'autres questions à ce sujet.