mensaje - uso de sockets en c# parte 2[cliente/servidor]
Comenzar con la programación de socket en C#- Mejores prácticas (3)
He visto muchos recursos aquí en SO sobre Sockets. Creo que ninguno de ellos cubrió los detalles que yo quería saber. En mi aplicación, el servidor hace todo el procesamiento y envía actualizaciones periódicas a los clientes.
La intención de esta publicación es cubrir todas las ideas básicas requeridas al desarrollar una aplicación de socket y analizar las mejores prácticas. Estas son las cosas básicas que verá con casi todas las aplicaciones basadas en socket.
1 - Encuadernación y escucha en un socket
Estoy usando el siguiente código. Funciona bien en mi máquina. ¿Debo ocuparme de otra cosa cuando implemente esto en un servidor real?
IPHostEntry localHost = Dns.GetHostEntry(Dns.GetHostName());
IPEndPoint endPoint = new IPEndPoint(localHost.AddressList[0], 4444);
serverSocket = new Socket(endPoint.AddressFamily, SocketType.Stream,
ProtocolType.Tcp);
serverSocket.Bind(endPoint);
serverSocket.Listen(10);
2 - Recepción de datos
He usado una matriz de 255 bytes de tamaño. Entonces, cuando recibo datos de más de 255 bytes, necesito llamar al método de recepción hasta que obtenga la información completa, ¿verdad? Una vez que obtuve la información completa, necesito anexar todos los bytes recibidos hasta ahora para obtener el mensaje completo. ¿Es eso correcto? ¿O hay un mejor enfoque?
3 - Envío de datos y especificación de la longitud de los datos
Dado que no hay forma de que TCP encuentre la longitud del mensaje a recibir, estoy planeando agregar la longitud del mensaje. Este será el primer byte del paquete. De modo que los sistemas del cliente saben cuántos datos hay disponibles para leer.
¿Algún otro mejor enfoque?
4 - Cierre del cliente
Cuando el cliente está cerrado, enviará un mensaje al servidor indicando el cierre. El servidor eliminará los detalles del cliente de su lista de clientes. A continuación se muestra el código utilizado en el lado del cliente para desconectar el zócalo (no se muestra la parte de mensajes).
client.Shutdown(SocketShutdown.Both);
client.Close();
Alguna sugerencia o problema?
5 - Cerrar el servidor
El servidor envía un mensaje a todos los clientes que indica el cierre. Cada cliente desconectará el socket cuando reciba este mensaje. Los clientes enviarán el mensaje de cierre al servidor y lo cerrarán. Una vez que el servidor recibe un mensaje de cierre de todos los clientes, desconecta el socket y deja de escuchar. Call Dispose en cada sockets de cliente para liberar los recursos. ¿Es ese el enfoque correcto?
6 - Desconexiones de clientes desconocidas
A veces, un cliente puede desconectarse sin informar al servidor. Mi plan para manejar esto es: cuando el servidor envía mensajes a todos los clientes, verifique el estado del socket. Si no está conectado, elimine ese cliente de la lista de clientes y cierre el socket para ese cliente.
¡Cualquier ayuda sería genial!
1 - Encuadernación y escucha en un socket
Luce bien para mi. Sin embargo, su código vinculará el socket solo a una dirección IP. Si simplemente desea escuchar en cualquier dirección IP / interfaz de red, use IPAddress.Any
:
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444));
Para ser una prueba de futuro, es posible que desee admitir IPv6. Para escuchar en cualquier dirección IPv6, use IPAddress.IPv6Any
en lugar de IPAddress.Any
.
Tenga en cuenta que no puede escuchar en ningún IPv4 ni en ninguna dirección IPv6 al mismo tiempo, excepto si usa un socket Dual-Stack . Esto requerirá que desactive la opción de socket IPV6_V6ONLY
:
serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, 0);
Para habilitar Teredo con su socket, debe configurar la opción de socket PROTECTION_LEVEL_UNRESTRICTED
:
serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)23, 10);
2 - Recepción de datos
Recomiendo usar un NetworkStream
que envuelva el socket en un Stream
lugar de leer los fragmentos manualmente.
Leer un número fijo de bytes es un poco incómodo:
using (var stream = new NetworkStream(serverSocket)) {
var buffer = new byte[MaxMessageLength];
while (true) {
int type = stream.ReadByte();
if (type == BYE) break;
int length = stream.ReadByte();
int offset = 0;
do
offset += stream.Read(buffer, offset, length - offset);
while (offset < length);
ProcessMessage(type, buffer, 0, length);
}
}
Donde realmente brilla NetworkStream
es que puedes usarlo como cualquier otro Stream
. Si la seguridad es importante, simplemente SslStream
el NetworkStream
en un SslStream
para autenticar el servidor y (opcionalmente) los clientes con certificados X.509. La compresión funciona de la misma manera.
var sslStream = new SslStream(stream, false);
sslStream.AuthenticateAsServer(serverCertificate, false, SslProtocols.Tls, true);
// receive/send data SSL secured
3 - Envío de datos y especificación de la longitud de los datos
Su enfoque debería funcionar, aunque probablemente no quiera ir por el camino de reinventar la rueda y diseñar un nuevo protocolo para esto. Eche un vistazo a BEEP o tal vez incluso algo simple como protobuf .
Dependiendo de sus objetivos, podría valer la pena pensar en elegir una abstracción sobre sockets como WCF o algún otro mecanismo RPC.
4/5/6 - Desconexiones de cierre y desconocidas
Lo que jerryjvl dijo :-) El único mecanismo de detección confiable son los pings o enviar keep-alives cuando la conexión está inactiva.
Si bien debe lidiar con desconexiones desconocidas en cualquier caso, yo personalmente mantendría algún elemento de protocolo para cerrar una conexión de mutuo acuerdo en lugar de simplemente cerrarlo sin previo aviso.
Como esto es ''comenzar'', mi respuesta se quedará con una implementación simple en lugar de una altamente escalable. Lo mejor es primero sentirse cómodo con el enfoque simple antes de complicar las cosas.
1 - Encuadernación y escucha
Tu código me parece bien, personalmente uso:
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444));
En lugar de ir por la ruta DNS, pero no creo que haya un problema real de ninguna manera.
1.5 - Aceptando conexiones del cliente
Solo mencionar esto por completitud ... supongo que estás haciendo esto, de lo contrario, no llegarías al paso 2.
2 - Recepción de datos
Haría que el buffer fuera un poco más largo que 255 bytes, a menos que pueda esperar que todos los mensajes de su servidor tengan como máximo 255 bytes. Creo que querría un búfer que probablemente sea más grande que el tamaño del paquete TCP, por lo que puede evitar hacer múltiples lecturas para recibir un solo bloque de datos.
Yo diría que escoger 1500 bytes debería estar bien, o tal vez incluso 2048 para un buen número redondo.
Alternativamente, tal vez pueda evitar el uso de un byte[]
para almacenar fragmentos de datos y, en su lugar, envolver su socket de cliente del lado del servidor en un NetworkStream
, envuelto en un BinaryReader
, para poder leer los componentes de su mensaje directamente desde el socket sin preocuparse sobre tamaños de buffer
3 - Envío de datos y especificación de la longitud de los datos
Su enfoque funcionará bien, pero obviamente requiere que sea fácil calcular la longitud del paquete antes de comenzar a enviarlo.
Alternativamente, si su formato de mensaje (orden de sus componentes) está diseñado de manera tal que en cualquier momento el cliente podrá determinar si debe haber más datos a continuación (por ejemplo, el código 0x01 significa que el siguiente será un int y un cadena, el código 0x02 significa que el siguiente será 16 bytes, etc., etc.). Combinado con el enfoque NetworkStream
en el lado del cliente, este puede ser un enfoque muy efectivo.
Para estar seguro, es posible que desee agregar la validación de los componentes que se reciben para asegurarse de que solo procesa valores sanos. Por ejemplo, si recibe una indicación para una cadena de 1TB de longitud, es posible que haya tenido un paquete dañado en alguna parte, y puede ser más seguro cerrar la conexión y forzar al cliente a volver a conectarse y ''comenzar de nuevo''. Este enfoque le da un muy buen comportamiento general en caso de fallas inesperadas.
4/5 - Cerrando el cliente y el servidor
Personalmente, optaría por Close
sin más mensajes; cuando se cierra una conexión, obtendrá una excepción en cualquier bloqueo de lectura / escritura en el otro extremo de la conexión que tendrá que atender.
Ya que tiene que atender ''desconexiones desconocidas'' de todos modos para obtener una solución robusta, hacer la desconexión más complicada generalmente no tiene sentido.
6 - Desconexiones desconocidas
No confiaría ni siquiera en el estado del socket ... es posible que una conexión muera en algún lugar a lo largo de la ruta entre el cliente / servidor sin que el cliente o el servidor lo noten.
La única manera garantizada de decirle a una conexión que ha fallecido inesperadamente es cuando intenta enviar algo a lo largo de la conexión. En ese momento siempre obtendrá una excepción que indica falla si algo salió mal con la conexión.
Como resultado, la única forma infalible de detectar todas las conexiones inesperadas es implementar un mecanismo ''ping'', donde idealmente el cliente y el servidor enviarán periódicamente un mensaje al otro extremo que solo da como resultado un mensaje de respuesta que indica que el ''ping'' fue recibido.
Para optimizar los pings innecesarios, es posible que desee tener un mecanismo de "tiempo de espera" que solo envíe un ping cuando no se haya recibido otro tráfico desde el otro extremo durante un período de tiempo determinado (por ejemplo, si el último mensaje del el servidor tiene más de x segundos de antigüedad, el cliente envía un ping para asegurarse de que la conexión no haya muerto sin notificación).
Más avanzado
Si desea una alta escalabilidad, tendrá que buscar métodos asíncronos para todas las operaciones de socket (Aceptar / Enviar / Recibir). Estas son las variantes ''Begin / End'', pero son mucho más complicadas de usar.
Recomiendo probar esto hasta que tengas la versión simple en funcionamiento.
También tenga en cuenta que si no planea escalar más de unas pocas docenas de clientes, esto no será un problema. Las técnicas de sincronización solo son realmente necesarias si tiene la intención de escalar en los miles o cientos de miles de clientes conectados sin que su servidor muera por completo.
Probablemente he olvidado un montón de otras sugerencias importantes, pero esto debería ser suficiente para lograr una implementación bastante sólida y confiable para comenzar con
Considere el uso de conectores asíncronos. Puede encontrar más información sobre el tema en