本文详解 go 中使用 x/net/websocket 时无法多次读写同一连接的根本原因——连接被 handler 自动关闭,并提供正确复用连接的实践方案。
本文详解 go 中使用 x/net/websocket 时无法多次读写同一连接的根本原因——连接被 handler 自动关闭,并提供正确复用连接的实践方案。
在 Go 中通过 x/net/websocket 实现 WebSocket 服务时,一个常见误区是:在 HTTP handler 中启动 goroutine 处理后续通信,而主线程立即返回。这会导致底层 WebSocket 连接被自动关闭——因为 x/net/websocket.Server.ServeHTTP 在 handler 函数返回后会调用 conn.Close(),无论连接是否仍在被其他 goroutine 使用。
你遇到的问题正源于此:
- 第一次 enc.Encode(ac) 成功发送,是因为它发生在 handler 主线程中,此时连接仍处于活跃状态;
- 而 go g.LivingDiscussions[...].Listen() 启动的 goroutine 在 handler 返回后才开始执行 dec.Decode(&msg),此时连接已被关闭,后续读写均失效(表现为第二次 Encode 不被服务端接收)。
✅ 正确做法:所有读写逻辑必须在 handler 函数内同步完成,禁止将长连接处理逻辑移至独立 goroutine 后直接返回。
以下是修正后的典型结构(以创建讨论并持续监听消息为例):
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
func Create(ws *websocket.Conn) {
Log.Connection(ws)
// 1. 接收创建请求
var dc g.DiscussionCreate
if err := json.NewDecoder(ws).Decode(&dc); err != nil {
Log.Err(err, "failed to decode DiscussionCreate")
return
}
// 2. 构建并返回讨论信息
discID := g.DiscussionID{
Subject: dc.Subject,
UDID: g.GenerateUID(time.Now().String()),
}
ssUserID := g.SSUserID{
Name: dc.Creator.Name,
UUID: dc.Creator.UUID,
WS: ws,
}
disc := g.SSDiscussion{
DiscussionID: discID,
Participants: []g.SSUserID{ssUserID},
}
g.LivingDiscussions = append(g.LivingDiscussions, disc)
di := g.Discussion{
DiscussionID: discID,
Participants: []g.UserID{dc.Creator},
}
if err := json.NewEncoder(ws).Encode(di); err != nil {
Log.Err(err, "failed to encode Discussion")
return
}
// ✅ 关键修正:不再 go ssUserID.Listen()
// 而是在当前 handler 中持续处理读写(即“阻塞式长连接”)
ssUserID.Listen() // 同步调用,handler 不返回,连接保持打开
}
对应地,Listen 方法需改为阻塞式循环,且显式检查连接状态:
func (ssUserID *SSUserID) Listen() {
dec := json.NewDecoder(ssUserID.WS)
enc := json.NewEncoder(ssUserID.WS)
var msg g.Message
for {
// 检查连接是否已断开(如客户端关闭、网络中断)
if err := ssUserID.WS.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
Log.Err(err, "SetReadDeadline failed")
break
}
if err := dec.Decode(&msg); err != nil {
if websocket.IsCloseError(err, websocket.CloseAbnormalClosure) ||
websocket.IsCloseError(err, websocket.CloseGoingAway) {
Log.Info("Client disconnected gracefully")
break
}
Log.Warn("Decode error, skipping:", err)
continue
}
// 处理业务逻辑(如广播、存储等)
Messages <p>⚠️ 注意事项:</p>
- x/net/websocket 已归档弃用(自 Go 1.19 起),官方推荐迁移到更现代、维护活跃的库,如 gorilla/websocket 或 nhooyr.io/websocket;
- 若坚持使用 x/net/websocket,务必确保 handler 永不返回,直到连接真正终止;
- 避免在多个 goroutine 中并发读写同一 *websocket.Conn —— 它不是并发安全的,应由单个 goroutine 顺序处理;
- 始终设置读/写超时(SetReadDeadline/SetWriteDeadline),防止 goroutine 泄漏。
总结:WebSocket 连接的生命周期由 handler 控制;只要 handler 在运行,连接就有效。将通信逻辑放入 goroutine 后立即返回 handler,等于主动“杀死”连接。真正的长连接,始于 handler 的不返回,成于单 goroutine 的有序读写。










