feat: WebSocket upgrader, hub, send, broadcast, handler con tests (issue 0011 fase 3-4)
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
||||
// WSHandler retorna un http.HandlerFunc que upgradea la conexion HTTP a WebSocket,
|
||||
// crea un WSClient, lo registra en el hub y lanza dos goroutines:
|
||||
// - readPump: lee mensajes del Conn y los publica al hub.Broadcast
|
||||
// - writePump: consume del client.Send y escribe al Conn
|
||||
//
|
||||
// El cliente se desregistra del hub cuando alguna de las pumps termina (cliente
|
||||
// desconectado, error de I/O, o canal cerrado). Asigna un ID hex aleatorio si
|
||||
// no se sobreescribe externamente.
|
||||
func WSHandler(hub *WSHub, origins []string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := WSUpgrader(w, r, origins)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
client := &WSClient{
|
||||
Hub: hub,
|
||||
Conn: conn,
|
||||
Send: make(chan []byte, 64),
|
||||
ID: randomID(),
|
||||
}
|
||||
hub.Register <- client
|
||||
|
||||
// writePump
|
||||
go wsWritePump(client)
|
||||
// readPump (bloqueante en el handler para mantener viva la request)
|
||||
wsReadPump(client)
|
||||
}
|
||||
}
|
||||
|
||||
// wsReadPump lee mensajes del Conn y los publica al hub.Broadcast.
|
||||
// Termina si Read retorna error (cliente desconectado o cerrado).
|
||||
// Al terminar, desregistra el cliente y cierra la conexion.
|
||||
func wsReadPump(client *WSClient) {
|
||||
defer func() {
|
||||
// Unregister no bloqueante: si el hub ya esta cerrado, no esperamos
|
||||
select {
|
||||
case client.Hub.Unregister <- client:
|
||||
case <-client.Hub.done:
|
||||
}
|
||||
_ = client.Conn.Close(websocket.StatusNormalClosure, "")
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
for {
|
||||
_, data, err := client.Conn.Read(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Encolar al hub.Broadcast sin bloquear si esta lleno
|
||||
select {
|
||||
case client.Hub.Broadcast <- data:
|
||||
default:
|
||||
// Hub saturado: dropear mensaje del cliente para no bloquear el read
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wsWritePump consume del canal Send del cliente y escribe al Conn.
|
||||
// Termina si el canal se cierra (hub desregistro al cliente) o si Write falla.
|
||||
func wsWritePump(client *WSClient) {
|
||||
for msg := range client.Send {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
err := client.Conn.Write(ctx, websocket.MessageText, msg)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// randomID genera un identificador hex aleatorio de 16 caracteres (8 bytes).
|
||||
// No es criptograficamente perfecto para autenticacion — solo identificacion.
|
||||
func randomID() string {
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "anon"
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
Reference in New Issue
Block a user