← Back to all tools

go-websocket

A WebSocket chat service that delivers only to the receiver, never broadcasts

  • Language: Go 1.23+
  • Platforms: Source only (Go 1.23+, runs on Windows / Linux / macOS)

Stack

  • Go
  • Gin
  • gorilla/websocket
  • 内嵌静态资源
  • Bootstrap 5

Overview

go-websocket is a targeted WebSocket chat service written in Go + Gin: a browser connects with a uid, and the server delivers each message only to the user named in receiver — it never broadcasts. The page template, the static assets and the config templates are all embedded into the binary with //go:embed, so go run . right after cloning is enough — nothing external is needed at runtime.

The point is to be a reading-and-tinkering codebase: the layering, the concurrency model, the config loading and the tests are all small enough to read top to bottom and change directly, with no extra build or runtime dependencies.

Features

  • Targeted delivery: a message goes only to the uid in receiver, never broadcast; if the receiver is offline the sender gets an explicit error back
  • Unforgeable identity: sender is filled in by the server from the TCP connection — whatever the client sends is ignored
  • uid validation up front: letters, digits, underscores and hyphens only, 1–64 characters. An invalid uid is rejected with 400 plus a JSON reason before the upgrade handshake
  • Concurrency-safe: the hub keeps the uid → connection map inside a single goroutine (no mutex on the map), and every write to a connection is funnelled into that connection's one and only write loop
  • Heartbeat and timeouts: the server pings on a fixed period and drops the connection when no pong (or no read) arrives, so dead connections do not pile up
  • Slow-client protection: every connection has its own outbound queue; once it fills up the peer is considered gone and gets dropped, so one slow client can neither stall the hub nor starve other recipients
  • Duplicate uid takeover + graceful shutdown: reconnecting with the same uid replaces the old connection; Ctrl+C / SIGTERM stops accepting connections first, then closes every WebSocket
  • Frontend niceties: light / follow-system / dark themes (applied on the first frame), exponential-backoff auto-reconnect, click an online user to pick the receiver, and the uid is stored locally

Quick Start

Requires Go 1.23 or later.

# Run directly (recommended: rerun after every edit, no files left behind)
go run .

# Or build first
go build -o go-websocket . && ./go-websocket

On startup it prints the version, the config source and the accessible URLs:

go-websocket dev (release)
Config source: embedded env.ini.release
Local access: http://127.0.0.1:8090
LAN access: http://192.168.1.10:8090
WebSocket shares the port above, path /ws?uid=<your-uid>

Open two browser windows (or one normal window plus one private window), note each one's uid, type the other's uid, and start chatting — or simply click the other user under "Online users".

Configuration

The config file is INI format. Sources, highest priority first:

  1. the file given by -config / the GWS_CONFIG environment variable
  2. env.ini in the working directory
  3. config/env.ini in the working directory
  4. the embedded env.ini.<GWS_ENV> (GWS_ENV defaults to release)
Key Default Description
env_mode release debug / release / test; anything else is treated as release
server.http_port 8090 Listening port
server.shutdown_timeout 10s Longest time to wait for a graceful shutdown
websocket.max_message_size 4096 Maximum size of a single message (bytes); larger ones are rejected
websocket.ping_period 30s How often the server sends a ping; 0 disables the heartbeat
websocket.pong_wait 60s How long to wait for a pong (also used as the read deadline)
websocket.send_queue 64 Per-connection outbound queue length; a full queue drops the connection
websocket.allow_all_origins false Allow connections from any origin; by default only same-origin is allowed

The pages and assets under web/ are compiled into the binary by //go:embed. After editing them you must rerun go run . or restart the process — reloading the browser alone will not pick up the change.

API

Method and path Parameters Description
GET / - The chat page
GET /ws uid (required) Upgrade to a WebSocket connection; an invalid uid returns 400
GET /api/online - Online users, {"count":N,"users":["alice","bob"]}

WebSocket messages are JSON. Upstream, only these two fields matter — sender is overwritten by the server:

{ "receiver": "bob", "content": "你好" }

Downstream (server → receiver):

{ "type": "chat", "sender": "alice", "receiver": "bob", "content": "你好", "time": "2026-10-01 13:43:43" }

A notice sent to the client that caused it (receiver offline, malformed message, …):

{ "type": "error", "content": "接收人 nobody 不在线", "time": "2026-10-01 13:43:43" }

Implementation Notes

  • One goroutine owns the hub map: Hub.clients is only ever mutated inside run(). Everything else goes through channels (register / unregister / deliver / online), so the map needs no lock
  • One writer per connection: gorilla/websocket does not support concurrent writes to the same connection. Only writePump writes to the socket, and every other goroutine just posts to the queue
  • No "close the channel" signalling: once send is closed, any further post panics — and the poster may be the hub or the read loop, so the ordering cannot be guaranteed. sync.Once + close(done) is used instead, which can never panic a poster
  • Slow clients are dropped outright: a full send queue means the peer has stopped reading, and piling messages up would only hurt everyone else. The connection is dropped and the sender is told why — a deliberate trade-off
  • uid is validated before Upgrade: the failure is a 400 plus a JSON reason. Reporting an error after the upgrade would only leave a close frame, which tells the frontend nothing
  • sender is overwritten by the server: it is taken from the connection identity only; a client-supplied sender is discarded so nobody can impersonate someone else
  • Read limit: SetReadLimit caps the size of a single message — otherwise one client could eat all the memory with an oversized frame
  • Heartbeat via gorilla's ping/pong: the server pings periodically and refreshes the read deadline in SetPongHandler, so a dead connection is cleaned up within pong_wait at worst
  • Only the read header timeout is set on HTTP: WebSockets are long-lived, so a ReadTimeout / WriteTimeout would cut normal sessions and heartbeats alike
  • Graceful shutdown takes two steps: srv.Shutdown stops accepting new connections first, then h.Close() disconnects the WebSockets still attached — upgraded connections are outside http.Server's bookkeeping
  • The frontend never builds HTML strings: all user-controlled text is written into the DOM through jQuery's .text(). Both the uid and the message body are fully controlled by the peer, and pasting them into innerHTML is a ready-made XSS

Known Limitations

  1. No authentication — anyone who knows a uid can connect, and uids are not reserved (a second connection with the same uid simply takes over). Fine on localhost or a LAN
  2. No persistence — messages are only forwarded; nothing is replayed when you come back online, and no history is stored
  3. No HTTPS / WSS — real TLS has to be terminated by a reverse proxy such as Nginx or Caddy
  4. Single process — the hub lives in memory, so instances cannot see each other's uids. Horizontal scaling would need cross-process delivery such as Redis
  5. No rooms or broadcast — point-to-point only: no groups, no chat rooms, no offline messages
  6. The online list is polled — the frontend fetches /api/online every 5 seconds rather than being notified
  7. The frontend is still jQuery plus a server-side template; there is no frontend/backend split and no asset build pipeline

Links

Source: GitHub

Last updated