1. WebSocket到底是什么:从一次脉脉消息推送说起
屏幕突然亮起来,锁屏页弹出脉脉的一条新通知:有人查看了你的职业档案,附带着一个头像和一个公司名。指尖点进去,消息列表里那条“谁看过我”已经躺在那里,没有重新加载,没有转圈刷新,一切像是秒级到达。
这个场景放在今天你觉得理所当然,但早个十年,要让一个网页在不出浏览器的情况下实时收到服务器主动推送的消息,是个相当折腾的事。而改变这一切的底层技术,就是 WebSocket。它也是我在很长一段时间里真正“用懂”之后,才敢说自己入了实时通信这个门的核心协议。
这篇文章我打算用最坦白的方式聊一聊 WebSocket 实时通信的入门路径——协议怎么握手、数据帧长什么样、心跳断线如何应对、抓包怎么看、生产环境有哪些坑。之所以标题里要提一嘴“脉脉”,是因为当初我就是一边拿浏览器开发者工具观察脉脉这类真实应用的消息推送,一边对着协议文档一点点试出来的。比起干啃 RFC 6455,有个看得见摸得着的参照物,入门速度快很多。
这篇文章不只写给后端,前端、客户端、运维、刚转行做全栈的朋友都能看。只要你需要在自己的项目里做消息推送、在线聊天、实时通知或者看板刷新,WebSocket 几乎是你绕不开的第一站。看完你至少能回答三个问题:WebSocket 解决了什么、一条消息从服务器到你屏幕上经历了什么、真出问题了我该从哪里下手查。
1.1 从 HTTP 轮询到长连接,实时通信经历了什么
要理解 WebSocket 的价值,得先看看它出现之前大家是怎么做“实时”的。
最原始的做法是轮询,前端每隔一两秒向后端发一次 HTTP 请求,问一句“有没有新消息”。这种方式实现起来几乎没有门槛,但问题也肉眼可见:大部分请求是空手而归的,服务器白忙活,带宽白白占用。哪怕把间隔拉到五秒,消息的实时性也只能说“差不多”,不是真正意义上的即时。要是请求频率一高,数据库和负载都会被无谓的查询拖垮,这就是著名的“C10K 问题”在应用层的常见体现之一。
后来有人发明了长轮询,也就是前端发请求过去,服务器不着急响应,等真有新消息了再返回,或者等个几十秒超时再让前端重新连。这种方式比普通轮询实时性好一些,但本质上还是“一问一答”,每来一条消息都要重新走一遍 HTTP 请求头、响应头,浪费依然存在,而且服务器的连接管理变得很复杂。再后来还有 Flash Socket、ActiveX 之类的方案,都因为兼容性、安全性、平台限制没能成为通用标准。
WebSocket 的思路完全不同。它一开始也只发一个 HTTP 请求,但目的不是拿数据,而是“申请升级协议”。服务器同意后,双方之间的连接就从 HTTP 变成了 WebSocket,这是一条全双工的、长久的、轻量级的通道。全双工的意思是客户端和服务端都可以随时发消息,不用等对方先开口。类比一下,HTTP 像寄信,你来我往,每封信都有完整信封和邮戳;WebSocket 像是两个人之间拉了一根专线电话,拨通之后谁想说就说,说多久都行。
1.2 WebSocket 与 HTTP 的核心区别
两者不是取代关系,而是分工关系。HTTP 仍然负责绝大多数请求响应场景——打开网页、提交表单、调用接口。WebSocket 则专门为“连接建立之后需要持续双向通信”的场景而生。
| 维度 | HTTP | WebSocket |
|---|---|---|
| 通信模式 | 半双工,请求-响应 | 全双工,双向随时推送 |
| 连接生命周期 | 一次请求一次响应后即结束 | 一次握手,长连接,直到主动关闭 |
| 消息开销 | 每次请求都要带头部,动辄几百字节 | 帧很小,文本帧仅需额外几个字节 |
| 实时性 | 取决于轮询频率 | 秒级甚至毫秒级 |
| 典型场景 | 页面加载、 REST API、表单提交 | IM 聊天、行情推送、在线协同、多人在线游戏 |
简单记一句话:HTTP 是“你问我答”,WebSocket 是“我们连线聊”。
1.3 脉脉里的哪些场景在用 WebSocket
回到脉脉这个 App 和它的网页版。你打开开发者工具,切到 Network 面板,刷新一下页面,找到类型是 WS 的请求,会看到一条以 ws:// 或 wss:// 开头的连接。这条连接就是它做实时推送的命脉。
我当初观察到的典型场景有这么几类。一是新消息提醒,有人给你发私信或者评论你的动态,服务器通过这条 WebSocket 连接主动推给你,不需要你手动刷新页面。二是“谁看过我”这类动态通知,大多数情况下不是实打实即时推送,而是服务端检测到你在线后通过连接下发。三是在线状态,你在聊天列表里看到对方“在线”或“离开”,背后也是 WebSocket 维持心跳并同步状态的结果。
如果你还没有留意过真实应用的连接长什么样,我强烈建议你打开浏览器开发者工具,随便找一个带实时功能的网站,点开 Network 面板,过滤 WS,然后一边操作页面一边观察消息列表。这种“偷师”式的学习方法,比看十篇理论文章都有用。这也是为什么我把脉脉称作我的“好搭档”——它就是我的活体协议教学标本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. WebSocket 核心机制拆解:握手、数据帧与会话生命周期
很多人学 WebSocket 的时候被卡住,不是因为不会写代码,而是因为对协议本身没有一个整体画面。其实整个 WebSocket 会话就分四个阶段:握手建立连接、双向传输数据帧、心跳保活、关闭连接。把这四个阶段搞明白,后面写代码、调 bug 都会顺很多。
2.1 建立连接:HTTP Upgrade 握手过程
WebSocket 连接不是凭空从 TCP 上变出来的,而是靠 HTTP 协议“升级”过来的。客户端先发起一个普通 TCP 连接,然后发送一个带着升级意图的 HTTP 请求。
客户端发送的请求头大致长这样:
http复制GET /websocket HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
这里面的核心字段有三个。Upgrade: websocket 和 Connection: Upgrade 告诉服务器“我想升级协议”。Sec-WebSocket-Version: 13 表示当前使用的协议版本号,13 对应 RFC 6455,也就是现在的通用标准。Sec-WebSocket-Key 是一个随机生成的 Base64 字符串,用于让服务器证明它真的支持 WebSocket。
服务器收到请求后,如果同意升级,会返回这样的响应:
http复制HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
状态码 101 表示“我同意切换协议”。Sec-WebSocket-Accept 不是随便生成的,它的计算规则是固定的:把客户端传来的 Sec-WebSocket-Key 加上一个固定的 GUID 字符串(258EAFA5-E914-47DA-95CA-C5AB0DC85B11),做一次 SHA-1 哈希,再对结果做 Base64 编码。
之所以要加 GUID 做校验,是为了防止普通的 HTTP 缓存代理误把 WebSocket 升级请求当成普通 GET 请求缓存下来,同时也保证握手双方确实都理解 WebSocket 协议。任何一个以“ WebSocket 服务器”自居的服务端,都必须能正确算出这个 Accept 值。
2.2 数据帧结构:二进制视角看一条消息
握手完成后,双方之间传输的就不再是 HTTP 报文,而是 WebSocket 数据帧。很多人第一次看帧格式会头皮发麻,其实结构并不复杂,把每个比特的位置对上就行。一个数据帧大概长这样:
code复制 0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
