Serie de Desarrollo de Servidor de Juegos
Esta serie presenta el desarrollo completo de un servidor de juegos en Golang utilizando las mejores prácticas de DevOps/GitOps y Kubernetes en la nube.
El objetivo es proporcionar una guía práctica para el desarrollo backend de serviodres de juegos, permitiendo comprender los principios fundamentales de Golang: "Share memory by communication" (compartir memoria mediante comunicación).
Además, este proyecto incluye técnicas de optimización de rendimiento en Linux (herramientas relacionadas con BPF) y prácticas de SRE (Site Reliability Engineering).
Arquitectura del Servidor
- Comprensión del negocio mediante arquitectura monolítica
- Transformación a arquitectura distribuida y microservicios
- Proyecto demo: go-mahjong-server
Conexión del Cliente al Servidor de Juegos
Activación del Modo Debug
Para habilitar logs detallados en el servidor, utilizamos nano.WithDebugMode():
// internal/game/game.go
nano.Listen(addr,
nano.WithPipeline(pip),
nano.WithHeartbeatInterval(time.Duration(heartbeat)*time.Second),
nano.WithLogger(log.WithField("component", "nano")),
nano.WithSerializer(json.NewSerializer()),
nano.WithComponents(comps),
nano.WithDebugMode(),
)
Análisis de Logs de Conexión
Al iniciar el servidor en modo debug, podemos observar el siguiente flujo de conexión:
...
time="2021-02-18T23:01:16+08:00" level=info msg="Nueva sesión establecida: Remoto=192.168.31.125:62569, UltimoTiempo=1613660476" component=nano
time="2021-02-18T23:01:16+08:00" level=info msg="Handshake de sesión Id=2, Remoto=192.168.31.125:62569" component=nano
time="2021-02-18T23:01:16+08:00" level=info msg="Recibido handshake ACK Id=2, Remoto=192.168.31.125:62569" component=nano
time="2021-02-18T23:01:16+08:00" level=info msg="UID=0, Mensaje={Solicitud Manager.Login (195bytes)}, Datos=&{Nombre:J1 Uid:1 UrlAvatar:http://ejemplo.com/avatar Sexo:1 Tarjetas:10 IP:192.168.31.125}" component=nano
time="2021-02-18T23:01:16+08:00" level=info msg="Jugador: 1 conectado: &{Nombre:J1 Uid:1 UrlAvatar:http://ejemplo.com/avatar Sexo:1 Tarjetas:10 IP:192.168.31.125}"
time="2021-02-18T23:01:16+08:00" level=info msg="Jugador: 1 no está en línea, creando nuevo jugador"
time="2021-02-18T23:01:16+08:00" level=info msg="Agregar sesión al grupo _SYSTEM_MESSAGE_BROADCAST, ID=2, UID=1" component=nano
time="2021-02-18T23:01:16+08:00" level=info msg="Tipo=Respuesta, ID=2, UID=1, MID=1, Datos=&{Uid:1 Nickname:J1 UrlAvatar:http://ejemplo.com/avatar Sexo:1 Tarjetas:10}" component=nano
time="2021-02-18T23:01:16+08:00" level=info msg="[SQL] SELECT `id`, `algo`, `hash`, `salt`, `role`, `status`, `is_online`, `last_login_at`, `priv_key`, `pub_key`, `coin`, `register_at`, `first_recharge_at`, `debug` FROM `user` WHERE `id`=? LIMIT 1 []interface {}{1}" component=model orm=xorm
time="2021-02-18T23:01:16+08:00" level=info msg="Tipo=Push, ID=2, UID=1, Route=onCoinChange, Datos=&{Monedas:10}" component=nano
time="2021-02-18T23:01:16+08:00" level=info msg="Recibido handshake ACK Id=2, Remoto=192.168.31.125:62569" component=nano
time="2021-02-18T23:01:16+08:00" level=info msg="UID=1, Mensaje={Solicitud DeskManager.MesasIncompletas (2bytes)}, Datos=[91 93]" component=nano
time="2021-02-18T23:01:16+08:00" level=debug msg="DeskManager.MesasIncompletas: El jugador no está en una sala" player=1
time="2021-02-18T23:01:16+08:00" level=info msg="Tipo=Respuesta, ID=2, UID=1, MID=2, Datos=&{Existe:false InfoMesa:{NumMesa: CreadoEn:0 Creador:0 Titulo: Desc: Estado:creado Rondas:0 Modo:0}}" component=nano
...
Interpretación del Proceso de Conexión
El flujo de conexión muestra claramente tres eventos importantes:
- Nueva sesión establecida: El servidor acepta una nueva conexión entrante
- Handshake de sesión: El cliente inicia el proceso de handshake con el servidor
- Handshake ACK: Confirmación exitosa del handshake entre cliente y servidor
Este es el proceso de handshake entre el cliente de juegos y el servidor utilizando el framework Nano.
Una vez establecida la conexión subyacente, el cliente envía una solicitud de handshake al servidor con los datos necesarios. El servidor valida estos datos y responde. Si el handshake es exitoso, el cliente envía una confirmación (ACK) al servidor, finalizando así la fase de handshake.
Mensaje de Login
Después del handshake, observamos el siguiente mensaje:
UID=0, Mensaje={Solicitud GestorAuth.IniciarSesion (195bytes)}, Datos=&{Nombre:J1 Uid:1 UrlAvatar:http://ejemplo.com/avatar Sexo:1 Tarjetas:10 IP:192.168.31.125}
Esto indica que el cliente de juegos envía un paquete de tipo Solicitud a la ruta GestorAuth.IniciarSesion del servidor.
Implementación del Handler de Login
Análisis de la Lógica de Negocio
El proceso de login incluye los siguientes pasos:
- Asociar el UID del jugador a la sesión actual
- Consultar la lista de jugadores
- Si el jugador no está en línea:
- Inicializar los datos del jugador
- Asociar la sesión al jugador
- Sincronizar tarjetas de forma asíncrona desde la base de datos
- Agregar el jugador a la lista de gestión
- Si el jugador ya está en línea:
- Restablecer la sesión anterior
- Asociar la nueva sesión
- Agregar al canal de broadcast
- Enviar respuesta al cliente
Protocolo de Comunicación
Definición de las estructuras de datos para el login:
// protocol/login.go
type SolicitudLoginServidor struct {
Nombre string `json:"nombre"`
Uid int64 `json:"uid"`
UrlAvatar string `json:"urlAvatar"`
Sexo int `json:"sexo"` // [0]desconocido [1]masculino [2]femenino
Tarjetas int `json:"tarjetas"`
IP string `json:"ip"`
}
type RespuestaLoginServidor struct {
Uid int64 `json:"uid"`
Apodo string `json:"apodo"`
UrlAvatar string `json:"urlAvatar"`
Sexo int `json:"sexo"`
Tarjetas int `json:"tarjetas"`
}
Resumen del Proceso
El flujo de comunicación entre el cliente y el servidor de juegos sigue un patrón establecido:
- Establecimiento de conexión TCP
- Proceso de handshake
- Envío de credenciales del jugador
- Validación y creación/recuperación de sesión
- Sincronización de estado del jugador
- Confirmación al cliente
Este patrón garantiza una comunicación confiable y segura entre el cliente y el servidor, permitiendo que los jugadores puedan unirse a partidas de mahjong de manera eficiente.