context 不能直接控制 websocket 连接的建立或关闭,但它是管理连接生命周期、读写超时、取消信号传递的唯一可靠方式。不结合 context 的 websocket 服务,在高并发或网络波动下极易出现 goroutine 泄漏、连接假死、资源堆积。
为什么 conn.SetReadDeadline 不够用
单纯靠 conn.SetReadDeadline 只能解决单次读操作超时,但无法应对以下场景:
- 整个连接会话需要统一截止时间(比如用户登录后只允许活跃 30 分钟)
- 上游请求已取消(如客户端关页、API 调用被 cancel),但读协程还在等消息
- 多个 goroutine 协同工作(读、写、心跳、业务处理),需同步退出信号
此时 context.Context 是唯一能跨 goroutine 广播“该停了”的机制。必须把 context 传进每个协程,并在关键阻塞点(如 conn.ReadMessage、writeChan )配合 <code>select 监听 ctx.Done()。
如何为每个连接绑定独立 context
不要用全局或 background context。应在握手成功后,立刻创建带取消和超时的子 context:
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
<pre class="brush:php;toolbar:false;">// 每个连接一个独立 context:30 分钟总存活 + 10 秒读写 deadline
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
// 启动读协程时传入 ctx
go readPump(ctx, conn)
// 启动写协程时也传入 ctx
go writePump(ctx, conn)
// 等待任一协程退出,或 context 超时
select {
case <p>}
</p>注意:context.WithTimeout 返回的 cancel 必须调用,否则 timer 不释放,长期运行会内存泄露。
读协程中如何安全响应 cancel 信号
不能只依赖 conn.SetReadDeadline,必须在每次 ReadMessage 前检查 ctx.Err(),并在阻塞等待时用 select:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
func readPump(ctx context.Context, conn *websocket.Conn) {
defer conn.Close()
<pre class="brush:php;toolbar:false;">for {
// 每次读前先检查是否已被取消
select {
case <p>}
</p>常见错误是把 ctx.Done() 检查放在循环末尾——万一 ReadMessage 阻塞住,就再也收不到取消信号了。
写协程必须避免 writeChan 积压导致 ctx 失效
写协程通常从 writeChan chan []byte 消费消息。如果业务端持续发消息,而网络慢或客户端断连,writeChan 会越积越多,ctx 就失去意义:
- 不能无缓冲地用
chan []byte,必须设容量(如make(chan []byte, 64)) - 向
writeChan发送时,必须用select+default或超时,防止 goroutine 卡死 - 一旦
ctx.Done()触发,应立即关闭writeChan并清空剩余消息
最易忽略的一点:conn.WriteMessage 本身可能阻塞(尤其在 TLS 下),所以写操作也得包一层 select,而不是直接调用。










