🌈个人主页 :一条泥憨鱼(欢迎各位大佬莅临)

🎬精选专栏传送门:
❄️《数据结构》 ❄️《AI与Agent那些事》
❄️《从0开始学计算机网络》 ❄️《后端开发》

前言:
你有没有想过这么一个问题:你在网页上聊天,对方发来一条消息,你的浏览器是怎么"知道"有新消息了?
不是你去刷一下页面,也不是你点了刷新按钮。消息就那么自己蹦出来了。你什么都没做,数据却主动送到了你眼前。
这事儿搁在十年前,是想都不敢想的。那时候的网页,想看到新内容,你得自己按 F5 刷新。
那现在凭啥能自动蹦出来?背后干活的,就是今天要聊的这个东西------WebSocket。
不过别急,要搞懂 WebSocket,得先看看它出来之前,网页是怎么"交流"的。


先搞清楚 HTTP 的"一问一答"困局
你平时上网,浏览器和服务器之间用的是 HTTP 协议。这个协议的工作方式,特别像你去窗口办事。
你递过去一张单子(请求),窗口里的人处理完,把结果递给你(响应)。然后呢?然后就没有了。窗口关了,各回各家。
下次你想再问点啥,得重新排一次队、再递一张单子。
这个模型叫"请求-响应 "。特点就是:永远是你先开口,服务器才回答。服务器从来不会主动跑过来跟你说"嘿,有新消息了"。
那问题来了。聊天的时候,对方发来一条消息,这条消息存在服务器上。服务器想告诉你,但它不能主动开口。它只能等你来问。
于是早期的开发者想了个笨办法------轮询。你不是不来问吗?那我隔几秒就来问一次。
客户端每隔几秒发一次请求,服务器每次都返回"没有新消息",重复多次

客户端 服务器
|---- 有消息吗? -------->|
|<--- 没有,下次再来 ----|
|---- 有消息吗? -------->|
|<--- 没有,下次再来 ----|
|---- 有消息吗? -------->|
|<--- 有!给你 -------->|
你隔几秒问一次,确实能"差不多实时"地拿到消息。但这个方案又蠢又费。
你想啊,如果一万个人同时在线,每个人都隔三秒问一次,服务器每秒得处理几千个"有消息吗"的废话请求。大部分时候答案都是"没有"。带宽白烧了,服务器也累得够呛。
而且还有延迟。假设消息在第 1 秒到了服务器,你正好在第 2 秒问过,那你要等到第 4 秒再问才能拿到。多等三秒,聊天体验就很拉胯。
那有没有办法让服务器主动开口?有。这就是 WebSocket 要干的事。
WebSocket 握手------从 HTTP 升级成"实时通道"
WebSocket 的思路很巧妙:先走一遍 HTTP 把话说清楚,然后把这个连接"升级"成一个能双向通话的通道
这个升级过程,就叫握手(Handshake)。
具体来说,客户端发一个普通的 HTTP 请求,但带着几个特殊的头:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
这里的关键是 `Upgrade: websocket` 和 `Connection: Upgrade`,意思就是:"哥们,我想把这个连接升级成 WebSocket。"
`Sec-WebSocket-Key` 是一串随机生成的字符串(Base64 编码的),它的作用是让服务器确认"你是真的想升级,而不是乱发请求"。
服务器收到之后,会把这串 Key 拼上一个固定字符串 `258EAFA5-E914-47DA-95CA-C5AB0DC85B11`,然后做一次 SHA-1 哈希,再 Base64 编码,得到一串新的字符串,放在 `Sec-WebSocket-Accept` 里返回:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
注意那个状态码:101 。它不是 200(成功),也不是 404(找不到),而是"切换协议 "。意思就是:"行,我同意升级,从现在开始咱们不用 HTTP 那套规矩了,走 WebSocket。"

这里有个坑要提醒你:**很多防火墙和代理服务器只认 HTTP,看到 101 状态码可能直接懵了,把连接掐断。**所以 WebSocket 一般用 80 端口(HTTP)或 443 端口(HTTPS/WSS),就是为了混在普通流量里,降低被拦的概率。
如果你想亲眼看看这个握手过程,可以用 Node.js 写个最简单的服务器:
javascript
// 极简 WebSocket 握手,不依赖任何库
const http = require('http');
const crypto = require('crypto');
const server = http.createServer((req, res) => {
// 只处理握手请求
const key = req.headers['sec-websocket-key'];
// 固定 GUID + SHA-1 哈希 + Base64
const accept = crypto
.createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
.digest('base64');
res.writeHead(101, {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Accept': accept
});
res.end();
});
server.listen(8080, () => {
console.log('WebSocket 服务器跑在 8080 端口');
});
跑起来之后,你用浏览器的控制台连一下:
javascript
// 浏览器控制台里直接跑
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => console.log('握手成功,通道已建立!');
看到控制台打出"握手成功",恭喜你,通道已经建立了。
不过握手只是第一步。握手之后,这个连接就变了个形态,变成了"全双工"。
全双工------两条车道同时跑
先解释一下"双工"这个词。听起来高大上,其实特别简单。
你用过对讲机吧?对讲机有个规矩:一个人说话的时候,另一个人只能听着。你按下通话键说话,说完松开,对方才能说。两边不能同时说话,不然就乱套了。
这种"同一时间只能一个方向传数据 "的方式,叫半双工(Half-duplex)。
而打电话就不一样了。你说话的时候,对方也能同时说话。两边可以同时说、同时听,互不干扰。这叫全双工(Full-duplex)。
HTTP 就是半双工------你发请求的时候服务器等着,服务器响应的时候你等着,永远是一来一回。
WebSocket 握手完成之后,这个连接就变成了全双工。两边随时都能发数据,不用等对方开口。

