Antes de comenzar el desarrollo del proyecto grupal Worms para la materia Taller de Programacion I, se realizaron dos pruebas de concepto (PoC) individuales en C++ con el objetivo de experimentar y comprender en profundidad las tecnologías centrales del trabajo final:
- Multithreading
- Sockets y networking
De esta forma nos familiarizamos con estas tecnologias antes de embarcarnos a desarrollar el TP real con el objetivo de tomar mejores decisiones luego.
Se puede acceder al codigo y al enunciado de esta PoC.
El objetivo de esta PoC era familiarizarse con:
- Encapsulación de sockets en C++
- Diseño de un protocolo binario
- Separación en capas (network / protocol / game / UI)
- Uso correcto de
RAIIy la librería estándarSTLdeC++ - Comunicación cliente-servidor bloqueante
En esta PoC el servidor atiende un único cliente.
Se trabajó con separación estricta de responsabilidades:
Socket: Encapsulación del file descriptorProtocolo: Serialización/deserializaciónGame: Lógica del gusanoClient/Server: Orquestación
Cumple con las restricciones técnicas:
C++17POSIX 2008- Sin variables globales, funciones globales y goto
- Sockets bloqueantes
- Debe haber una clase Socket tanto el socket aceptador como el socket usado para la comunicación.
- Debe haber una clase Protocolo que encapsule la serialización y deserialización de los mensajes entre el cliente y el servidor.
- Uso de
RAII - Uso de
STL
Se implementa un cliente que simula comandos de un jugador desde stdin:
select <scenario>dir <direction>movejump <type>
El servidor:
- Carga escenarios desde archivo
- Emula la lógica del gusano (posición entera x,y)
- Valida colisiones
- Simula gravedad
- Devuelve siempre la posición final calculada
- El cliente imprime la posición recibida
Se implementó un protocolo binario explícito con serialización manual.
Cliente → Servidor
01 <L> <N>: Seleccionar y crear una nueva partida con el escenario pedido. El campo determina la longitud del nombre ; es un entero de 16 bits sin signo en big endian y es una secuencia de bytes sin el \0.03 <D>: Cambiar de dirección en la que mira el gusano. El campo es un entero de 8 bits sin signo. Vale 00 si la nueva dirección es a la izquierda o 01 si es a la derecha.04: Mover el gusano05 <T>: Realizar un salto. El campo es un entero de 8 bits sin signo. Vale 00 si el salto es hacia adelante o 01 si el salto es hacia atrás.
Servidor → Cliente
00 <X> <Y>: Código de éxito al seleccionar y crear una nueva partida, seguido de la posición inicial del gusano. Tanto como son enteros de 32 bits sin signo en big endian.01: Código de error al seleccionar y crear una nueva partida (el escenario no fue encontrado).<X> <Y>: Respuesta a todos los casos. El servidor responde con la posición final del gusano. Tanto como son enteros de 32 bits sin signo en big endian.
Cliente: ./client <ip/hostname server> <puerto/servicename>
Servidor: ./server <puerto/servicename> <escenarios>
El archivo <escenarios> tiene la definición de los escenarios que el cliente podrá elegir y crear una nueva
partida con él.
Se puede acceder al codigo y al enunciado de esta PoC.
El objetivo de esta PoC era familiarizarse con:
- Multithreading en C++
- Race conditions
- Sincronización con mutex (
std::mutex) y condition variables (std::condition_variable) - Uso de monitores
- Thread-safe queues
- RAII aplicado a threads
El foco no fue la lógica del juego sino el modelo concurrente del servidor.
Se implementa un servidor tipo lobby:
- Múltiples clientes pueden conectarse
- Cada cliente puede enviar mensajes de chat
- El servidor realiza broadcast a todos
- Se notifica la cantidad de jugadores conectados
Cada vez que un jugador se una o se retire del lobby, el servidor le envia un mensaje a todos los clientes conectados con la cantidad total de jugadores en el lobby. Además, cada jugador podrá enviar mensajes de chat: el servidor deberá obtenerlos y enviarlos a todos los clientes conectados, sean jugadores o espectadores. Es un broadcast. Tanto el servidor como los clientes deberán imprimirlos.
Mensajes posibles:
Jugadores <N>, esperando al resto de tus amigos...<chatmsg>
El servidor crea:
- Dos threads por cliente: a. Receiver thread b. Sender thread
- Un thread aceptador (lobby)
- Thread principal
Dado que los mensajes de chat de un jugador son enviados al resto de los clientes y que el servidor también envia mensajes a todos los clientes, esto impone una race condition sobre los sockets (múltiples threads quieren enviar usando el mismo socket). Por ello el servidor tiene una thread-safe queue por cada thread del cliente que se dedique a enviar mensajes hacia el cliente (outgoing queues).
Las race conditions sobre los sockets no son las únicas: como también se permite que los clientes se unan y retiren del lobby en distintos tiempos, hay que agregar o remover la queue de cada cliente de “la lista de queues” del lobby y como esta está compartida entre threads se protege con un monitor (hay múltiples threads recorriendo esa lista para hacer un broadcast).
En la solución implementada:
- Se tene una thread-safe queue por cliente
- El sender thread consume de su propia queue
- Un monitor protege la estructura compartida de queues
- Uso de:
a.
std::mutexb. std::condition_variabled. Colas bloqueantes
Se evita:
- Uso de
sleep()para sincronizar - Deadlocks
- Race conditions
Cliente → Servidor:
0x05 <len> <chatmsg>: El campo0x05es un byte con el número literal0x05,<chatmsg length>son 2 bytes sin signo en big endian con la longitud del mensaje y<chatmsg>es el mensaje sin el salto de línea sin el\0al final.
Servidor → Cliente:
0x06 <player_cnt>: El campo0x06es un byte con el número literal0x06y<player cnt>es un campo de 1 byte sin signo con la cantidad de jugadores actualmente unidos a la partida y esperando en el lobby.0x09 <len> <chatmsg>: Es el mensaje de chat que un cliente envió al servidor y este lo está reenviando a todos los jugadores.
Cliente: ./client <ip/hostname server> <puerto/servicename>
Servidor: ./server <puerto/servicename>
El servidor lee de la entrada estándar a la espera de leer la letra q que le indica que debe finalizar cerrando todos los sockets, queues y joinenando todos los threads sin enviar ningún mensaje adicional ni imprimir por salida estándar.
El servidor:
- Lee
qdesde stdin - Cierra sockets
- Finaliza queues
- Hace
join()de todos los threads - Libera recursos correctamente (
RAII)