客户端通过WebSocket构造函数第二个参数传子协议(字符串或数组),浏览器自动映射为Sec-WebSocket-Protocol请求头;服务端须在101响应中原样返回匹配值,否则ws.protocol为空。

直接在 WebSocket 构造函数中传入子协议名,连接建立时浏览器会自动带上 Sec-WebSocket-Protocol 请求头,服务端按规则响应后,ws.protocol 就能拿到协商成功的协议名。关键不是“怎么建连接”,而是“怎么让子协议真正生效”。
客户端怎么传子协议
第二个参数就是子协议声明位置,别绕到 header 或 query 里去:
- 单协议:直接传字符串,如
new WebSocket("wss://api.example.com", "json-rpc")</li> <li>多协议候选:传字符串数组,按优先级从左到右,如 <code>new WebSocket("wss://api.example.com", ["myapp-v2", "myapp-v1"])</li> <li>不协商子协议:省略该参数、传 <code>null、undefined或空数组[],此时请求头不会出现Sec-WebSocket-Protocol - 协议名必须合法:只含小写字母、数字、
.和-,例如"chat.v2"合法,"Chat/V2"或"user_v1"会被浏览器静默忽略或报错
服务端怎么响应才算成功
服务端不能只“支持”某个协议,而必须在 101 响应中**原样返回**客户端列表里的某一个值:
- Node.js +
ws库:在handleProtocols回调中返回字符串(如return "json-rpc"),返回null表示全部拒绝 - FastAPI:调用
websocket.accept(subprotocol="json-rpc"),且该字符串必须出现在客户端传的数组中,一个字符都不能差 - .NET 的
ClientWebSocket:用options.AddSubProtocol("json-rpc")添加,不是赋值给某个属性 - Spring Boot:需在握手拦截器
beforeHandshake中读取请求头,并确保响应头写回匹配项
怎么确认子协议真的协商上了
别等出问题才查,连接一打开就看 ws.protocol:
立即学习“Java免费学习笔记(深入)”;
- 加一行日志:
ws.onopen = () => console.log("Negotiated:", ws.protocol) - 输出为空字符串
""?立刻检查 Network 面板:- 请求头是否有
Sec-WebSocket-Protocol(值是否合法、是否被代理过滤) - 响应头是否有同名字段,且值与客户端所列完全一致(大小写、连字符、顺序都算)
- 请求头是否有
- 只要其中任一环失败,
ws.protocol就是空,但连接仍能建立——后续所有依赖该协议的解析逻辑都会静默失效
常见陷阱和注意事项
子协议不是运行时开关,也不是安全机制,它只在握手阶段起作用:
-
ws.protocol是只读的,连接建立后无法修改,协商失败只能重连 - 代理(Nginx、CDN)常默认丢弃
Sec-WebSocket-Protocol头,需显式配置:proxy_pass_request_headers on;,并确认没被proxy_set_header覆盖 - 子协议不校验消息内容:浏览器不会检查你发的是不是 JSON、字段对不对、opcode 是不是文本帧——它只负责把字节流送过去
- 不要用 URL 参数或自定义 header 做认证替代子协议,那是绕过握手期校验,失去协议层拦截非法连接的能力


















