直接用 net/http 搭配 gorilla/websocket 是最稳妥的选择,因其稳定、维护活跃、自动处理握手/帧解析/心跳/关闭等细节,且避免了手动实现易出错和过时库的风险。

为什么直接用 net/http 搭配 gorilla/websocket 是最稳妥的选择
Go 标准库没有原生 WebSocket 实现,net/http 只能处理升级请求(Upgrade),真正握手、帧解析、心跳、连接管理都得靠第三方库。目前生产环境几乎统一用 gorilla/websocket,它稳定、文档清晰、维护活跃,且不依赖其他重型框架。
别碰 gobwas/ws 或自己基于 net/http 手搓——前者已归档,后者极易在掩码、分片、关闭码、ping/pong 超时等细节上出错。你不是在写 RFC 实现,是在做业务通信。
-
gorilla/websocket的Upgrader自动处理Sec-WebSocket-Key验证和 101 切换,不用手动写 header - 所有读写方法(
WriteMessage、ReadMessage)默认带缓冲和错误重试逻辑,比裸conn.Read/Write安全得多 - 连接关闭时会自动发送
CloseMessage帧,避免客户端卡在 “closing” 状态
如何正确升级 HTTP 连接并避免 400 / 426 错误
常见错误是把 WebSocket 路由和普通 HTTP 路由混写,或没设置 Upgrader.CheckOrigin,导致浏览器发来的 Origin 头被拒绝,返回 400;或者服务端没响应 Upgrade: websocket,返回 426 Upgrade Required。
关键点在于:必须用 http.HandlerFunc 显式处理升级,不能塞进中间件链里(除非中间件明确支持 http.Hijacker)。
立即学习“go语言免费学习笔记(深入)”;
func wsHandler(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 生产请校验 Origin,例如 r.Header.Get("Origin") == "https://myapp.com"
},
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, "Upgrade error", http.StatusBadRequest)
return
}
defer conn.Close()
// 后续读写在此进行
for {
_, msg, err := conn.ReadMessage()
if err != nil {
break
}
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
break
}
}
}
-
CheckOrigin默认返回false,必须显式设为true或实现白名单逻辑,否则跨域请求必失败 -
Upgrade必须在 response body 写入前调用,任何fmt.Fprintf(w, ...)或json.NewEncoder(w).Encode(...)都会导致 panic 或 500 - 不要对
*http.ResponseWriter做任何其他操作(如设置Header().Set),升级后它就失效了
如何安全地并发读写并避免 websocket: write deadline exceeded
WebSocket 连接是全双工的,但 gorilla/websocket 的底层连接不允许多 goroutine 同时调用 WriteMessage —— 会 panic 报 concurrent write to connection。而读写超时(write deadline exceeded)通常是因为写操作阻塞太久,比如客户端断连但服务端还在发消息。
标准解法是:一个 goroutine 专责读(处理业务逻辑 + 关闭信号),另一个 goroutine 专责写(从 channel 拉消息,带超时控制)。
func handleConnection(conn *websocket.Conn) {
// 读协程
go func() {
defer conn.Close()
for {
_, msg, err := conn.ReadMessage()
if err != nil {
return
}
// 解析 msg,投递到业务 channel 或广播池
}
}()
// 写协程
writer := func() {
defer conn.Close()
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for {
select {
case message, ok := <-broadcastChan:
if !ok {
return
}
if err := conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
return
}
if err := conn.WriteMessage(websocket.TextMessage, message); err != nil {
return
}
case <-ticker.C:
if err := conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
return
}
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}()
go writer()
}
- 每个连接必须有自己的
writeWait(建议 10s)和pingPeriod(建议 30s),不能全局共用 - 写操作前务必调用
conn.SetWriteDeadline,否则网络卡住时会永久阻塞 goroutine - 不要用
conn.SetDeadline同时管读写——读写超时需求不同,混合设置反而容易误关连接
客户端断连时,服务端怎么知道并清理资源
WebSocket 没有 TCP 那样的 FIN 包保证,客户端静默掉线(比如关机、断网)时,服务端可能数分钟甚至更久才收到 EOF。不能只靠 ReadMessage 返回 error 就清理——那太慢,也漏掉“假在线”连接。
必须主动探测:服务端定期发 Ping,客户端必须回 Pong;若连续几次没收到 Pong,就关连接。
-
Upgrader提供EnableCompression: true和HandshakeTimeout,但这两个和断连检测无关,别混淆 -
conn.SetPongHandler必须在Upgrade后立刻注册,且 handler 内不要做耗时操作(比如查 DB),否则会阻塞整个连接的读循环 - 真实项目中,连接要绑定用户 ID 或 session,并注册到
map[string]*websocket.Conn中;关闭前记得delete,否则内存泄漏
最易忽略的是:Ping/Pong 是 WebSocket 协议层机制,不是业务消息,ReadMessage 永远不会读到 PongMessage —— 它被库自动处理了,你只需设 handler 更新最后活跃时间即可。










