1. 当前位置: 网站首页 >  路由器百科 >  WebSocket是什么意思?网页不刷新温度自己跳,我抓了个包,这条线握手完就再没挂断

WebSocket是什么意思?网页不刷新温度自己跳,我抓了个包,这条线握手完就再没挂断

WebSocket是什么意思:轮询与WebSocket长连接对比、101握手流程、帧结构与心跳示意图

手机打开 Home Assistant 的页面搁在桌上,客厅温度从 26.4 跳到 26.5,我没碰屏幕。这是我上回把 HA 装进 NAS 之后最纳闷的一件事:网页不是点一下才动吗,它凭什么自己动?

差不多同一时间,我在 qBittorrent 的网页版里盯下载速度,那个数字也在跳,每一两秒跳一格。同样是「活」的页面,后来我才搞明白,这两个的实现路子完全不同——一个靠网页一遍遍去问,一个靠服务器主动推。后者用的就是 WebSocket。

两个不刷新的页面,一个靠问一个靠推

qBittorrent 那个速度数字,用的是最朴素的办法:轮询。网页里的脚本每隔一两秒向服务器发一次请求——「现在多快了?」服务器答一句「38.2MB/s」,连接拆掉,隔一秒再来一遍。你看它跳得挺欢,其实是网页在一秒一秒地刷,只不过每次只刷一小块数据,不是整个页面,所以看起来不像刷新。

这办法笨归笨,能用。但账得算清楚:假如有 100 个人同时开着这个页面,每人每秒问一次,服务器每秒就得应付 100 个请求,其中绝大部分的回答是「跟刚才一样,没变化」。人一多,服务器大半算力都花在回答「没变化」上了。

HA 的温度不是这么来的。开着抓包看一眼就知道,页面加载完之后,温度更新的时候浏览器没有发起新的请求——是服务器把「26.5」这一条消息主动推下来的。这有个前提:那条连接得一直开着。服务器想主动说话的时候,手里必须有一条现成的、没挂断的线。

HTTP 的规矩是你问我答,服务器没有主动开口的门

普通 HTTP 就是这个规矩,我在讲 HTTP 和 HTTPS 那篇里写过:一次请求一次响应,说完就散。这在网页还是「文档」的年代没毛病——文档不会自己变。可现在的网页是「应用」,温度、聊天消息、下载速度随时在变,还让浏览器一遍遍干问,太浪费。

于是 2011 年 12 月,RFC 6455 把 WebSocket 定成了标准。思路就一句话:先借着 HTTP 的门进来握个手,握手成了,这条 TCP 连接就不拆了,改成一条两头都能随时开口的电话线。它跟 TCP 和 UDP 那篇讲的传输层不是一层面的东西——WebSocket 是骑在 TCP 上面的一种应用层协议,地位上和 HTTP 平级。

握手:借 HTTP 的门进来,101 之后再不回头

光看文字不过瘾,我在自己电脑上起了个最小的 WebSocket 服务,用最原始的 socket 把握手亲手走了一遍。客户端发出来的其实就是一个长相特殊的 HTTP 请求:

GET /chat HTTP/1.1 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: wN4C0c0BNszHE5gU96k5Xw== Sec-WebSocket-Version: 13

重点在前两个头:Upgrade: websocket,直译就是「我想升级成 websocket」;Connection: Upgrade 是「这条连接别按老规矩处理」。请求本身还是 HTTP 的格式,所以走 80 或者 443 端口出门——这就是为什么家里用这些实时页面从来不用在路由器上开端口转发,跟端口转发那篇里为了外人连进来而开大门是两码事:连接是家里主动发起出去的,方向反着呢。端口号还是那几个老端口,路由器只当它是普通网页流量。

服务器要是懂行,回这么一句:

HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: 7pBSP80/MOKV55KufDiJMIOhP/o=

101 这个码在状态码那篇里提过:1xx 家族的意思是「收到了,协议正在切换」。101 Switching Protocols 就是「换好了」。从这一行之后,这条连接上跑的不再是 HTTP 的问答,而是 WebSocket 自己的格式,直到哪一边主动关闭为止。也就是说,握手总共就发生这一次,之后浏览器再想收温度更新,一个字都不用先发,等着就行。

握手里最费解的是 Key 和 Accept 这一对。Sec-WebSocket-Key 是客户端现场生成的 16 个随机字节的 base64,我这回抓到的是 wN4C0c0BNszHE5gU96k5Xw==,每次连接都不一样。服务器拿到它,在屁股后面拼上一串写死在协议里的字符 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,整体做一遍 SHA-1,再 base64,得出 Accept 回给你。