这就像从对讲机切换到了电话。服务器想给你推消息,随时都能推;你想给服务器发消息,也随时能发。两边互不等待。
那数据是怎么传的呢?WebSocket 有自己的一套"帧"格式。你不需要记所有细节,只需要知道:文本消息会被打包成一个数据帧,里面有消息内容、长度、以及一个"这是最后一段"的标志位。
用刚才那个服务器接着写,接收消息并回一句:
javascript
// 在握手成功之后,处理数据帧
server.on('upgrade', (req, socket) => {
socket.on('data', (buffer) => {
// 这里简化处理,假设收到的就是一条文本帧
// 真实的帧解析要处理掩码、长度、分片等,比较复杂
console.log('收到消息:', buffer.toString());
// 构造一个最简单的文本帧回给对方
// 0x81 表示"这是文本帧且是最后一帧"
const frame = Buffer.concat([
Buffer.from([0x81, 0x05]), // 0x05 表示后面跟 5 个字节
Buffer.from('hello')
]);
socket.write(frame);
});
});
当然,这个代码只是个演示。真实的帧解析要处理的东西多得多------掩码、分片、不同长度编码、二进制帧......所以实际开发中,没人会手写这个,都用现成的库,比如 `ws` 或者 `socket.io`。
但理解了"帧"这个概念就够了:WebSocket 不是把一整条消息直接扔过去的,而是拆成一个个帧,每个帧有头(描述类型和长度)和载荷(实际数据)。
心跳------怎么判断对方还活着
现在通道建好了,双向通信也通了。看起来一切完美。
但有个问题:你怎么知道对方还活着?
你可能会说,连接断了我重新连呗。但问题是------很多连接断掉的时候,你根本不知道。
举个例子。你手机连着 WiFi,走到电梯里,信号断了。**TCP 连接还在那儿挂着,但你这边已经收不到任何数据了。**服务器那边也一样,它发了几次数据都石沉大海,但它不知道是你挂了还是网络抽风了。
更麻烦的是,很多中间设备(比如路由器、防火墙、负载均衡器)会主动掐断长时间没有数据传输的连接。你连着 WebSocket 但一直不说话,过了几分钟,中间某个路由器觉得"这连接没用了",啪一下给你掐了。但你这边毫无察觉。

怎么解决?靠"心跳"。
**心跳(Heartbeat)**的概念是从生物里借来的。人活着,心脏就一直跳。连接活着,就得一直"跳"------定期发点东西让对方知道"我还活着"。
WebSocket 协议里专门设计了两个控制帧:Ping和 Pong。
你发一个 Ping 帧过去,对方必须回一个 Pong 帧。如果一段时间没收到 Pong,你就可以认为对方挂了,主动断开连接,该重连的重连,该报错的报错。
javascript
// 带心跳的伪代码
const WebSocket = require('ws');
const ws = new WebSocket('ws://example.com/chat');
// 每隔 30 秒发一个 ping
const timer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping(); // 发 ping
}
}, 30000);
// 收到 pong 就说明对方还活着
ws.on('pong', () => {
console.log('收到 pong,连接正常');
});
// 超过 10 秒没收到 pong,判定死了
ws.on('close', () => {
console.log('连接断了,准备重连...');
clearInterval(timer);
// 这里写重连逻辑
});
这里有个踩坑点必须提醒你:**有些 WebSocket 库默认不自动回 pong。**你发了 ping,对面没反应,你就以为它挂了,其实它活着呢,只是没回你。
所以用库的时候,一定要确认它有没有自动处理 pong 帧。如果没有,你得自己写。
另外,心跳间隔也别设太短。你每 5 秒 ping 一次,服务器没病都得被你累出病来。一般 30 秒到 60 秒一次比较合理。具体看你业务场景------如果实时性要求高,可以短一点;如果只是保活,60 秒足够。
总结一下
WebSocket 解决了 HTTP"一问一答"的困局,核心就是三件事:
-握手:通过 HTTP 的 `Upgrade` 头 + 101 状态码,把普通连接升级成 WebSocket 通道。
-
全双工:升级之后,两边可以同时收发数据,不用等对方开口。
-
心跳:用 Ping/Pong 定期确认对方还活着,防止连接被中间设备悄悄掐断。
这三个概念是环环相扣的:没有握手就建立不了通道,没有全双工就没有实时推送的意义,没有心跳连接就不可靠。
如果你想动手验证一下,最快的办法是装个 `wscat`:
bash
npm install -g wscat
wscat -c ws://echo.websocket.org
连上去之后随便发条消息,服务器会原样弹回来。你还能在浏览器控制台里用 `new WebSocket()` 连同一个地址,看看两边能不能互相通信。
WebSocket 看起来简单,但背后藏着不少细节:帧格式、掩码、分片、子协议、断线重连......这次先搞懂这三个核心概念,剩下的,遇到问题再慢慢啃。