|
WebSocket 握手阶段偷了个懒——它直接复用了 HTTP 的议详 Upgrade 头。客户端发一个带 Connection: Upgrade 和 Upgrade: websocket 的议详 GET 请求,服务器如果同意,议详回 101 Switching Protocols。议详这之后,议详同一个 TCP 连接上跑的议详不再是 HTTP 的请求-响应模型,而是议详全双工的数据帧。我见过不少项目在 Nginx 里配 WebSocket 时忘了加 proxy_set_header Upgrade $http_upgrade,议详结果握手一直失败,议详查半天以为是议详代码问题。 帧结构是议详 WebSocket 最值得细看的部分。每个帧开头两个字节,议详第一个字节的议详 FIN 位标记消息是否结束,RSV1-3 留给扩展用,议详opcode 区分文本(0x1)、议详二进制(0x2)、关闭(0x8)、ping(0x9)、pong(0xA)。第二个字节的 MASK 位决定 payload 是否掩码处理。客户端发往服务器的帧必须掩码,反过来不强制。这个设计当年引发过争议,有人觉得多余,但它确实挡住了早期代理缓存污染攻击。掩码键是 4 字节,payload 每个字节跟掩码键按位异或,解起来不费劲。 浏览器里 new WebSocket('wss://example.com/chat') 一行就建立连接。onopen 触发后,send() 可以发字符串或 ArrayBuffer/Blob。接收端 onmessage 拿到 event.data,文本就是字符串,二进制看 binaryType 设成 'blob' 还是 'arraybuffer'。有个容易踩的坑:send() 在连接未就绪时调用会抛 InvalidStateError,得等 readyState 变成 OPEN。readyState 四个值——CONNECTING(0)、OPEN(1)、CLOSING(2)、CLOSED(3)——记得比记 HTTP 状态码还熟。 心跳机制不是协议强制的,但生产环境几乎必加。TCP 连接可能被中间设备静默断开,双方都不知道。做法是客户端定时发 ping,服务端回 pong;或者服务端主动 ping。浏览器 WebSocket API 不暴露 ping/pong 帧的发送接口,所以前端通常用应用层心跳——定时 send 一个特定消息,服务端原样返回或回确认。Node.js 的 ws 库倒是可以直接 ws.ping()。我一般设 30 秒间隔,超时两次就重连。 重连策略值得单独写。直接 setInterval 死循环重连很危险,服务端挂了会引发雪崩。指数退避是基本操作:第一次 1 秒,第二次 2 秒,4 秒,8 秒,封顶 30 秒,加随机抖动避免同时重连。重连成功后要重新订阅频道、恢复状态。有些团队用 Socket.IO 这类库省事,它自带重连、房间、降级到长轮询,但协议层多了层封装,调试时抓包看到的不再是纯 WebSocket 帧。 安全方面,wss 跟 https 一样基于 TLS,生产环境没理由用 ws。跨域问题不存在——WebSocket 不受同源策略限制,服务器得自己校验 Origin 头。我见过没校验 Origin 的聊天服务被恶意页面连上,用户身份被冒用。认证通常在握手阶段做:把 token 放 URL query 或 Sec-WebSocket-Protocol 头里,服务端在 upgrade 事件里验证,不通过就 destroy socket。连接建立后再发认证消息也行,但中间有个未认证窗口。 代理和负载均衡是部署时的痛点。Nginx 从 1.3 开始支持 WebSocket 代理,核心是 proxy_http_version 1.1 加 Upgrade 头透传。HAProxy 需要配 option http-server-close 和 timeout tunnel。云厂商的 ALB 一般有 idle timeout,默认 60 秒,得调大或者用心跳保活。Kubernetes 里 Ingress 控制器对 WebSocket 的支持参差不齐,Nginx Ingress 需要加注解 nginx.ingress.kubernetes.io/websocket-services。 最后说个实际数字:单个 WebSocket 连接在服务端占用的内存,Node.js 下大约 10-20KB,取决于库和缓冲区配置。一台 4GB 内存的机器撑几万连接不算难,瓶颈往往在文件描述符上限和网络带宽。C10K 问题在 WebSocket 场景下变成了 C10M,但那是另一个话题了。 |