我照这个步骤自己手算了一遍,跟服务器回给我的 Accept 一个字节不差。注意这不是加密——那串字符是公开的,谁都能算。它的用意写在 RFC 里:防止路上那些老代理服务器把升级请求当普通 HTTP 响应缓存下来捣乱,顺便确认对面是真懂 WebSocket 的服务器,不是被误导的普通 HTTP 服务。说白了是对暗号,不是防窃听。真要保密,用 wss,把 WebSocket 装进 TLS 里跑,跟 https 一个道理。

连上之后说的不是人话,是帧

握手完,这条线上两头发的数据都叫「帧」。我让客户端发了一句「家里温度26.5」过去,服务器收到后回一句。拆开看,帧的最前面是两个字节的状态位:FIN(这帧是不是整条消息的最后一帧)和 opcode(这帧是什么类型)。常用的 opcode 就这几个:

opcode帧类型干什么用
0x1文本帧JSON 消息,最常见
0x2二进制帧画面、音频这类裸数据
0x8关闭帧礼貌告别,别直接拔线
0x9ping你还在吗
0xApong在

还有条规矩挺有意思:客户端发给服务器的每一帧都必须加掩码——正文前面带 4 个随机字节当密钥,内容逐字节跟它做 XOR,服务器收到再异或回去还原。服务器发给客户端的帧反倒不许掩码,相当于明着来。RFC 给的理由是防中间代理的缓存投毒。我实验里那句「家里温度26.5」就是被 4 个字节揉过一遍的,服务器解出来才认得。

ping 和 pong 是协议自带的心跳。麻烦的是,浏览器里的 JavaScript 根本发不了协议层的 ping,只能靠网页在应用层自己发个小 JSON,比如 {"type":"ping"},对方回一个,隔二三十秒一轮。为什么非心跳不可,往下看。

为什么手机一锁屏,这条线就悄悄死了

WebSocket 跑在 TCP 上。NAT 会话表那篇里算过账:路由器给每条连接记一笔,TCP 的条目闲置久了会被清掉。这条 WebSocket 长连接平时流量稀稀拉拉,正是最容易被清的那类。会话表条目一没,外面的服务器再推消息,路由器查无此连接,包直接扔——而且服务器未必知道,它会抱着一条「半开连接」继续发,发一个丢一个。所以两头都得没话找话,心跳的真正作用是给 NAT 表上那一笔续命,顺便确认对面还喘气。

手机锁屏更狠:系统直接把无线断了,TCP 连接物理上死亡。解锁之后网页脚本发现心跳超时,重连、重新握手、重新订阅——这整套容错是每个实时网页自己写的代码,协议不管。这也是为什么解锁手机回到 HA 页面,偶尔能看到所有数字齐刷刷刷新一次:它刚重连完。HTTP/3 那篇里 QUIC 的连接迁移能救换网断线,但 WebSocket 的主流实现还骑在老 TCP 上,手机从 WiFi 切到流量,这条线基本必断必重连。

家里谁在用它,怎么亲眼看到

顺手盘了盘,家里依赖这条线的还不少:

场景用它干什么
HA 的手机页面官方文档写明前端全走 /api/websocket,设备状态变了主动推
浏览器里看监控画面画面数据用二进制帧一路推过来,参见监控远程排查那篇里的实时链路
网页版聊天、在线客服新消息服务器直接弹过来
qBittorrent 下载页反例:老实轮询,每秒一问

想亲眼看看也不难,跟查缓存那篇用的是同一个入口:浏览器按 F12 打开开发者工具,Network 面板上有一档筛选叫 WS,专列 WebSocket 连接。打开 HA 页面点进去,能看到一条 status 是 101 的连接挂在那就是它;再点 Messages 或 Frames 标签,温度更新时一条条小消息往上蹦,全是服务器推的,浏览器一个请求都没发。要是 WS 那栏是空的,而 Network 列表里每秒冒一小撮请求,那这页面就是轮询派,跟 WebSocket 没关系。

收尾一句:一问一答是 HTTP 的本分,101 之后两头随便说话是 WebSocket 的特权。下回再看见网页里的数字自己跳,先按 F12 分清是它在问还是在推——问的看的是服务器脸色,推的才是真有一条不肯挂断的线。

展开全文


版权说明 手机扫码阅读
版权所有:《数巢笔记》 => 《WebSocket是什么意思?网页不刷新温度自己跳,我抓了个包,这条线握手完就再没挂断》
本文地址:https://www.shunot.com/lybk/1166.html
除非注明,文章均为 《SHUNOT》 原创,欢迎转载!转载请注明本文地址,谢谢。

发表评论

联系我们

在线咨询:点击这里给我发消息

微信号:master_135

工作日:9:00-23:00,节假日休息

扫码关注