J'ai déjà écrit quelque chose de similaire dans le passé. Mes recherches d'il y a quelques années m'ont montré qu'écrire votre propre implémentation de socket était la meilleure solution, en utilisant la fonction asynchrone sockets. Cela signifie que les clients qui ne font pas vraiment quelque chose nécessitent relativement peu de ressources. Tout ce qui se passe est géré par le pool de threads de .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 du client, mais si vous avez besoin de recherches plus rapides pour des listes plus importantes, vous pouvez l'écrire comme vous le souhaitez.
private List<xConnection> _sockets;
Il faut aussi que le socket é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 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("An error occurred 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 rear previously, so make sure there is only one
//BeginAccept running
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (Exception e)
{
throw new ApplicationException("An error occurred starting listeners. Check inner exception", e);
}
return true;
}
J'aimerais juste noter que le code de gestion des exceptions a l'air mauvais, mais la raison en est que j'avais un code de suppression des exceptions dans ce code afin que toutes les exceptions soient supprimées et que le retour soit effectué. false
si une option de configuration était définie, mais je voulais l'enlever par souci de brièveté.
La commande _serverSocket.BeginAccept(new AsyncCallback(acceptCallback)), _serverSocket) ci-dessus configure essentiellement notre socket serveur pour qu'il appelle 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 bloquantes. Cela devrait permettre de 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 receiving 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 incoming 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 qui arrive, met en file d'attente BeginReceive
qui est un callback qui s'exécute lorsque le client envoie des données, puis met en attente le prochain acceptCallback
qui acceptera la prochaine connexion client qui arrivera.
El BeginReceive
est ce qui 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 enverra des données. Le site ReceiveCallback
sera appelée, c'est ainsi que 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 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);
En général, dans le code que vous voulez, je ferais le réassemblage des paquets en messages, puis je les créerais comme des tâches 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 s'exécute.
Le callback accept termine la lecture du socket de données en appelant end receive. Cela remplit le tampon fourni par la fonction begin receive. Une fois que vous avez fait ce que vous vouliez à l'endroit où j'ai laissé le commentaire, nous appelons la fonction suivante BeginReceive
qui exécutera à nouveau le callback si le client envoie d'autres données.
Maintenant, voici la partie vraiment délicate : Lorsque le client envoie des données, votre callback de réception peut n'être appelé qu'avec une partie du message. Le réassemblage peut devenir très très compliqué. J'ai utilisé ma propre méthode et créé une sorte de protocole propriétaire pour faire cela. 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 une méthode synchrone Send
appel. Pour moi, cela convenait parfaitement en raison de la taille des messages et de la nature multithread de mon application. Si vous voulez envoyer des messages à tous les clients, il vous suffit de parcourir en boucle la liste _sockets.
La classe xConnection que vous voyez référencée ci-dessus est essentiellement une simple enveloppe pour une socket afin d'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;
}
Pour référence, voici également les using
que j'inclus, car je suis toujours contrarié 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 il fonctionne. Il y a également quelques nuances dans le code que vous devriez être prudent de changer. Par exemple, il n'y a qu'un seul BeginAccept
appelé à un moment donné. 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 même, dans le ReceiveCallback
nous traitons tout ce qui est reçu du socket avant de mettre en file d'attente la réception suivante. Cela signifie que pour une seule socket, nous ne sommes jamais en fait que dans le mode ReceiveCallback
une fois à tout moment, et nous n'avons pas besoin d'utiliser la synchronisation des threads. Cependant, si vous réorganisez cette opération pour appeler la réception suivante immédiatement après avoir extrait les données, ce qui pourrait être un peu plus rapide, vous devrez vous assurer que vous synchronisez correctement les threads.
J'ai également supprimé 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.
2 votes
Êtes-vous absolument sûr qu'il doit s'agir d'une connexion longue durée ? C'est difficile à dire à partir du peu d'informations fournies, mais je ne le ferais que si c'est absolument nécessaire
0 votes
Oui, il faut qu'elle soit de longue durée. Les données doivent être mises à jour en temps réel, donc je ne peux pas faire de sondage périodique, les données doivent être poussées vers le client au fur et à mesure qu'elles se produisent, ce qui signifie une connexion constante.
1 votes
Ce n'est pas une raison valable. Http supporte très bien les connexions de longue durée. Vous ouvrez simplement une connexion et attendez une réponse (stalled poll). Cela fonctionne bien pour de nombreuses applications de style AJAX, etc. Comment pensez-vous que gmail fonctionne :-)
2 votes
Gmail fonctionne en interrogeant périodiquement les e-mails, il ne maintient pas une connexion longue durée. Cela convient parfaitement aux courriels pour lesquels une réponse en temps réel n'est pas nécessaire.
0 votes
On pourrait discuter de la façon dont il doit être "en temps réel".
2 votes
L'interrogation, ou l'extraction, est bien adaptée, mais la latence se développe rapidement. Le push n'est pas aussi efficace, mais il permet de réduire ou d'éliminer la latence.
0 votes
Le push est beaucoup plus efficace si seul un sous-ensemble d'utilisateurs a besoin d'être informé de ce qui se passe. L'interrogation est associée à une surcharge de mise à l'échelle très élevée, puisque votre serveur passe la plupart de son temps à répondre aux requêtes indiquant qu'il n'y a pas de mises à jour. Cela ajoute également une charge réseau, ce qui est particulièrement important dans les applications mobiles... Ne mettez jamais en œuvre une solution d'interrogation sur un réseau de données cellulaire.
0 votes
@MystereMan Man Je suis à la recherche d'un serveur tcp haute performance très similaire à ce que vous décrivez. Avez-vous réussi à le faire fonctionner ? Si oui, qu'avez-vous fait ?
0 votes
Des centaines de clients ne nécessitent rien de surprenant en termes d'évolutivité. Des sockets bloqués avec des threads feront l'affaire. Les connexions de longue durée n'ont pas d'incidence.