优先用 gorilla/websocket 而非 net/http 或裸 net 包:它封装 WebSocket 握手与帧处理,避免手动解析 Sec-WebSocket-Key;广播需为每个连接配独立写 goroutine 和带缓冲 channel(建议 16–64),防止单连接阻塞全局消息。

用 net/http 还是 net 包直接写 TCP 服务?
聊天服务器的核心是实时双向通信,HTTP 协议天然不支持服务端主动推消息(除非用长轮询或 SSE,但延迟和连接开销大),所以别用 net/http 搭骨架。直接上 net 包监听 TCP,或者更稳妥地选 gorilla/websocket——它封装了 WebSocket 握手、帧解析和并发安全的读写,比裸写 TCP 少踩 80% 的坑。
常见错误现象:read: connection reset by peer 频繁出现,往往是没处理好连接生命周期,比如 goroutine 泄漏或未关闭底层 conn;用 http.ServeMux 路由 WebSocket 升级请求却忘了调用 Upgrade 方法,结果返回 200 HTML 页面而不是 101 切换协议。
- WebSocket 场景优先用
gorilla/websocket,别自己解析Sec-WebSocket-Key - 纯内网低延迟场景(如游戏内聊)可考虑
net+ 自定义二进制协议,但得自己实现心跳、粘包、断线重连 -
net/http.Server的ReadTimeout/WriteTimeout对 WebSocket 无效,必须用websocket.Upgrader的CheckOrigin和连接级别的SetReadDeadline
怎么安全地广播消息给所有在线用户?
“遍历 map[*websocket.Conn]struct{} 然后 WriteMessage” 是最常见写法,也是最危险的——任意一个连接卡住(比如客户端网络抖动),整个广播就阻塞,其他用户消息全积压。
正确做法是给每个连接配独立的写 goroutine + 带缓冲的 channel。连接结构体里放 send chan []byte,主循环从 channel 读数据并 WriteMessage;广播时只往所有 channel 发送,不等写完成。
立即学习“go语言免费学习笔记(深入)”;
- channel 缓冲区大小建议设为 16–64,太小容易丢消息,太大内存占用不可控
- 必须在
defer或close前关掉 send channel,否则 goroutine 永远阻塞在ch - 不要用全局锁保护用户 map,改用
sync.Map,否则高并发下Range和LoadOrStore争抢严重
websocket.Upgrader 的 CheckOrigin 必须自定义吗?
默认情况下,Upgrader.CheckOrigin 返回 false(拒绝所有跨域请求),本地开发时浏览器直接报 Unexpected response code: 403。不改它,前端连握手都过不去。
线上环境不能简单返回 true,那等于开放任意域名劫持。实际只需校验 r.Header.Get("Origin") 是否在白名单里,比如允许 https://admin.example.com 和 http://localhost:3000。
- 开发阶段可临时设为
func(r *http.Request) bool { return true },但提交前务必删掉 - 如果用 Nginx 反向代理,注意它可能不透传
Origin头,需加proxy_set_header Origin $scheme://$host; -
CheckOrigin函数里别做耗时操作(如查 DB),它在每次握手时同步执行,会拖慢连接建立
为什么客户端收不到消息,但服务端没报错?
最常被忽略的是:WebSocket 连接空闲时,NAT 网关或中间代理会静默断开 TCP 连接,而 Go 代码感知不到——ReadMessage 不报错,只是永远阻塞;WriteMessage 也成功返回,其实数据发到了已关闭的 socket 上。
解决办法只有两个:服务端定期发 ping 帧(conn.SetPingHandler + conn.WriteMessage(websocket.PingMessage, nil)),客户端必须响应 pong;同时设好读写超时,超时后主动 Close 连接。
-
conn.SetReadDeadline(time.Now().Add(30 * time.Second))必须在每次ReadMessage前调用,不是只设一次 - ping 间隔建议 25 秒,比超时短 5 秒,留出网络抖动余量
- 别依赖
http.ErrAbortHandler或io.EOF判断断连——它们只在连接真正关闭时触发,中间断连完全静默
复杂点在于:ping/pong 本身不携带业务数据,你得在应用层再设计一套心跳确认机制,否则无法区分“连接活着但客户端卡死”和“连接真断了”。这个边界,得靠客户端上报状态+服务端超时清理双保险。


















