Estoy trabajando en un servidor de juegos genérico que administra juegos para un número arbitrario de clientes de red de socket TCP que juegan un juego. Tengo un 'diseño' pirateado junto con cinta adhesiva que funciona, pero parece frágil e inflexible. ¿Existe un patrón bien establecido sobre cómo escribir una comunicación cliente / servidor que sea robusto y flexible? (Si no, ¿cómo mejorarías lo que tengo a continuación?)
Aproximadamente tengo esto:
- Al configurar un juego, el servidor tiene un hilo para cada socket de jugador que maneja las solicitudes síncronas de un cliente y las respuestas del servidor.
- Sin embargo, una vez que el juego se está ejecutando, todos los hilos, excepto uno, duermen, y ese hilo se desplaza por todos los jugadores uno por uno y se comunica sobre su movimiento (en solicitud-respuesta invertida).
Aquí hay un diagrama de lo que tengo actualmente; haga clic para obtener una versión más grande / legible, o PDF de 66kB .
Problemas:
- Requiere que los jugadores respondan exactamente a su vez con exactamente el mensaje correcto. (Supongo que podría dejar que cada jugador responda con una mierda aleatoria y solo seguir adelante una vez que me den un movimiento válido).
- No permite a los jugadores hablar con el servidor a menos que sea su turno. (Podría hacer que el servidor les envíe actualizaciones sobre otros jugadores, pero no procesar una solicitud asincrónica).
Requisitos finales:
El rendimiento no es primordial. Esto se usará principalmente para juegos que no sean en tiempo real, y principalmente para enfrentar a las IA entre sí, no a los humanos nerviosos.
El juego siempre estará basado en turnos (incluso en una resolución muy alta). Cada jugador siempre obtiene un movimiento procesado antes de que todos los demás jugadores tengan un turno.
La implementación del servidor sucede en Ruby, si eso marca la diferencia.
Respuestas:
No estoy seguro de qué es exactamente lo que quieres lograr. Pero, hay un patrón que se usa constantemente en los servidores de juegos y que puede ayudarte. Usa colas de mensajes.
Para ser más específico: cuando los clientes envían mensajes al servidor, no los procese de inmediato. Más bien, analícelos y póngalos en una cola para este cliente específico. Luego, en algún bucle principal (tal vez incluso en otro hilo) revise todos los clientes, recupere los mensajes de la cola y los procese. Cuando el procesamiento indique que el turno de este cliente ha finalizado, pase al siguiente.
De esta manera, los clientes no están obligados a trabajar estrictamente giro por giro; solo lo suficientemente rápido como para que tenga algo en la cola para cuando se procese el cliente (por supuesto, puede esperar al cliente o saltear su turno si se retrasa). Y puede agregar soporte para solicitudes asincrónicas agregando una cola "asíncrona": cuando un cliente envía una solicitud especial, se agrega a esta cola especial; esta cola se verifica y procesa con más frecuencia que las de los clientes.
fuente
Los hilos de hardware no se escalan lo suficientemente bien como para hacer de "uno por jugador" una idea razonable para un número de jugadores de 3 dígitos, y la lógica de saber cuándo despertarlos es una complejidad que crecerá. Una mejor idea es encontrar un paquete de E / S asíncrono para Ruby que le permita enviar y recibir datos sin tener que pausar un hilo completo del programa mientras se realiza la operación de lectura o escritura. Esto también resuelve el problema de esperar a que los jugadores respondan, ya que no tendrás ningún hilo en una operación de lectura. En cambio, su servidor solo puede verificar si un límite de tiempo ha expirado y luego notificar al otro jugador en consecuencia.
Básicamente, 'E / S asíncrona' es el 'patrón' que está buscando, aunque en realidad no es un patrón, más un enfoque. En lugar de llamar explícitamente a 'leer' en un socket y pausar el programa hasta que lleguen los datos, configura el sistema para que llame a su controlador 'onRead' cuando tenga datos listos y continúe procesando hasta ese momento.
Cada jugador tiene un turno y cada jugador tiene un socket que envía datos, que es un poco diferente. Es posible que algún día no quieras un socket por jugador. Es posible que no use enchufes en absoluto. Mantenga las áreas de responsabilidad separadas. Lo sentimos si esto suena como un detalle sin importancia, pero cuando combina conceptos en su diseño que deberían ser diferentes, entonces hace que sea más difícil encontrar y discutir mejores enfoques.
fuente
Ciertamente hay más de una forma de hacerlo, pero personalmente, me saltaría los hilos por completo, y solo usaría un bucle de eventos. La forma en que lo haga dependerá en cierta medida de la biblioteca de E / S que esté utilizando, pero básicamente, el bucle del servidor principal se verá así:
Por ejemplo, supongamos que tiene n clientes involucrados en un juego. Cuando los primeros n − 1 de ellos envían sus movimientos, simplemente verifica que el movimiento parece válido y envía un mensaje que dice que se ha recibido el movimiento, pero aún estás esperando que otros jugadores se muevan. Después de que todos los n jugadores se hayan movido, procesas todos los movimientos que has guardado y envías los resultados a todos los jugadores.
También puede refinar este esquema para incluir tiempos de espera: la mayoría de las bibliotecas de E / S deben tener algún mecanismo para esperar hasta que lleguen nuevos datos o haya transcurrido un período de tiempo determinado.
Por supuesto, también podría implementar algo como esto con subprocesos individuales para cada conexión, haciendo que esos subprocesos pasen cualquier solicitud que no puedan manejar directamente a un subproceso central (ya sea uno por juego o uno por servidor) que ejecuta un bucle como se muestra arriba, excepto que habla con los hilos del controlador de conexión en lugar de hablar directamente con los clientes. Ya sea que encuentre esto más simple o más complicado que el enfoque de un solo hilo, realmente depende de usted.
fuente