Documentation
¶
Overview ¶
client.go añade el rol "cliente" al core en Go — hasta acá, este paquete solo traía NetworkHost/Server (topología Servidor Dedicado); un cliente Go real hacía falta para que un proceso Go (ej. un binario embebido en una app, como netservice.go en el POC de taca-taca) pueda conectarse a un Server externo sin reimplementar el protocolo a mano. Habla el mismo framing binario que el resto de los clientes (typescript/networkcore.ts, csharp/NetworkCore/NetworkClient.cs, etc.).
Package networkcore es el port a Go de forgenet/csharp/NetworkCore: transporte UDP genérico (handshake, heartbeat, input, snapshot, reliable, ping) sin ningún esquema de juego hardcodeado. El juego se engancha vía callbacks (OnInput, OnTick, StateProvider, QueueEvent) — ver host.go.
Es la misma spec de protocolo binario que el core en C#: comparten framing (header, handshake, input, ack, ping) y, a diferencia de la versión previa de este servidor (que hardcodeaba PlayerState), ahora también comparten el formato de snapshot (Tick + StatePayload opaco + Events), porque los dos se generalizaron de la misma forma.
Index ¶
- Constants
- Variables
- func EncodeInputPayload(deltaX, deltaY int16, rotation uint16, actions uint32) []byte
- func EncodeJoinPayload(mode, role uint8, roomCode string) []byte
- func LoadTLSCertificate(certFile, keyFile string) (*tls.Config, error)
- type ClientConnection
- type DevCertificate
- type GameEvent
- type GameSnapshot
- type HandshakeRejectedError
- type NetworkClient
- func (c *NetworkClient) CreateRoom(host string, port uint16, role uint8, timeout time.Duration) error
- func (c *NetworkClient) Disconnect()
- func (c *NetworkClient) JoinRoom(host string, port uint16, role uint8, roomCode string, timeout time.Duration) error
- func (c *NetworkClient) SendInput(deltaX, deltaY int16, rotation uint16, actions uint32, reliable bool)
- func (c *NetworkClient) SendPing()
- type NetworkHost
- func (h *NetworkHost) AdmitPlayer(peer Peer, role uint8) (playerID uint16, reconnected bool)
- func (h *NetworkHost) ConnectedPlayerCount() int
- func (h *NetworkHost) GetClientRole(playerID uint16) (role uint8, ok bool)
- func (h *NetworkHost) HandlePacket(data []byte, peer Peer)
- func (h *NetworkHost) PendingReliableCount(playerID uint16) int
- func (h *NetworkHost) QueueEvent(evt GameEvent)
- func (h *NetworkHost) QueueReliableEvent(evt GameEvent)
- func (h *NetworkHost) Stop()
- type Peer
- type PlayerInput
- type RoomFactory
- type Server
- func (s *Server) CreateRoom() string
- func (s *Server) CreateRoomWithCode(code string) (string, error)
- func (s *Server) HandlePacket(data []byte, peer Peer)
- func (s *Server) StartUDP(port uint16) error
- func (s *Server) StartWebTransport(opts WebTransportOptions) (*DevCertificate, error)
- func (s *Server) Stop()
- func (s *Server) UDPPort() uint16
- type ServerOptions
- type WebTransportOptions
Constants ¶
const ( PacketSnapshot = 0x01 PacketInput = 0x02 PacketAck = 0x03 PacketPing = 0x04 PacketHandshake = 0x05 PacketDisconnect = 0x06 )
Packet types
const ( FlagCompressed = 0x01 FlagReliable = 0x02 FlagOrdered = 0x04 )
Flags
const ( HandshakeModeCreate uint8 = 0x00 HandshakeModeJoin uint8 = 0x01 )
Modos de handshake — quién crea una sala nueva vs. quién se une a una existente por código. Esto sí es genérico: cualquier juego con sesiones de partida necesita esta distinción.
const ( ReasonRoomNotFound uint8 = 0x01 ReasonRoomFull uint8 = 0x02 )
Motivos de rechazo del handshake, enviados en un PacketDisconnect.
const HeaderSize = 9
HeaderSize: seq(4) + ack(4) + typeAndFlags(1)
Variables ¶
var ErrRoomCodeEmpty = errors.New("networkcore: código de sala vacío")
ErrRoomCodeEmpty: CreateRoomWithCode lo devuelve si code es "".
var ErrRoomCodeTaken = errors.New("networkcore: código de sala en uso")
ErrRoomCodeTaken: CreateRoomWithCode lo devuelve si code ya está en uso.
Functions ¶
func EncodeInputPayload ¶
EncodeInputPayload: payload de 10 bytes que va después del header en un PacketInput.
func EncodeJoinPayload ¶
EncodeJoinPayload: Mode(1) + Role(1) + RoomCodeLen(1) + RoomCode(N). Va después del header en un PacketHandshake enviado por el cliente. RoomCode se ignora en HandshakeModeCreate (el server genera el código).
func LoadTLSCertificate ¶
LoadTLSCertificate carga un certificado real (de una CA — ej. Let's Encrypt vía ACME/Certbot corriendo aparte) desde un par de archivos PEM, para usar en WebTransportOptions.TLSConfig en producción. El certificado self-signed que genera GenerateDevCertificate solo lo acepta un browser vía el pinning de serverCertificateHashes — no sirve como reemplazo de un certificado de CA real de cara al público.
Types ¶
type ClientConnection ¶
type ClientConnection struct {
PlayerID uint16
Peer Peer
Role uint8
LastSeq uint32
LastHeartbeat time.Time
Ping int
Connected bool
ReliableQueue []reliableMessage
// contains filtered or unexported fields
}
ClientConnection: estado de sesión de un cliente. No tiene ningún campo de estado de juego — eso vive en el juego, no acá. Role es opaco: el core lo guarda y lo expone (ver GetClientRole) pero nunca lo interpreta — cada juego define sus propios valores y qué significan.
type DevCertificate ¶
type DevCertificate struct {
TLSConfig *tls.Config
HashBase64 string // sha-256 del cert DER, base64 sin padding
}
DevCertificate es el resultado de generar un certificado de desarrollo: el TLS config listo para usar, y el hash que el cliente browser necesita pasarle a `new WebTransport(url, { serverCertificateHashes: [...] })`.
func GenerateDevCertificate ¶
func GenerateDevCertificate() (*DevCertificate, error)
GenerateDevCertificate crea un certificado self-signed ECDSA P-256, válido ~13 días (el máximo que acepta serverCertificateHashes del navegador son 14). Clave nueva en cada llamada — no determinístico a propósito: para pruebas automatizadas conviene leer el hash del resultado en vez de hardcodear uno, y para desarrollo manual alcanza con volver a copiar el hash si el proceso se reinicia.
NO usar esto en producción — ahí hace falta un certificado real de una CA (Let's Encrypt, etc.), el navegador no acepta serverCertificateHashes para eso.
type GameEvent ¶
GameEvent es genérico: el core no sabe qué significa EventType ni qué hay en Data — cada juego define su propia tabla de tipos y formato de payload.
type GameSnapshot ¶
GameSnapshot: Tick/Timestamp son del core (sincronización); el estado del juego en sí viaja como bytes opacos en StatePayload — el core nunca lo interpreta, solo lo transporta.
func DecodeSnapshot ¶
func DecodeSnapshot(payload []byte) (*GameSnapshot, error)
DecodeSnapshot nunca hace panic: un payload corrupto o truncado devuelve error en vez de indexar fuera de rango.
func (*GameSnapshot) Encode ¶
func (s *GameSnapshot) Encode() []byte
Encode: Tick(8) + StatePayloadLen(4) + StatePayload(N) + EventCount(1) + Events(PlayerID(2)+EventType(1)+DataLen(1)+Data).
type HandshakeRejectedError ¶
type HandshakeRejectedError struct {
Reason uint8
}
HandshakeRejectedError: el server rechazó el handshake — sala inexistente (Join a un código que no existe) o sala llena (tope de ServerOptions. MaxPlayersPerRoom).
func (*HandshakeRejectedError) Error ¶
func (e *HandshakeRejectedError) Error() string
type NetworkClient ¶
type NetworkClient struct {
PlayerID uint16
RoomCode string
Connected bool
LastPing int
// --- Hooks: el juego se engancha acá, igual que en NetworkHost ---
OnSnapshot func(snapshot *GameSnapshot)
OnPong func(ms int)
OnClosed func()
// contains filtered or unexported fields
}
NetworkClient: rol "cliente" del protocolo — se conecta a un Server (Go, o cualquier otro lenguaje que hable el mismo framing).
func NewNetworkClient ¶
func NewNetworkClient() *NetworkClient
func (*NetworkClient) CreateRoom ¶
func (c *NetworkClient) CreateRoom(host string, port uint16, role uint8, timeout time.Duration) error
CreateRoom hace un handshake HandshakeModeCreate: le pide al server que arranque una sala nueva. Si conecta a tiempo, arranca el loop de recepción en background y deja el código de sala asignado en RoomCode (para poder compartirlo — ej. como QR). Bloquea hasta que llega la respuesta o expira timeout.
func (*NetworkClient) Disconnect ¶
func (c *NetworkClient) Disconnect()
func (*NetworkClient) JoinRoom ¶
func (c *NetworkClient) JoinRoom(host string, port uint16, role uint8, roomCode string, timeout time.Duration) error
JoinRoom se une a una sala existente por código (HandshakeModeJoin) — ya sea porque el código lo compartió otro cliente, o porque este mismo cliente lo recuerda de una conexión anterior y está reconectando (ver el comentario grande en NetworkHost.AdmitPlayer: el server reconoce la reconexión por IP y devuelve la misma identidad).
func (*NetworkClient) SendInput ¶
func (c *NetworkClient) SendInput(deltaX, deltaY int16, rotation uint16, actions uint32, reliable bool)
func (*NetworkClient) SendPing ¶
func (c *NetworkClient) SendPing()
type NetworkHost ¶
type NetworkHost struct {
// --- Hooks genéricos: acá es donde el juego se engancha ---
// OnPlayerConnected: reconnected=true si esto fue una reconexión
// reconocida por IP (ver AdmitPlayer) — el juego puede usarlo para NO
// pisar el estado que ya tenía ese jugador con uno vacío.
OnPlayerConnected func(playerID uint16, role uint8, reconnected bool)
OnPlayerDisconnected func(playerID uint16)
OnInput func(input PlayerInput)
OnTick func(tick uint64)
StateProvider func() []byte
// contains filtered or unexported fields
}
NetworkHost: rol "servidor" del protocolo para UNA sala/partida. No sabe nada del juego que corre encima — se engancha vía los campos de callback de abajo, igual que NetworkHost en el core de C#. Tampoco sabe de qué transporte vienen los paquetes (UDP, WebTransport, o ambos a la vez) — ver Peer — ni a qué sala pertenece: eso lo decide Server, que crea una instancia de NetworkHost por sala y le rutea sus paquetes (ver server.go).
func NewNetworkHost ¶
func NewNetworkHost() *NetworkHost
NewNetworkHost crea un host sin arrancarlo todavía.
func (*NetworkHost) AdmitPlayer ¶
func (h *NetworkHost) AdmitPlayer(peer Peer, role uint8) (playerID uint16, reconnected bool)
AdmitPlayer registra un cliente nuevo en esta sala, o reconoce una reconexión, y dispara OnPlayerConnected(playerID, role, reconnected). La llama Server, después de decidir (crear/unirse) a qué sala pertenece el cliente. Role es opaco para el core — cada juego define sus propios valores y qué hacer con ellos (ej. no crear una "barra" de juego para el rol que representa al tablero).
Reconexión: si hay un cliente marcado desconectado (ver heartbeatLoop) con la misma IP (Peer.IP()) que quien está haciendo el handshake, se le devuelve la MISMA identidad (PlayerID, y el Role ORIGINAL — no el que venga en este handshake, para no perder de vista quién era) en vez de crear un jugador nuevo. Es una heurística por IP, no un token de sesión (el protocolo no lleva uno todavía): funciona bien en la LAN/datos móviles típica, donde cada dispositivo tiene su propia IP — no es a prueba de balas si dos jugadores comparten la misma IP pública (NAT compartido) y ambos están desconectados a la vez.
Idempotente además en el sentido de siempre: un cliente que ya está admitido (mismo Peer.Key(), sin pasar por desconexión) devuelve su PlayerID actual sin duplicar estado ni disparar el hook de nuevo (cubre el reintento de un handshake cuyo ack se perdió).
func (*NetworkHost) ConnectedPlayerCount ¶
func (h *NetworkHost) ConnectedPlayerCount() int
ConnectedPlayerCount solo cuenta Connected==true — h.clients también incluye clientes desconectados que quedaron "reclamables" para una reconexión (ver heartbeatLoop/AdmitPlayer), así que len(h.clients) ya no alcanza.
func (*NetworkHost) GetClientRole ¶
func (h *NetworkHost) GetClientRole(playerID uint16) (role uint8, ok bool)
GetClientRole devuelve el rol opaco con el que se admitió a un jugador (ver AdmitPlayer). ok=false si el jugador no existe (ya desconectado, o ID inválido).
func (*NetworkHost) HandlePacket ¶
func (h *NetworkHost) HandlePacket(data []byte, peer Peer)
HandlePacket procesa un paquete crudo recibido de un Peer que YA pertenece a esta sala. El handshake no pasa por acá: es Server quien lo intercepta primero para decidir a qué sala corresponde (crear una nueva o unirse a una existente) y recién ahí llama a AdmitPlayer — ver server.go.
func (*NetworkHost) PendingReliableCount ¶
func (h *NetworkHost) PendingReliableCount(playerID uint16) int
PendingReliableCount: cuántos paquetes reliable (ver QueueReliableEvent) todavía no fueron confirmados por este cliente. Diagnóstico genérico — útil para cualquier juego que quiera saber si un cliente se está quedando atrás, no es específico de ningún evento en particular.
func (*NetworkHost) QueueEvent ¶
func (h *NetworkHost) QueueEvent(evt GameEvent)
QueueEvent: el juego encola sus propios eventos (ej. GOAL) para que salgan en el próximo snapshot. Entrega "mejor esfuerzo": si el paquete se pierde, el evento no vuelve a mandarse (a diferencia del snapshot en general, no hay una versión "más nueva" de un evento puntual). En LAN casi no importa; en una red con pérdida real (internet) un evento que cambia el marcador puede perderse en silencio — para eso ver QueueReliableEvent.
func (*NetworkHost) QueueReliableEvent ¶
func (h *NetworkHost) QueueReliableEvent(evt GameEvent)
QueueReliableEvent hace lo mismo que QueueEvent (el evento sale en el próximo snapshot de todos modos) pero además lo manda ya mismo, a cada cliente conectado, como un paquete aparte marcado FlagReliable — el core lo reintenta (mismo mecanismo que ya existía para reliable input, ver sendLoop) hasta que el cliente lo confirma con PacketAck o se agotan los reintentos. Pensado para eventos que el juego no puede permitirse perder en una red con pérdida real (ej. GOAL en un server público).
La entrega es "al menos una vez", no "exactamente una vez": el cliente puede recibir el mismo evento acá y de nuevo en el snapshot normal — el juego debe tratarlo como una notificación idempotente (ej. "hubo un gol, resincronizá con StatePayload"), no como algo que suma un contador él mismo.
func (*NetworkHost) Stop ¶
func (h *NetworkHost) Stop()
Stop detiene los loops de fondo de esta sala (tick, retransmisión, heartbeat). No toca ningún transporte — eso es responsabilidad de Server, que es quien los posee (ver server.go).
type Peer ¶
Peer es la identidad de un cliente a nivel transporte — un socket UDP crudo (ver transport_udp.go) o una sesión WebTransport (ver transport_webtransport.go). El core no distingue entre ambos: cualquier cosa que pueda mandar bytes y tenga una clave estable sirve. Esto es lo que permite que clientes Unity (UDP) y clientes de navegador (WebTransport) jueguen en la misma partida, contra el mismo NetworkHost.
IP(): el protocolo no lleva ningún token de sesión, así que AdmitPlayer usa la IP (sin el puerto, que cambia en cada reconexión — nuevo socket UDP o nueva sesión QUIC) como heurística para reconocer "es el mismo dispositivo reconectándose" — ver el comentario grande ahí.
type PlayerInput ¶
type PlayerInput struct {
PlayerID uint16
Seq uint32
Timestamp int64
DeltaX int16
DeltaY int16
Rotation uint16
Actions uint32
}
PlayerInput: forma genérica (movimiento 2D + rotación + bitmask de acciones) — el core no interpreta estos valores, solo los transporta y se los pasa al juego vía NetworkHost.OnInput.
type RoomFactory ¶
type RoomFactory func() *NetworkHost
RoomFactory crea un NetworkHost nuevo, con sus propios hooks, cada vez que se crea una sala — cada juego decide qué enganchar (ver ejemplo-tacataca), el Server no sabe nada de eso.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server multiplexa muchas salas (partidas) concurrentes sobre el mismo transporte compartido — genérico, no sabe qué juego corre en cada una. Un cliente que manda PacketHandshake con HandshakeModeCreate arranca una sala nueva y recibe un código; con HandshakeModeJoin se une a la sala de ese código. Todo lo demás (input, ack, ping) se rutea a la sala del cliente que lo mandó.
func NewServer ¶
func NewServer(factory RoomFactory, opts ServerOptions) *Server
NewServer crea un Server sin arrancar ningún transporte todavía — llamar StartUDP y/o StartWebTransport después.
func (*Server) CreateRoom ¶ added in v0.2.0
CreateRoom arranca una sala directamente, sin pasar por un handshake de red — a diferencia de una sala creada porque un cliente mandó HandshakeModeCreate, acá no hay ningún cliente admitido todavía.
Pensado para el modo "host embebido" (ej. un tablero en LAN, que es el mismo proceso que corre el Server): la app llama a esto una vez al arrancar y ya tiene una sala fija a la que los mandos se unen con JoinRoom, sin que el propio tablero tenga que hacerse pasar por cliente de sí mismo para "crear" la sala. La sala recién creada NO cuenta como "vacía" para el janitor (ver sweepEmptyRooms/hadPlayer) hasta que admite a su primer cliente de verdad, así que puede esperar indefinidamente a que alguien se una sin que EmptyRoomGracePeriod la destruya.
func (*Server) CreateRoomWithCode ¶ added in v0.3.0
CreateRoomWithCode es CreateRoom pero con un código elegido por el caller en vez de uno random — útil para el caso "host embebido, una sola sala por proceso" (ver el comentario grande de CreateRoom): sin esto, cada arranque tiene un código distinto y cualquier conexión manual (tipeando IP/puerto en vez de escanear un QR) necesita que el operador lea y transcriba ese código cada vez. Con un código fijo conocido de antemano, la conexión manual puede pedir solo IP y puerto. code no puede superar 255 bytes (RoomCodeLen es un solo byte en el wire format, ver EncodeJoinPayload/encodeHandshakeAck en protocol.go).
func (*Server) HandlePacket ¶
HandlePacket es el punto de entrada común para cualquier transporte — igual que antes lo era NetworkHost.HandlePacket, pero a nivel Server, porque el Server es quien sabe a qué sala pertenece cada paquete.
func (*Server) StartUDP ¶
StartUDP liga un socket UDP y arranca su loop de recepción. Se puede combinar con StartWebTransport en el mismo Server — ambos transportes alimentan el mismo Server.HandlePacket, así que un cliente UDP (Unity) y un cliente WebTransport (navegador) pueden terminar en la misma sala.
port=0 le pide al SO cualquier puerto disponible — útil para un deployment que no quiere depender de que un puerto fijo esté libre. El puerto real que asignó el SO queda disponible en UDPPort().
func (*Server) StartWebTransport ¶
func (s *Server) StartWebTransport(opts WebTransportOptions) (*DevCertificate, error)
StartWebTransport arranca un listener WebTransport (QUIC/HTTP-3) además de (o en vez de) StartUDP — se pueden usar los dos a la vez en el mismo Server: un cliente Unity (UDP) y un cliente de navegador (WebTransport) pueden terminar jugando la misma partida, porque ambos transportes alimentan el mismo Server.HandlePacket.
Si opts.TLSConfig es nil, genera y devuelve un certificado de desarrollo (ver GenerateDevCertificate) — el llamador necesita su HashBase64 para configurar el cliente browser.
func (*Server) Stop ¶
func (s *Server) Stop()
Stop cierra todos los transportes activos y detiene todas las salas.
type ServerOptions ¶
type ServerOptions struct {
// RoomCodeLength: longitud de los códigos de sala generados. Default: 6.
RoomCodeLength int
// MaxPlayersPerRoom: tope de clientes admitidos por sala (0 = sin
// tope). El Server solo cuenta clientes — no sabe qué rol tiene cada
// uno, así que un límite que dependa del rol (ej. "2 barras + 1
// tablero" en taca-taca) es responsabilidad del juego, no de acá.
MaxPlayersPerRoom int
// HandshakeRateLimit: máximo de paquetes de handshake aceptados desde
// un mismo Peer (misma IP:puerto UDP, o misma sesión WebTransport) por
// HandshakeRateLimitWindow — el resto se descarta en silencio. Default:
// 10 intentos / 10s. Esto NO protege contra un atacante que rota de
// origen en cada intento (el core no ve la IP real, solo Peer.Key()) —
// sí frena un cliente roto reintentando en loop o un flood simple
// desde una única conexión.
HandshakeRateLimit int
HandshakeRateLimitWindow time.Duration
// EmptyRoomGracePeriod: cuánto se espera, desde que una sala que ya
// tuvo jugadores queda en 0 conectados, antes de destruirla — no
// instantáneo, para darle tiempo a AdmitPlayer de reconocer una
// reconexión (ver el comentario grande ahí) antes de que la sala misma
// deje de existir. Default: 30s.
EmptyRoomGracePeriod time.Duration
}
ServerOptions configura el Server. Todo acá es genérico — no hay nada específico de ningún juego.
type WebTransportOptions ¶
type WebTransportOptions struct {
// Addr, ej. ":9443". El navegador se conecta a wss/https en ese puerto.
Addr string
// Path del endpoint WebTransport, ej. "/webtransport". Default: "/webtransport".
Path string
// TLSConfig propio (para producción, con un certificado real de una
// CA — ver LoadTLSCertificate). Si es nil, se genera un certificado
// self-signed de desarrollo automáticamente (ver GenerateDevCertificate)
// — NO usar eso en producción.
TLSConfig *tls.Config
// AllowedOrigins: orígenes (ej. "https://miapp.com") desde los que se
// acepta la conexión WebTransport. Vacío (default) acepta cualquier
// origen — cómodo para desarrollo, pero en producción cualquier página
// podría abrir una sesión contra este server; hay que restringirlo al
// dominio real donde vive el cliente.
AllowedOrigins []string
}
WebTransportOptions configura el listener WebTransport.