← 返回工具列表

go-websocket

按 receiver 精准投递的 WebSocket 聊天服务,不广播

  • 语言: Go 1.23+
  • 平台: 源码交付(Go 1.23+,Windows / Linux / macOS 均可运行)

技术栈

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

概览

go-websocket 是一个用 Go + Gin 写的 WebSocket 定向聊天服务:浏览器带上 uid 连上来,服务端按消息里的 receiver 精准投递给指定用户,而不是广播。页面模板、静态资源与配置模板全部通过 //go:embed 内嵌在二进制里,clone 下来 go run . 就能跑,运行时不依赖任何外部文件。

定位是源码实践 —— 代码分层、并发模型、配置加载与测试都控制在可以直接读、直接改的规模,不引入额外的构建或运行时依赖。

功能

  • 定向投递:按 receiver 只发给指定 uid,不广播;接收人不在线会给发送方回一条明确的 error 回执
  • 身份不可伪造:sender 由服务端按 TCP 连接填写,客户端传什么都不作数
  • uid 校验前置:只允许字母、数字、下划线、连字符,长度 1~64;非法 uid 在升级握手之前就返回 400 + JSON 原因
  • 并发安全:连接中心在单个 goroutine 里维护 uid → 连接 映射(map 无锁),每个连接的写操作全部收敛到它唯一的写循环
  • 心跳与超时:服务端按周期发 ping,等不到 pong(或读超时)就断开,死连接不会堆积
  • 慢客户端保护:每个连接一个待发送队列,队列满即判定「对端不读了」并断开,慢连接拖不住 Hub
  • 重复 uid 顶号 + 优雅退出:同一个 uid 再次连接时替换旧连接;Ctrl+C / SIGTERM 先停止监听,再统一断开全部 WebSocket 后退出
  • 前端体验:亮色 / 跟随系统 / 暗色三态主题(首帧即应用)、断线指数退避自动重连、点击在线用户即可选中接收人、uid 本地持久化

快速开始

需要 Go 1.23 及以上。

# 直接跑(推荐,改完代码重跑即可,不产生文件)
go run .

# 或者编译后运行
go build -o go-websocket . && ./go-websocket

启动后会打印版本、配置来源与可访问地址:

go-websocket dev(release)
配置来源: 内嵌 env.ini.release
本机访问: http://127.0.0.1:8090
局域网访问: http://192.168.1.10:8090
WebSocket 与上述地址同端口,路径 /ws?uid=你的uid

开两个浏览器窗口(或一个正常窗口 + 一个隐私窗口),各拿到一个 uid,互相填写对方 uid 就能发消息;也可以直接在左侧「在线用户」里点对方。

配置

配置文件是 INI 格式,来源优先级从高到低:

  1. -config 参数 / GWS_CONFIG 环境变量指定的文件
  2. 工作目录下的 env.ini
  3. 工作目录下的 config/env.ini
  4. 二进制内嵌的 env.ini.<GWS_ENV>(默认 release)
配置项 默认值 说明
env_mode release debug / release / test,非法取值按 release 处理
server.http_port 8090 监听端口
server.shutdown_timeout 10s 优雅退出的最长等待时间
websocket.max_message_size 4096 单条消息的最大字节数,超过会被拒绝
websocket.ping_period 30s 服务端发 ping 的间隔;0 表示关闭心跳
websocket.pong_wait 60s 等待 pong 的超时(同时作为读超时)
websocket.send_queue 64 每个连接的待发送队列长度,队列满即断开
websocket.allow_all_origins false 是否允许任意来源连接;默认只允许同源

web/ 下的页面与静态资源是 //go:embed 打进二进制的,改完必须重新 go run . 或重启进程,只刷新浏览器不会生效。

接口

方法与路径 参数 说明
GET / - 聊天页面
GET /ws uid(必填) 升级为 WebSocket 连接;uid 非法返回 400
GET /api/online - 在线用户列表,{"count":N,"users":["alice","bob"]}

WebSocket 报文是 JSON。上行只有两个字段有意义,sender 由服务端覆盖:

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

下行(服务端 → 接收方):

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

服务端给触发者的提示(接收人离线、报文非法等):

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

实现要点

  • 连接中心单 goroutine 管 map:Hub.clients 只在 run() 这一个 goroutine 里改动,外部一律通过 channel(register / unregister / deliver / online)投递,因此 map 不需要加锁
  • 连接只有一个写方:gorilla/websocket 不支持并发写同一个连接,每个连接只有 writePump 会写,其他 goroutine 只投递
  • 不用「关闭 channel」通知退出:send 关闭后任何一次投递都会 panic,而投递方可能来自 Hub 也可能来自读循环,时序无法保证;改用 sync.Once + close(done),最多白投一次
  • 慢客户端直接断开:send 队列满说明对端已经不读了,继续堆消息只会连累别人,判定后立即断开并回告发送方
  • uid 校验前置:校验放在 Upgrade 之前,用 400 + JSON 说明原因;升级之后再报错就只能靠关闭帧,前端拿不到任何信息
  • sender 服务端覆盖:只信连接身份,客户端传的 sender 一律丢弃,避免冒充
  • 读限制:SetReadLimit 限制单条消息大小,否则一个客户端就能用超大帧把内存吃光
  • 心跳走 gorilla 的 ping/pong:SetPongHandler 里续读截止时间,死连接最多 pong_wait 就被清理
  • HTTP 超时只限制读请求头:WebSocket 是长连接,设了 ReadTimeout / WriteTimeout 会把正常会话和心跳一起掐断
  • 优雅退出分成两步:先 srv.Shutdown 停止接收新连接,再 h.Close() 断开还挂着的 WebSocket —— 升级过的连接不在 http.Server 的管理范围里
  • 前端不拼 HTML:所有用户可控文本都通过 jQuery 的 .text() 写进 DOM,uid 与消息内容完全由对端控制,拼进 innerHTML 就是现成的 XSS

已知局限

  1. 没有鉴权 —— 任何人只要知道 uid 就能连上来,uid 也不做占用校验(同 uid 直接顶号),默认只适合本机或内网
  2. 没有持久化 —— 消息只做转发,离线不补发、历史不落库
  3. 没有 HTTPS / WSS —— 真正的 TLS 需要由前置 Nginx / Caddy 之类的反向代理终结
  4. 单进程 —— 连接中心在内存里,多实例部署时不同进程的 uid 互相看不见,横向扩展得引入 Redis 之类的跨进程投递
  5. 没有分组 / 广播 —— 只支持点对点,没有房间、群聊与离线消息
  6. 在线列表是轮询的 —— 前端每 5 秒拉一次 /api/online,没有做成上行/下行通知
  7. 前端仍是 jQuery + 服务端模板,没有做前后端分离与构建链路

链接

源码:GitHub

最近更新