gorilla/websocket需用Upgrader升级HTTP连接,禁用默认跨域限制;读写须单goroutine,加锁管理连接池;需心跳保活、设读写超时、Nginx反向代理配置适配。

用 gorilla/websocket 实现基础连接与消息收发
Go 官方标准库不提供 WebSocket 支持,gorilla/websocket 是最成熟、被广泛采用的第三方实现。它轻量、稳定,且对并发连接和错误处理有良好抽象。
关键点在于:服务端需显式升级 HTTP 连接,不能直接用 http.HandleFunc 返回普通响应;客户端发起的 ws:// 请求必须被 websocket.Upgrader 拦截并转换为长连接。
-
Upgrader.CheckOrigin默认拒绝所有跨域请求,开发时需显式允许(如返回true),上线务必按需校验r.Header.Get("Origin") - 每个连接应启动独立 goroutine 处理读写,避免阻塞其他连接;但注意:
conn.ReadMessage()和conn.WriteMessage()都是非线程安全的,不能在多个 goroutine 中并发调用同一连接 - 使用
conn.SetReadDeadline()防止客户端假死占用资源;超时后ReadMessage()会返回*net.OpError,需主动关闭连接
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, "Upgrade error", http.StatusUpgradeRequired)
return
}
defer conn.Close()
for {
_, msg, err := conn.ReadMessage()
if err != nil {
if !websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
return
}
break
}
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
break
}
}
}
如何安全地广播消息给所有在线客户端
没有内置“广播”机制,必须自己维护连接集合。常见错误是直接用 map[*websocket.Conn]bool 并发读写——这会导致 panic。必须加锁,且锁粒度要细:读写 map 本身需互斥,但对单个 conn.WriteMessage() 的调用应尽量不持锁。
- 推荐用
sync.Map存储活跃连接,键可设为自增 ID 或 session token,避免用*websocket.Conn作 key(指针不稳定) - 写消息前检查连接状态:
conn.WriteMessage()可能因网络中断立即失败,需捕获websocket.IsCloseError(err, ...)和io.ErrClosedPipe等错误,并从集合中移除该连接 - 不要在广播循环中调用
conn.SetWriteDeadline()后再写——如果某个连接慢,会拖累整个广播。应为每个连接单独设置 deadline 并忽略其超时错误
连接生命周期管理:断开、重连与心跳保活
浏览器或移动端网络不稳定,onclose 不一定触发,服务端无法感知断连。单纯依赖 TCP keepalive(默认 2 小时)远远不够。
立即学习“go语言免费学习笔记(深入)”;
- 客户端应定期发送
websocket.PingMessage(如每 30 秒),服务端用conn.SetPingHandler()响应;若超时未收到 ping,视为失联 - 服务端也要主动发 ping:
conn.SetPongHandler()配合定时器,每 25 秒调用一次conn.WriteMessage(websocket.PingMessage, nil);注意:ping 必须在 write lock 内发送,避免与业务消息竞争 - 连接关闭时,务必从连接池中删除,并关闭对应 channel(如有用于通知的 channel);否则 goroutine 泄漏风险极高
生产环境必须面对的几个硬伤
gorilla/websocket 本身不处理集群间广播、连接数限制、TLS 卸载、反向代理兼容性等问题。这些不是“可选项”,而是上线前必须补上的环节。
- Nginx 默认缓冲 WebSocket 帧,需显式配置:
proxy_http_version 1.1、proxy_set_header Upgrade $http_upgrade、proxy_set_header Connection "upgrade" - 单机连接数受限于文件描述符(
ulimit -n),通常需调至 65535+;同时 Go 的GOMAXPROCS不宜设得过高,避免 goroutine 调度开销压垮 CPU - 如果需要多节点广播(如用户 A 发消息,B 在另一台机器上收到),必须引入 Redis Pub/Sub 或消息队列,不能只靠本地 map
真正难的从来不是“怎么连上”,而是“怎么在千人并发、弱网、滚动发布、节点故障时,依然让消息不丢、不断、不乱序”。这些细节藏在 SetReadDeadline 的时间设定里,藏在 sync.Map 的使用姿势里,也藏在 Nginx 那几行容易被复制粘贴错的配置里。










