首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
WebSocket 握手到底发生了什么?101 之前的三道坎
📅 2026/10/28 2:05:22
✍️ 爱科研究院
👁 阅读 3,247
WebSocket 握手到底发生了什么101 之前的三道坎上个月给一个内部工具加实时推送本地跑得好好的部署到测试环境就报WebSocket connection to wss://…/ws failed。浏览器控制台就这一行没有状态码没有原因。查了快两个小时最后发现是网关没透传Upgrade头——代码一行没错配置少了两行。那次之后我把握手这一整套流程完整捋了一遍才发现 101 之前其实有三道坎每一道挂了都只给你一句「连接失败」。这篇从报文层面讲清楚握手怎么完成再讲三道坎分别卡在哪。一、握手本质上是一次「协议升级申请」WebSocket 没有发明新的端口也没有新的连接方式。它就是先走一条普通的 HTTP 连接然后在请求头里申请咱们换个协议聊聊。客户端发出去的东西大概长这样GET /ws HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13服务器如果同意回一个 101 状态码同时带上它算出来的Sec-WebSocket-AcceptHTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbKxOo这个 Accept 值不是随便给的。算法是固定的把客户端给的 Key 拼上一个固定字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11做一次 SHA-1再 base64。自己写一遍就明白了importbase64importhashlib GUID258EAFA5-E914-47DA-95CA-C5AB0DC85B11defcompute_accept(key:str)-str:sha1hashlib.sha1((keyGUID).encode(ascii)).digest()returnbase64.b64encode(sha1).decode(ascii)print(compute_accept(dGhlIHNhbXBsZSBub25jZQ))# s3pPLMBiTxaQ9kYGzzhZRbKxOo这个设计的目的很朴素证明服务端真的懂 WebSocket 协议而不是随便一个会把 HTTP 请求原样返回的东西。所以 Key 不需要保密也不需要随机到多安全它就是个「你懂不懂行」的握手暗号。101 一旦发出这条 TCP 连接就脱离了 HTTP 的语义后面跑的是 WebSocket 自己的帧格式。同一个端口、同一条连接协议换了。二、第一道坎中间层不认 Upgrade这是线上最常见的一道也是我踩的那次。WebSocket 依赖两个头Upgrade: websocket和Connection: Upgrade。而Connection是个逐跳头意思是「只对当前这一跳有效」——很多网关、负载均衡、CDN 在转发时会把它干掉或者干脆不认 Upgrade 这个动作。结果就是客户端发的是升级申请服务端收到的是个普通的 GET回 200 或者 400浏览器看到不是 101直接判定失败。Nginx 里必须显式打开location /ws { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; # 1.0 不支持 Upgrade proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; # 默认 60s会切掉长连接 }三行里最容易漏的是第一行。proxy_http_version默认是 1.0而 HTTP/1.0 压根没有 Upgrade 机制头配得再对也没用。我那次就是漏了它。三、第二道坎鉴权抢在升级之前第二道坎比较隐蔽路径和鉴权顺序。有些框架把 WebSocket 端点挂在普通路由上中间件先跑鉴权。如果 token 放在 query 里、或者放在Sec-WebSocket-Protocol里而鉴权中间件只读Authorization头就会直接回 401。浏览器的 WebSocket API 有个硬限制不能自定义请求头。你没法像 fetch 那样加Authorization。所以常见做法是把 token 放 query string或者用子协议字段传。这不算优雅但确实是现实约束。排查的时候有个笨办法特别有效用 curl 手动发一次握手请求看服务端到底回了什么。curl-i-N\-HConnection: Upgrade\-HUpgrade: websocket\-HSec-WebSocket-Version: 13\-HSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ\http://127.0.0.1:8000/ws?tokenxxx回 101 说明服务端没问题问题在中间那层回 401 就是鉴权回 400 多半是头不全。一条命令把责任范围切干净比在浏览器里猜快得多。顺手整理一张状态码对照表遇到的时候对号入座返回码常见原因先查哪里400升级头不全或Sec-WebSocket-Version不是 13网关是否透传、客户端版本401 / 403鉴权没通过或 Origin 校验拒绝token 传递方式、允许的来源列表404路径不对或网关前缀没剥掉location 配置、应用路由426服务端要求升级但没完成握手是否启用了 WebSocket 模块502 / 504后端没起来或者握手超时进程状态、上游地址还有一个容易被当成「跨域问题」的情况有些框架默认校验Origin头只允许白名单里的来源。本地调试用file://打开页面时Origin 是null直接被 403。这个跟浏览器的同源策略没关系是服务端自己的安全策略去配置里加白名单就行。四、第三道坎握手成功但帧走不通101 拿到了连接建立然后消息发不出去。这类问题出在帧这一层。WebSocket 的帧头里有几个关键位opcode 标记这是文本、二进制还是控制帧MASK 位标记是否掩码payload 长度分 7 位、16 位、64 位三种编码。有个规则经常被自研协议的人忽略客户端发给服务端的帧必须掩码服务端发给客户端的帧必须不掩码。反了的话对端会直接断开而且不同实现的报错还不一样有的给 1002有的直接 TCP 断。如果你用的是成熟库Python 的websockets、aiohttpNode 的ws这些细节不用管。但只要你碰过一次自研帧解析就会明白为什么大家都说「别自己实现协议」。五、握手成功之后真正的麻烦才开始101 只解决「连上」不解决「一直连着」。长连接服务还有个容易低估的账容量。一个 WebSocket 连接占的内存不多服务端每连接几十 KB读写缓冲加连接对象一万连接也就几百 MB看着不吓人。真正卡住你的是文件描述符。Linux 默认单进程能开的文件描述符是 1024一条连接占一个。所以第一件事是改ulimit -n或者用 systemd 的LimitNOFILE抬到 65535 以上。这个坑我踩过压测到一千出头忽然开始报Too many open files日志里全是 accept 失败看着像网络问题其实是句柄耗尽。第二个是并发模型。同步框架每条连接占一个线程或进程几千连接就把内存吃干净。得用异步框架asyncio、Tornado、Node或者协程方案一条连接对应一个轻量任务上万才撑得住。第三个是发布时的连接迁移。服务端滚动重启挂着的老连接全断。能不能让用户无感取决于有没有「先摘流量、等连接自己迁走、再停进程」这套流程。这块不做每次发版都是一次小型事故。说完容量回到超时。长连接最容易被中间设备干掉因为它们判断连接是否还活着的标准很粗暴一段时间没有数据就认为你死了。Nginx 默认proxy_read_timeout 60s云厂商的负载均衡常见 300 秒或者 900 秒超过了直接 FIN。所以应用层心跳是必须的不能指望 TCP keepalive——那个默认两小时才探测一次来不及。importasyncioimportwebsocketsasyncdefkeep_alive(ws):whileTrue:awaitasyncio.sleep(25)# 小于各层最短空闲超时的一半pongawaitws.ping()# 控制帧不进业务队列awaitasyncio.wait_for(pong,timeout10)asyncdefmain():asyncwithwebsockets.connect(ws://127.0.0.1:8000/ws)asws:asyncio.create_task(keep_alive(ws))asyncformsginws:print(收到:,msg[:80])asyncio.run(main())两个细节值得说。一是心跳间隔要小于整条路上最短的那个空闲超时的一半60 秒的超时配 25 秒心跳比较稳。二是用 ping/pong 控制帧别用业务消息充当心跳——业务通道可能被限流、被顺序阻塞心跳就失去意义了。六、断线重连别写成死循环最后一件事重连。见过太多重连代码是while True: try connect except: sleep(1)。服务端一挂几千个客户端每秒重连一次直接把刚恢复的服务再打挂一次。这种事故我亲身经历过恢复用了四十分钟。正确姿势是指数退避加随机抖动importasyncioimportrandomasyncdefreconnect_loop(url,max_delay30):delay1whileTrue:try:asyncwithwebsockets.connect(url)asws:delay1# 连上了就重置awaitconsume(ws)exceptExceptionase:print(断开:,type(e).__name__)awaitasyncio.sleep(delayrandom.uniform(0,delay*0.3))delaymin(delay*2,max_delay)加了抖动之后几千个客户端的重连时间点会被打散到几秒的区间里不会形成尖峰。七、上线前的自测清单最后给一份我每次上线都会过的清单五分钟能跑完能挡掉大部分低级问题。用 curl 手发一次握手确认返回 101 而不是 200/400断网再恢复看客户端有没有按退避重连而不是死循环狂刷让连接空闲超过网关超时比如 90 秒确认心跳能撑住不断关掉一台后端实例看连接有没有平滑迁走压到预期连接的八成看文件描述符和内存有没有见顶服务端重启一次确认客户端能自己恢复而不是要用户手动刷新这六条里最容易被跳过的是第三条和第六条也恰恰是线上最容易出事的。写在最后WebSocket 的复杂度全在「连接之外」握手要过中间层那关帧要守规矩长连接要自己养着断了要聪明地重连。101 只是入场券。按我这几次踩坑的概率排出问题先查这三处网关有没有透传 Upgrade 和用 HTTP/1.1、空闲超时是不是 60 秒、客户端有没有真的发心跳。九成问题在这三条里。有别的坑欢迎评论区补充看到都会回。本文为原创技术分享涉及的实现思路仅供学习与合法用途参考。请遵守相关法律法规勿将技术用于任何违规场景。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/28 2:05:22
00 开篇词:为什么每个后端都该懂一点网关
2026/10/28 2:05:22
随机森林:让一群“小专家”帮你做决定
2026/10/28 2:05:22
企业级区块链实战:基于Hyperledger Fabric搭建商品溯源全链路系统
2026/10/28 5:26:06
电脑录屏快捷键全攻略:七类方案与效率实践
2026/10/28 5:26:06
强化学习最优贝尔曼公式原理与实践解析
2026/10/28 5:26:06
边缘智能体落地Arm架构的核心原理与工程实践
2026/10/28 5:26:06
AI空性自感:融合东方哲学的认知架构探索
2026/10/28 5:26:06
32位Win8.1内存只认3.25G?PAE补丁实战与避坑指南
2026/10/28 5:21:06
看图软件怎么选?批量识字OCR实测与工程化流程
2026/10/28 0:00:12
Stable Diffusion文生图全解析:从CLIP到UNet的扩散模型原理与实战
2026/10/28 0:00:12
从 Notebook 到 Python 项目:如何重构量化行情获取代码?
2026/10/28 0:00:12
2026版人形机器人标准:具身智能与模块化设计解析
2026/10/27 11:41:45
全国计算机二级C语言真题PDF高效刷题指南
2026/10/27 9:24:40
requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南
2026/10/27 12:03:45
递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现
2026/10/25 22:15:16
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/26 11:29:48
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/25 12:42:56
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)