2009-07-22 24 views
29

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.Introducción a la programación de socket en C# - Mejores prácticas

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 la 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 utilizado una matriz de bytes de tamaño 255. 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 - El envío de datos y especificar la longitud de datos

Puesto que no hay forma en TCP para encontrar la longitud del mensaje para recibir, tengo la intención de sumar 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 enfoque mejor?

4 - Cerrando el cliente

Cuando el cliente está cerrado, se enviará un mensaje al servidor que indica la estrecha. 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 - Cerrando el servidor

servidor envía un mensaje a todos los clientes que indican 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. Llame al Elimine en cada socket de cliente para liberar los recursos. ¿Es ese el enfoque correcto?

6 - desconexiones cliente desconocidos

A veces, un cliente puede desconectar 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!

+0

he tenido problemas con la Dirección IP de escucha en alguna versión anterior de Windows. Uso de IPAddress ipAddress = new IPAddress (nuevo byte [] {0, 0, 0, 0}); parece ser lo mejor para mi! – clamchoda

Respuesta

24

Dado que esto es 'comenzar', mi respuesta se limitará a una implementación simple en lugar de una muy escalable. Lo mejor es primero sentirse cómodo con el enfoque simple antes de complicar las cosas.

1 - Encuadernación y escuchar
Su código parece bien a mí, personalmente utilizo:

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444)); 

En lugar de tomar la ruta de DNS, pero no creo que hay un problema real, ya sea camino.

1,5 - Aceptar conexiones de clientes
Sólo mencionar esto por el bien integridad ... Estoy asumiendo que usted está haciendo esto de otra manera no podrían obtener al paso 2.

2 - Recepción de datos
Haría que el búfer fuera un poco más largo que 255 bytes, a menos que pueda esperar que todos los mensajes del 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 se puede evitar el uso de un byte[] para almacenar fragmentos de datos, y en lugar de envolver su socket de cliente-servidor en un NetworkStream, envuelto en una BinaryReader, por lo que se puede leer los componentes de su mensaje direclty de de la toma sin preocuparse por el tamaño del buffer.

3 - El envío de datos y especificar la longitud de datos
Su enfoque funciona muy bien, pero sí requiere obviamente que es fácil calcular la longitud del paquete antes de empezar 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 (por ejemplo, el código 0x01 significa que el siguiente será un int y una 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 eficaz.

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 - Cierre el cliente y el servidor
Personalmente optaría por sólo 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 abastecerse de 'desconexiones desconocidas' de todos modos para obtener una solución robusta, hacer la desconexión más complicada generalmente no tiene sentido.

6 - desconexiones Desconocida
yo no confiaría incluso el estado de toma ... Es posible que una conexión a morir en algún lugar a lo largo del camino entre cliente/servidor sin el cliente o el servidor se diera cuenta.

La única manera garantizada de decirle a una conexión que ha fallecido inesperadamente es cuando vuelva a intentar 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 envían periódicamente un mensaje al otro extremo que solo da como resultado una respuesta mensaje que indica que se recibió el 'ping'.

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 servidor tiene más de x segundos, el cliente envía un ping para asegurarse de que la conexión no haya fallado sin notificación).

más avanzada
Si desea una alta escalabilidad que tendrá que buscar en métodos asincrónicos para todas las operaciones de socket (Aceptar/Enviar/Recibir). Estas son las variantes 'Begin/End', pero son mucho más complicadas de usar.

Recomiendo que no intente esto hasta que tenga 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 conseguir que una implementación bastante robusto y fiable para comenzar con

+1

Esa fue una gran explicación. Gracias –

+0

Solo tenga en cuenta que una vez que haya hecho todo eso probablemente haya más cosas que considerar que he olvidado mencionar ... – jerryjvl

13

1 - encuadernación y la escucha en un socket

Me parece bien. Sin embargo, su código vinculará el socket solo a una dirección IP. Si simplemente desea escuchar en cualquier interfaz IP/red, utilice IPAddress.Any:

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444)); 

Para ser preparada para el futuro, es posible que desee para soportar 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 Dual-Stack Socket.Esto requerirá que desarmar la opción IPV6_V6ONLY socket:

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, 0); 

Para habilitar Teredo con su zócalo, es necesario configurar la opción PROTECTION_LEVEL_UNRESTRICTED socket:

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)23, 10); 

2 - Recepción de datos

Yo recomendaría usar un NetworkStream que envuelve el zócalo en un Stream en lugar de leer los fragmentos manualmente.

La lectura de un número fijo de bytes es un poco incómodo sin embargo:

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); 
    } 
} 

Dónde NetworkStream realmente brilla es que se puede usar como cualquier otro Stream. Si la seguridad es importante, simplemente ajuste 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 - El envío de datos y especificar la longitud de datos

Su enfoque debería funcionar, aunque es probable que no quiera ir por la carretera a reinventar la rueda y el diseño de un nuevo protocolo para esto. Eche un vistazo a BEEP o tal vez incluso algo simple como protobuf.

Según 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 - cierre & desconexiones Desconocida

Lo jerryjvl dichos :-) El único mecanismo de detección fiable son los pings o el envío de HTTP abiertas cuando la conexión está inactiva.

Si bien tiene que ocuparse de 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.

+0

Aunque no puede doler, he encontrado que en la práctica se centra en la recuperación robusta de lo inesperado mensajes mejor que los mensajes de 'desconexión elegante' ... YMMV :) – jerryjvl

+0

Gran publicación. Gracias. ¿Tiene más información sobre el uso de NetworkStream? Cualquier enlace haría. –

+0

Enlace y ejemplo añadido a la respuesta. – dtb

Cuestiones relacionadas