He escrito algo similar a esto en el pasado. Desde mi investigación hace años, demostré que escribir su propia implementación de socket era la mejor opción, utilizando los sockets asíncronos. Esto significaba que los clientes que realmente no hacían nada realmente requerían relativamente pocos recursos. Todo lo que ocurre es manejado por el grupo de hilos .net.
Lo escribí como una clase que gestiona todas las conexiones para los servidores.
Simplemente utilicé una lista para mantener todas las conexiones del cliente, pero si necesita búsquedas más rápidas para listas más grandes, puede escribirla como quiera.
private List<xConnection> _sockets;
También necesita el socket realmente escuchando conexiones entrantes.
private System.Net.Sockets.Socket _serverSocket;
El método de inicio realmente inicia el socket del servidor y comienza a escuchar cualquier conexión entrante.
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;
}
Solo me gustaría señalar que el código de manejo de excepciones se ve mal, pero la razón es que tenía un código de supresión de excepciones allí para que cualquier excepción se suprima y regrese false
si se configuró una opción de configuración, pero quería eliminarlo por brevedad sake.
El _serverSocket.BeginAccept (nuevo AsyncCallback (acceptCallback)), _serverSocket) anterior configura esencialmente nuestro socket de servidor para llamar al método acceptCallback cada vez que un usuario se conecta. Este método se ejecuta desde el conjunto de hilos .Net, que maneja automáticamente la creación de hilos de trabajo adicionales si tiene muchas operaciones de bloqueo. Esto debería manejar de manera óptima cualquier carga en el servidor.
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);
}
}
El código anterior esencialmente acaba de terminar de aceptar la conexión que entra, las colas, BeginReceive
que es una devolución de llamada que se ejecutará cuando el cliente envíe datos, y luego acceptCallback
pone en cola la siguiente que aceptará la próxima conexión del cliente que ingrese.
La BeginReceive
llamada al método es lo que le dice al socket qué hacer cuando recibe datos del cliente. Para BeginReceive
, debe darle una matriz de bytes, que es donde copiará los datos cuando el cliente envíe datos. Se ReceiveCallback
llamará al método, que es cómo manejamos la recepción de datos.
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);
}
}
}
}
EDITAR: en este patrón olvidé mencionar que en esta área de código:
//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);
Lo que generalmente haría es en el código que desee, volver a ensamblar los paquetes en mensajes y luego crearlos como trabajos en el grupo de subprocesos. De esta manera, BeginReceive del siguiente bloque del cliente no se retrasa mientras se ejecuta el código de procesamiento de mensajes.
La devolución de llamada de aceptación finaliza la lectura del socket de datos llamando al final de la recepción. Esto llena el búfer proporcionado en la función de recepción de inicio. Una vez que haga lo que quiera donde dejé el comentario, llamaremos al siguiente BeginReceive
método que ejecutará la devolución de llamada nuevamente si el cliente envía más datos. Ahora aquí está la parte realmente complicada, cuando el cliente envía datos, su devolución de llamada de recepción solo se puede llamar con parte del mensaje. El reensamblaje puede volverse muy, muy complicado. Utilicé mi propio método y creé una especie de protocolo propietario para hacer esto. Lo dejé fuera, pero si lo solicita, puedo agregarlo. Este controlador fue en realidad el código más complicado que jamás haya escrito.
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;
}
El método de envío anterior en realidad usa una Send
llamada síncrona , para mí eso estuvo bien debido al tamaño de los mensajes y la naturaleza multiproceso de mi aplicación. Si desea enviar a cada cliente, simplemente necesita recorrer la lista _sockets.
La clase xConnection que ves mencionada anteriormente es básicamente un contenedor simple para que un socket incluya el búfer de bytes, y en mi implementación, algunos extras.
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
También para referencia aquí están los using
mensajes que incluyo, ya que siempre me molesto cuando no están incluidos.
using System.Net.Sockets;
Espero que sea útil, puede que no sea el código más limpio, pero funciona. También hay algunos matices en el código que debería estar cansado de cambiar. Por un lado, solo tiene una sola BeginAccept
llamada en cualquier momento. Solía haber un error .net muy molesto alrededor de esto, que fue hace años, así que no recuerdo los detalles.
Además, en el ReceiveCallback
código, procesamos todo lo recibido del socket antes de poner en cola la próxima recepción. Esto significa que para un solo socket, en realidad solo estamos ReceiveCallback
una vez en cualquier momento, y no necesitamos usar sincronización de subprocesos. Sin embargo, si reordena esto para llamar a la próxima recepción inmediatamente después de extraer los datos, lo que puede ser un poco más rápido, deberá asegurarse de sincronizar correctamente los hilos.
Además, corté mucho mi código, pero dejé la esencia de lo que está sucediendo en su lugar. Este debería ser un buen comienzo para su diseño. Deja un comentario si tienes más preguntas sobre esto.