跳到正文

WebSocket

好消息:不用配置

章节「好消息:不用配置」

Caddy 能识别 WebSocket 握手请求并自动升级,不需要任何特殊指令:

ws.example.com {
reverse_proxy localhost:3000
}

握手、Upgrade 头、Connection 头、消息透传,全都自动处理。

WebSocket 是长连接,默认的空闲超时会把它掐掉:

ws.example.com {
reverse_proxy localhost:3000 {
transport http {
dial_timeout 10s
response_header_timeout 0
keepalive 30s
}
}
}

response_header_timeout 0 表示不限制——对长连接是必要的,否则握手慢一点就被判超时。

服务端也要配合:Node 的 ws / socket.io 要关掉心跳超时,Java 的 WebSocket 要调大 session timeout。

2. 中间的代理会掐断

章节「2. 中间的代理会掐断」

如果 Caddy 前面还有一层(Cloudflare、Nginx、SLB),每一层都有自己的空闲超时,取最短的那个:

层 默认空闲超时
Cloudflare 代理模式 100 秒(有流量时自动续)
AWS ALB 60 秒
Nginx proxy_read_timeout 60 秒
Heroku Router 55 秒

服务端每 30 秒发一个 ping 就能绕过所有这些限制。

后端通常会校验 Origin,反代后这个头还在。Caddy 可以顺手规范化:

ws.example.com {
reverse_proxy localhost:3000 {
header_up Origin {http.request.scheme}://{http.request.host}
}
}

或者干脆在 Caddy 层限来源:

ws.example.com {
@bad not remote_ip 192.168.0.0/16
respond @bad "Forbidden" 403
reverse_proxy localhost:3000
}

4. 多实例时的会话粘性

章节「4. 多实例时的会话粘性」

socket.io 长轮询(fallback transport)会被负载均衡打散,导致「session id unknown」。两个解法:

用粘性会话:

reverse_proxy localhost:3000 localhost:3001 {
lb_policy cookie io 1h
}

只升级 WebSocket(socket.io 会自动处理):

@ws {
header Connection *Upgrade*
header Upgrade websocket
}
handle @ws {
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3001
}

浏览器 DevTools → Network → WS:

  • 状态码 101 Switching Protocols = 成功
  • Frames 面板有持续消息 = 数据在流
  • 30 秒后断开 = 某层超时了

命令行:

终端窗口
websocat -v wss://ws.example.com/socket

服务端(Node):

const wss = new WebSocketServer({ server, path: '/socket' });
// 每 25秒 ping 一次,低于常见的 60s 超时
setInterval(() => {
wss.clients.forEach((client) => {
if (client.readyState === client.OPEN) client.ping();
});
}, 25_000);
报错 原因
Unexpected server response: 403 Origin 校验失败
Connection closed 固定 30/60 秒 空闲超时,配置 transport 超时 + 加心跳
1006 Abnormal Closure 网络层断开,看代理超时和负载均衡配置
握手成功但消息不通 后端路径和 Caddy 的 handle_path 剥掉的前缀不一致
多实例下「用户已断开」 没做会话粘性