17370845950

Gorilla WebSocket 连接超时断开问题的完整解决方案

nginx 默认 `proxy_read_timeout` 为 60 秒,导致后端 websocket 连接在空闲约 1 分钟后被强制关闭;需在 nginx 配置中显式延长该超时值,并配合 gorilla 的心跳机制确保长连接稳定。

WebSocket 在生产环境中常因反向代理(如 Nginx)的默认超时策略而意外中断。你遇到的 websocket: close 1006 unexpected EOF 错误,并非客户端主动关闭或 Go 程序异常,而是 Nginx 在 60 秒无数据读取 后主动终止了与后端 Go 服务的连接——此时 Gorilla WebSocket 尝试从已关闭的底层 TCP 连接读取数据,便触发 unexpected EOF。

✅ 正确的 Nginx 配置(关键修复)

你需要在 location /ws/(或对应 WebSocket 路径)块中显式设置更长的 proxy_read_timeout,同时保留 WebSocket 必需的升级头配置

server {
    listen 80;
    server_name example.com;

    location /ws/ {
        proxy_pass http://127.0.0.1:1234;

        # 必须:启用 WebSocket 协议升级
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # 必须:透传真实客户端信息
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # ? 关键:延长读超时(建议至少 3600s,即 1 小时)
        proxy_read_timeout 3600s;

        # 可选但推荐:写超时与读超时保持一致
        proxy_send_timeout 3600s;
    }

    # 其他 location(如 /)可保留原有静态或 HTTP 服务配置
}
⚠️ 注意:proxy_read_timeout 必须放在 location 块内(而非 server 或 http),且作用于 proxy_pass 的后端连接,直接影响 WebSocket 长连接的保活能力。

✅ Go 服务端增强:添加 Ping/Pong 心跳

仅调大 Nginx 超时还不够——若客户端长时间不发消息,Nginx 仍可能因无流量而断连。Gorilla WebSocket 提供内置心跳支持,推荐在服务端主动发送 Ping,并自动响应 Pong:

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true // 生产环境请按需校验 Origin
    },
    // 可选:设置读写超时(单位:秒),避免 goroutine 泄漏
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

func handleWS(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("Upgrade error: %v", err)
        return
    }
    defer conn.Close()

    // ? 启用自动 Pong 响应(收到 Ping 自动回 Pong)
    conn.SetPongHandler(func(string) error {
        conn.SetReadDeadline(time.Now().Add(30 * time.Second))
        return nil
    })

    // ? 设置初始读取截止时间(防阻塞)
    conn.SetReadDeadline(time.Now().Add(30 * time.Second))

    fo

r { _, message, err := conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Printf("WebSocket error: %v", err) } break } // 示例:回显消息 if err := conn.WriteMessage(websocket.TextMessage, message); err != nil { log.Printf("Write error: %v", err) break } } }

此外,建议客户端(JavaScript)也定期发送 Ping(如每 25 秒),以维持双向活跃状态:

const ws = new WebSocket("ws://example.com/ws/");

// 每 25 秒发送一次 ping(服务端会自动 pong 响应)
let pingInterval;
ws.addEventListener("open", () => {
    pingInterval = setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({ type: "ping" }));
        }
    }, 25000);
});

ws.addEventListener("close", () => {
    clearInterval(pingInterval);
});

✅ 总结:三步确保 WebSocket 稳定长连接

  1. Nginx 层:在 WebSocket 对应 location 中设置 proxy_read_timeout(≥3600s)和 proxy_send_timeout;
  2. Go 层:启用 SetPongHandler 并合理设置 SetReadDeadline,避免空闲超时;
  3. 客户端层:主动发送 Ping 心跳,配合服务端 Pong 响应,形成闭环保活机制。

完成上述配置后,WebSocket 连接将稳定维持数小时甚至更久,彻底解决“一分钟自动断开”问题。务必重启 Nginx(nginx -s reload)并重新部署 Go 服务以使配置生效。