必须用gorilla/websocket,因其唯一支持子协议协商:标准库和golang.org/x/net/websocket均不解析sec-websocket-protocol头,upgrade时需显式传入subprotocols切片(如[]string{"json","protobuf"}),不匹配则返回400;nginx代理须透传该头,否则协商失败。

Go 服务端要支持 WebSocket 子协议(subprotocol),必须用 gorilla/websocket,标准库和 golang.org/x/net/websocket 都不支持子协议协商 —— 它们连 Sec-WebSocket-Protocol 请求头都不解析,更不会在响应中写入 Sec-WebSocket-Protocol。
为什么必须用 gorilla/websocket.Upgrader.CheckOrigin 以外的字段?
子协议不是靠中间件或路由层控制的,它必须在握手阶段由 Upgrader 显式声明。关键点是:子协议列表得通过 Upgrader.Upgrade() 的第三个参数传入,而不是写在 CheckOrigin 或其他回调里。
-
Upgrader默认不启用子协议,即使客户端发了Sec-WebSocket-Protocol: json, protobuf,服务端也会忽略 - 必须显式提供允许的子协议列表,例如
[]string{"json", "protobuf"} - 服务端只会从中选一个匹配项返回给客户端;不匹配则拒绝升级(HTTP 400)
如何正确传入子协议列表?
调用 upgrader.Upgrade() 时,第三个参数是 *http.Header,你要在这里设置 Sec-WebSocket-Protocol 响应头 —— 但更推荐直接传入子协议切片,让 gorilla/websocket 自动处理协商逻辑。
- 错误写法:
upgrader.Upgrade(w, r, nil)→ 子协议被完全忽略 - 正确写法:
upgrader.Upgrade(w, r, &websocket.HandshakeParams{Subprotocols: []string{"json", "protobuf"}}) - 或者更简洁地:
upgrader.Upgrade(w, r, http.Header{"Sec-WebSocket-Protocol": []string{"json"}}),但需确保该值在客户端请求头中真实存在
客户端怎么确认子协议生效?
浏览器或 Go 客户端连接后,不能只看连接是否建立成功,得检查实际协商结果。子协议不匹配时,Upgrade() 会返回 websocket.ErrBadHandshake,但错误信息模糊,容易误判为跨域或升级失败。
- 浏览器端可通过
ws.protocol获取最终协商出的子协议名(如"json") - Go 客户端用
websocket.DefaultDialer.Subprotocols设置请求列表,连接后查conn.Subprotocol() - 服务端可在
conn.Subprotocol()中拿到最终选定的协议名,用于后续消息路由或序列化选择
nginx 反向代理下子协议容易失效
如果服务前有 nginx,默认不会透传 Sec-WebSocket-Protocol 头,导致服务端收不到客户端声明的子协议,协商必然失败。
- 必须在 nginx 配置中显式添加:
proxy_set_header Sec-WebSocket-Protocol $http_sec_websocket_protocol; - 同时确保
proxy_http_version 1.1;和proxy_set_header Upgrade $http_upgrade;已启用 - 漏掉任意一项,客户端看到的都是
Unexpected response code: 200或静默断连
子协议本身不改变帧格式,但它决定了双方对 payload 的解释方式;一旦协商完成,就再无回头路 —— 错误选择会导致序列化/反序列化崩溃,而这种问题往往在消息交互几轮后才暴露,不是连接阶段就能发现的。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











