Files
gomoku/server/network/reader.go
T
tiennm99 9e14f3a9b3 feat(server): new ws network layer and protobuf dispatch on :1999/gomoku
- Add server/network/{server,codec,reader,writer,dispatch,handlers_stateless}.go
- Single /gomoku WS endpoint; binary protobuf frames only (no JSON/base64)
- Per-connection reader + writer goroutines; write mutex serialises WS writes
- Dispatch type-switch: stateless (heartbeat, set_nickname, get_rooms,
  set_client_info, client_exit) inline; stateful requests → player.CmdCh
- Add Player.SendCh, CmdCh, LastHeartbeat, ClientVersion, Send() to database.Player
- Rewire main.go: NewServer(":1999").Serve() + database.StartCleanup()
- Empty wss.go (old shim superseded by server.go)
2026-04-11 13:56:22 +07:00

71 lines
2.0 KiB
Go

package network
import (
"time"
"github.com/gorilla/websocket"
"github.com/tiennm99/gomoku/server/database"
"github.com/tiennm99/gomoku/server/pkg/log"
)
const (
// maxMessageSize caps inbound WS frame size at 4 KB.
// Protobuf game messages are tiny; anything larger is malicious or buggy.
maxMessageSize = 4 * 1024
// readDeadline is the maximum time to wait for a frame before considering
// the client dead. Clients send heartbeats every ~50 s; 90 s gives headroom.
readDeadline = 90 * time.Second
)
// readLoop blocks until the connection closes or an unrecoverable read error occurs.
// It decodes each binary WS frame into a Request and dispatches it.
// On exit it closes the player's CmdCh and removes them from the store.
func readLoop(conn *websocket.Conn, player *database.Player) {
conn.SetReadLimit(maxMessageSize)
_ = conn.SetReadDeadline(time.Now().Add(readDeadline))
// Extend deadline on each ping/pong so the 90 s window is per-message, not absolute.
conn.SetPongHandler(func(string) error {
return conn.SetReadDeadline(time.Now().Add(readDeadline))
})
defer func() {
// Signal state machine and writer goroutine that this player is gone.
if player.CmdCh != nil {
close(player.CmdCh)
}
database.RemovePlayer(player.ID)
log.Infof("[reader] player %d disconnected\n", player.ID)
}()
for {
msgType, data, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err,
websocket.CloseGoingAway,
websocket.CloseNormalClosure,
websocket.CloseNoStatusReceived) {
log.Errorf("[reader] player %d read error: %v\n", player.ID, err)
}
return
}
if msgType != websocket.BinaryMessage {
log.Errorf("[reader] player %d: unexpected message type %d, dropping\n", player.ID, msgType)
continue
}
// Reset deadline on any incoming frame (heartbeats count).
_ = conn.SetReadDeadline(time.Now().Add(readDeadline))
req, err := DecodeRequest(data)
if err != nil {
log.Errorf("[reader] player %d decode error: %v\n", player.ID, err)
continue
}
Dispatch(player, req)
}
}