不能直接用 http.RoundTrip 转发 WebSocket 请求,因为其会消费 req.Body 并关闭原始 TCP 连接,而 WebSocket 升级依赖未读取的连接及原始 Upgrade/Connection 头;必须通过 Hijack() 获取底层 net.Conn 进行双向字节流透传。

为什么不能直接用 http.RoundTrip 转发 WebSocket 请求
因为 http.RoundTrip 会消费并关闭原始请求的 req.Body,而 WebSocket 升级依赖原始未读取的 TCP 连接和 HTTP 头(尤其是 Upgrade: websocket 和 Connection: upgrade)。一旦 body 被读取或 response.WriteHeader 被调用,底层连接就无法 hijack —— 此时再想升级为 WebSocket 已经失效。
必须用 Hijack() 获取原始 net.Conn
代理要转发 WebSocket,核心是“不走标准 HTTP 流程”,而是手动接管连接:
- 在 handler 中检查
r.Header.Get("Upgrade") == "websocket",确认是 WS 请求 - 调用
w.(http.Hijacker).Hijack()获取底层net.Conn和bufio.ReadWriter - **立刻**调用
conn.SetReadDeadline防止阻塞,然后启动两个 goroutine:一个读原始请求头/数据并透传给后端,一个读后端响应并写回客户端 - 不要调用
w.WriteHeader或任何Write,否则 hijack 会 panic
gorilla/websocket.Upgrader 不适用于代理场景
Upgrader.Upgrade() 是为「终结型」WebSocket 服务设计的,它会自己完成握手、发送 101 响应、接管连接。但在代理中,你不能发 101 给客户端——你要把整个握手流程原样透传给后端服务器。所以:
- 别用
upgrader.Upgrade(w, r, nil),它会直接返回错误或 panic - 别设置
CheckOrigin,那跟代理无关;Origin 校验应由后端服务决定 - 真正要做的,是解析客户端 Upgrade 请求头,构造等效的 HTTP/1.1 请求,用
net.Dial连后端,再双向拷贝字节流
WSS 代理必须跳过 TLS 验证且复用 client hello
当代理转发 wss:// 请求时,目标是后端的 TLS 服务(如 wss://backend.example.com/ws),但客户端发来的是明文 HTTP Upgrade 请求(因 TLS 在代理前已解密)。此时:
立即学习“go语言免费学习笔记(深入)”;
- 代理需用
tls.Dial连后端,且tls.Config.InsecureSkipVerify = true(仅限测试)或配置正确 CA - **关键点**:不能简单发一个新 HTTP 请求,而要复用客户端的原始
ClientHello字节(如果代理本身是 HTTPS 入口),否则某些 WSS 服务会拒绝握手 - 更稳妥的做法是让代理监听
https://端点,用http.Server.TLSConfig提供证书,再对后端做 TLS 透传或终止
最易被忽略的是:WebSocket 代理不是“升级后转发消息”,而是“在升级发生前,把原始 TCP 流无损地桥接到另一个 TCP 流”。任何试图用 HTTP 语义包装 WebSocket 数据的行为,都会破坏帧格式或掩码逻辑。

















