WebSocket 入门:从一次握手看懂长连接
聊天消息、股票行情、协同编辑——很多场景都需要服务器主动把数据推给浏览器。HTTP 是“客户端先问、服务端再答”的请求-响应模型,要实现实时推送,只能靠反复轮询或长轮询,既浪费带宽,延迟也高。WebSocket 就是为解决这个问题而生的:一次握手,建立一条全双工的长连接,此后双方可以随时互相发送消息。
一次看似普通的 HTTP 握手
WebSocket 的握手借用了 HTTP。浏览器先发出一个普通的 HTTP 请求,但带上两个关键头:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Upgrade: websocket 是在告诉服务器:“我想把这条连接升级成 WebSocket”。服务器同意后返回 101 状态码:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept 是服务器把客户端的 Key 加上一段固定字符串后做 SHA-1 再 Base64 得出的,用来证明服务器确实理解 WebSocket 协议。握手完成后,HTTP 就退场了,同一条 TCP 连接转而传输 WebSocket 帧。
帧:长连接上的最小单位
握手之后传输的不再是 HTTP 报文,而是一帧一帧的消息。每帧都有简短的头部:操作码说明它是文本、二进制、关闭还是心跳(Ping/Pong),长度字段控制着载荷大小,一个大消息可以拆成多帧连续发送。Ping/Pong 帧用来探测对方是否还活着,中间的代理和负载均衡器也靠它判断连接是否空闲。
理解“帧”能帮你避开一个常见误区:应用层按帧解析即可,半包、粘包是 TCP 层面的现象,乱序和丢失由 TCP 负责,应用程序不用操心。
为什么不是一直用它
长连接也有代价。服务器要为每个连接维持状态,内存和文件描述符都会随在线用户数增长;连接断开后还要处理重连、断线重发和消息去重。聊天室和行情推送这类“服务端主动推、消息频繁”的场景值得付出这个代价;而偶尔查一次的数据,用普通 HTTP 反而更简单、更省资源。
一个实用的判断方法是:数据是不是“不知道什么时候会来”?如果是,就用 WebSocket;如果客户端知道自己什么时候需要数据,就用 HTTP。常见的折中做法是:用 HTTP 做登录和拉取历史记录,用 WebSocket 只做实时增量推送。
写一段最简单的客户端
浏览器原生就支持 WebSocket,不用任何库:
const ws = new WebSocket("wss://example.com/chat");
ws.onopen = () => ws.send(JSON.stringify({ type: "hello", name: "Tom" }));
ws.onmessage = (e) => console.log("收到:", e.data);
ws.onclose = () => console.log("连接已关闭,稍后重连");
注意两点:生产环境一律用 wss://(TLS 加密),不要用明文 ws://;onclose 里要安排重连——网络波动、服务器重启、代理超时都会悄悄断开连接,一个带退避间隔的重连循环,比“假设连接永远不断”可靠得多。
小结
WebSocket 的本质可以用三句话概括:用一次 HTTP 握手换来一条长期可用的 TCP 连接;连接之上的通信单位是帧;它适合服务端主动推送的场景,但不适合所有场景。下次看到实时聊天或行情页面时,不妨打开浏览器开发者工具的 Network 面板,找找那条返回 101 的请求——那就是长连接诞生的瞬间。
- 点赞
- 收藏
- 关注作者
评论(0)