WebSocket 这东西,我用 JavaScript 写了好几年,从最早为了做一个在线客服页面被轮询折腾到怀疑人生,到后来被 WebSocket 的实时推送能力彻底解放。这中间踩过的坑、熬夜查过的文档、写完后想给自己两巴掌的蠢代码,一抓一大把。很多朋友跑来问我 WebSocket 怎么用,我翻了一圈市面上的教程,要么上来就贴一堆代码看不懂在干嘛,要么讲原理讲得云里雾里,真正能把“为什么这么做”和“实际怎么落地”讲透的太少了。
这篇文章我就按照自己实际做项目的经验,把 JavaScript 里 WebSocket 的使用从原理到实践完整拆一遍。你会知道 WebSocket 到底解决什么问题、和 HTTP 的关系是什么、怎么建立一个稳定的连接、心跳检测和断线重连怎么做、遇到浏览器崩溃和连接断开的报错怎么排查,以及部署到 nginx 后面的时候要改哪些配置。适合刚接触 WebSocket 的前端新手,也适合那些已经能跑通 demo 但一上生产就翻车的同学。
1. 为什么需要 WebSocket:从一个反人性的轮询需求说起
1.1 HTTP 短轮询和长轮询到底哪里不对劲
在我真正用上 WebSocket 之前,做过一个项目,需求是页面上的订单状态要在 3 秒内更新。那时候我第一反应就是轮询,setInterval 每隔 3 秒发一次请求,接口返回最新的状态,然后局部更新页面。代码写起来确实简单,能在本地跑通,上线之后问题就来了。
首先是请求量的问题。页面开着的人越多,服务器收到的无效请求就越多。假设用户停留在页面上 10 分钟,那就是 200 个请求,其中 199 个都是在问“有没有变化”,而绝大多数时候答案都是“没有”。这是把有限的服务器带宽和数据库连接浪费在反复确认上。
其次是延迟问题。3 秒的间隔意味着用户看到状态变化最快也要等 3 秒,最慢可能要快 6 秒。你想把间隔缩短到 1 秒,服务器又扛不住。这就是 HTTP 短轮询的死结。
后来我试过长轮询,也就是客户端发一个请求过去,服务器先挂住这个请求,等有数据了才返回,客户端收到后再立刻发下一个。这个方案比短轮询好一些,消息可以做到几乎实时,但服务器端挂着一大堆等待中的请求,每个都占用一个连接资源,在高并发下照样吃不消。而且还要处理超时重发、连接中断等等情况,代码复杂度直线上升。
1.2 WebSocket 的设计取舍和适用边界
WebSocket 的设计思路和 HTTP 是完全不同的。HTTP 是“一问一答”,客户端发请求,服务器返回响应,然后这个连接的生命周期基本就结束了。WebSocket 建立的是一条长连接,客户端和服务器都能主动往对方那边发送数据,没有“请求-响应”这种模式,而是一条管道,两头都能随时往里扔数据。
这就解决了上面说的问题。连接建立之后,服务器有数据直接推给客户端,没有数据时连接是空闲的,不产生额外请求。订单状态变化的那一刻,服务器推一条消息过来,客户端收到就更新,真正做到实时。
那我是不是所有场景都应该用 WebSocket?不是。这也是我给很多人的忠告。如果你的场景是“隔几分钟拉一次数据”“用户主动点击才刷新”“数据变化不频繁”,用普通的 HTTP 请求完全够,甚至更简单更稳定。WebSocket 的适用场景有这些特征:需要服务器主动推送、对实时性要求很高、消息频率较高、客户端需要保持状态。典型的比如聊天室、在线协作编辑、股票行情、游戏对战、客服系统、监控面板。如果你的需求不符合这些特征,不要为了用而用。
1.3 WebSocket 连接的工作流程:建立、通信、关闭
WebSocket 连接的完整生命周期可以分成三个阶段:握手、双向通信、关闭。
握手阶段实际上是走 HTTP 的。客户端发一个带有特殊请求头的 HTTP 请求,服务器确认之后返回 101 状态码,通信协议才会从 HTTP 升级为 WebSocket。这个过程看起来是“一次请求”,但那个请求不是普通的业务请求,它只是完成协议的切换。
双向通信阶段就是数据往来。数据以帧的形式传输,可以传文本,也可以传二进制数据,比如图片、音频、ArrayBuffer。这个阶段没有请求和响应的概念,两端地位完全平等。
关闭阶段可以由任何一端发起,也可以因为网络异常或服务器崩溃而断开。需要注意的是,WebSocket 的连接不会永远活着,网络波动、代理超时、服务器重启都会导致连接断开,所以生产环境里必须考虑断线重连。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 协议机制拆解:握手细节、数据帧和心跳保活
2.1 从 HTTP 到 WebSocket:一次握手到底做了什么
很多教程直接让你 new WebSocket(url) 就完事了,根本不讲握手过程。但如果你不懂握手,后面遇到“连接一直失败”“wss 握手不过”“代理服务器报 400”这类问题的时候,你会一头雾水。
客户端发起握手时,请求头大概是这样的:
http复制GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
关键的几个头:Upgrade 和 Connection 告诉服务器要把协议升级成 WebSocket,Sec-WebSocket-Key 是一个经过 base64 编码的随机字符串,相当于客户端给服务器出的“考题”,Sec-WebSocket-Version 一般固定是 13,这是目前的标准版本。
服务器收到之后,会拿 Sec-WebSocket-Key 拼上一个固定的 GUID,做一次 SHA-1 哈希,再 base64 编码,放到 Sec-WebSocket-Accept 响应头里返回。这个过程是为了证明“服务器这边确实支持 WebSocket 协议”,避免客户端连到一个不支持 WebSocket 的普通 HTTP 服务器上。
如果握手成功,状态码是 101,也就是“切换协议”。从这一刻开始,连接不再是 HTTP,而是 WebSocket。如果服务器不支持或者配置不对,会返回 400 或者其他错误码。
我在实战中遇到过一个很典型的坑:本地开发用 http 访问页面,然后连的是 ws:// 开头的 WebSocket 地址,没问题。但一旦上线到了 https 环境,页面本身是 https,你必须用 wss:// 去连 WebSocket,不能再用 ws://。浏览器出于安全策略会直接阻止这种“https 页面请求 ws 连接”的行为。wss 和 ws 的关系,就相当于 https 和 http,加密和不加密的区别。
2.2 数据帧与消息分片:文本、二进制、Ping/Pong
握手完成后,数据不是以普通 HTTP 报文传输的,而是以帧为单位。每个帧有固定的格式,包含操作码、长度、掩码等信息。这部分你不需要深究到二进制层面,但有几个概念要清楚。
WebSocket 的操作码区分了不同类型的帧。文本帧是 0x01,二进制帧是 0x02,关闭帧是 0x08,Ping 帧是 0x09,Pong 帧是 0x0A。浏览器原生 API 里,你不需要手动构造帧,但当你收到数据时,需要看 event.data 的类型,是字符串还是 Blob 还是 ArrayBuffer,这其实对应了服务端发送的是文本帧还是二进制帧。
这里有个容易被忽略的细节:如果服务端发来的是一个大消息,WebSocket 协议允许它被拆分成多个帧传输,这就是分片。浏览器会自动把分片重组好,你拿到的还是一个完整的消息。但是如果网络不稳定,分片传输中出现了丢包或者中断,你可能会收到不完整的数据,甚至连接直接断开。
我一般在前端代码里会加一个判断,根据数据格式做不同处理:
javascript复制ws.onmessage = function (event) {
if (typeof event.data === 'string') {
// 处理文本消息
console.log('收到文本:', event.data);
} else if (event.data instanceof ArrayBuffer) {
// 处理二进制数据
console.log('收到二进制数据');
} else if (event.data instanceof Blob) {
// 处理 Blob,比如图片、文件
console.log('收到 Blob');
}
};
在某些游戏或者音视频场景中,前后端可能直接约定二进制协议,一堆字节按顺序排列代表不同字段,这种时候直接用 ArrayBuffer。如果是普通的聊天消息,JSON 字符串就够了。
2.3 心跳保活:连接看似活着,其实已经死了
这是我见过最多新手忽略的问题,也是生产环境里 WebSocket 连接“莫名其妙”断开的最大原因之一。
WebSocket 连接在建立之后,默认情况下连接是“沉默”的。连接建立后,如果长时间没有任何数据流动,中间的网络设备比如路由器、防火墙、负载均衡器,会把空闲的连接默默掐掉。这不是 WebSocket 独有的问题,TCP 连接本身就会遇到,只不过 HTTP 短连接用完了就断开,长连接才会被空闲超时影响到。
你可能会想:断了就断了,反正我做了重连。但问题是,连接断开时服务器或者客户端并不会立刻感知到,尤其是当你只是默默打开页面不操作的时候。你看着页面,以为连接还在,实际上底层早就断了。等你下一次主动发消息的时候,才发现发送失败,或者你期待服务器推消息,结果什么都没有。
解决方案就是心跳机制。客户端每隔一段时间发一个 Ping 帧,服务器收到后自动回一个 Pong 帧,如果客户端超过一定时间没收到 Pong,就认为连接已经死了,主动关闭并重新连接。用浏览器原生 WebSocket API 的话,你不能手动发 Ping 帧,因为浏览器不给你这个接口。常见的做法是直接发一个自定义的文本消息,比如
