mod_proxy_wstunnel仅透传http upgrade请求,双向通信依赖后端websocket协议实现;常见断连因后端未响应ping帧、proxytimeout过短(默认60秒)或upgrade/connection头被清洗。

mod_proxy_wstunnel 本身不支持真正的双向通信(如 WebSocket 的 full-duplex),它只是把 HTTP Upgrade 请求透传给后端 WebSocket 服务——真正双向能力完全依赖后端服务是否正确实现 WebSocket 协议。
为什么 mod_proxy_wstunnel 看起来“断连”或收不到后端推送
常见现象是前端能发消息,但收不到后端主动推送;或者连接几秒后自动关闭。这不是 Apache 配置问题,而是:
• 后端 WebSocket 服务未正确响应 ping 帧,导致 Apache 认为连接失效
• Apache 默认超时太短(ProxyTimeout 默认 60 秒),而 WebSocket 连接常需长保持
• 缺少必要的 HTTP 头透传,比如 Upgrade、Connection 被 Apache 或中间 CDN 清洗掉了
必须加的 Apache 配置项和顺序
仅启用模块和简单 ProxyPass 不够,以下配置缺一不可:
-
LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so(确保已加载) - 在
<virtualhost></virtualhost>或<location></location>中写:
ProxyRequests Off
ProxyPreserveHost On
ProxyTimeout 300
# 关键:显式允许 Upgrade 和 Connection 头透传
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /ws/(.*) ws://127.0.0.1:8080/$1 [P,L]
<h1>或等价写法(不用 Rewrite):</h1><h1>ProxyPass "/ws/" "ws://127.0.0.1:8080/"</h1><h1>ProxyPassReverse "/ws/" "ws://127.0.0.1:8080/"</h1>
注意:ProxyPass 直接写 ws:// 协议前缀才能触发 mod_proxy_wstunnel,写 http:// 会走 mod_proxy_http 并失败。
后端服务必须响应 ping 并维持心跳
Apache 会定期发送 WebSocket ping 帧(默认每 60 秒),若后端不回 pong,连接会被 mod_proxy_wstunnel 主动关闭。常见错误包括:
- Node.js 的
ws库默认处理 ping/pong,但 Express +http-proxy-middleware类代理不会自动透传 - Java Spring Boot 的
@MessageMapping不处理底层 ping,需手动注册PongMessageHandler或启用WebSocketHandlerRegistry的默认心跳 - Go 的
gorilla/websocket需调用conn.SetPingHandler(...)并确保conn.ReadMessage()或conn.NextReader()持续被调用,否则 ping 帧堆积不触发回调
调试连接是否真走 wstunnel 的方法
别只看浏览器 Network 标签里状态码是 101 —— 那只说明 Upgrade 成功,不等于数据通路正常:
- 在 Apache 日志中开启
LogLevel proxy:trace4,搜索wstunnel或ws://关键字,确认日志出现proxy_wstunnel: connecting to ws://... - 用
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" http://yourdomain/ws/test测试,应返回HTTP/1.1 101 Switching Protocols,且响应头含Upgrade: websocket和Connection: Upgrade - 后端打印原始请求头,确认
Sec-WebSocket-Key和Origin未被 Apache 丢弃(ProxyPreserveHost On不能代替头透传,必要时加RequestHeader set Origin "https://yourdomain")
真正难的不是配 Apache,而是后端 WebSocket 实现是否扛得住真实网络抖动、是否漏处理控制帧、是否在 idle 时仍保持读循环 —— 这些问题在本地直连时根本暴露不出来。










