资讯动态

HTTP不够用?一文说透WebSocket协议原理与生产实践

发布时间:2026/10/15 18:00:10 来源:尧图企业网站定制
你有没有遇到过这种情况页面上的在线状态总要刷新才更新聊天消息发出去之后要等一个响应才能看到自己发出去的内容后台任务跑完了页面却毫无反应。你开始怀疑HTTP协议是不是不够用了于是去查WebSocket协议可一看文档又愣住了既然HTTP已经能上网为什么还要发明一个WebSocket这个问题当年也困扰了我很久后来自己动手把一个轮询系统改造成长连接又因为连接断连、代理超时折腾了大半夜才算真正想明白。这篇文章不打算把RFC翻一遍抄给你就想聊聊HTTP的边界在哪、WebSocket到底补了什么以及上线之后那些文档里不会写的坑。1. 从HTTP的“一问一答”说起为什么实时场景这么别扭1.1 短连接、无状态与服务器的“闭嘴”困境HTTP协议从设计上就是一个“请求-响应”模型客户端先说话服务器才能回答。你发一个GET它回一个200你发一个POST它回一个JSON。整个过程里服务器永远是被动的一方它不能哪天一高兴就主动往客户端塞数据。就算HTTP/1.1加了keep-alive允许同一个TCP连接上连续发多个请求本质上还是“一个请求对应一个响应”顺序不能乱服务器也不能跨过客户端先开口。这就像对讲机你要先按住通话键说“在吗”对方才能松开按键回你。如果对方那边临时有事想主动联系你他没有办法只能等下一次你按键。可实时聊天、在线通知、协同编辑这类场景需要的恰恰是“电话模式”任何一方都可以随时说话不用等对方先开口。HTTP的“一问一答”结构在这一点上天然和实时场景拧着劲儿。另一个麻烦是HTTP协议本身是无状态的。上一次请求和下一次请求之间没有上下文服务器不记得你。虽然可以用Cookie、Token让请求带上身份但每次请求都要重新解释一遍“我是谁、我要什么”这些开销在普通接口上无所谓在动不动就一秒请求几百次的实时场景里就成了累赘。1.2 轮询、长轮询的代价看似能用实际烧钱既然服务器不能主动推那浏览器就想了个变通办法轮询。客户端每隔两秒发一个请求问“有没有新消息”服务器一看没有就回个空数据。这个方案写起来非常简单但是问题也很明显。先说延迟。如果轮询间隔是2秒那消息最坏情况下要等2秒才送到间隔改成500毫秒延迟倒是低了可每秒请求数量直接翻倍服务器得为大量“没有新消息”的空请求买单。对客户端来说每个HTTP请求都要走一遍建连、发头、等响应、断开的流程消耗带宽和电量在移动端尤其心疼。再说长轮询。长轮询的思路是客户端发一个请求服务器先不急着返回把它挂着等真有了新消息再一次性返回。这样消息一到就能立刻回来看起来实时性不错。但服务器要为每一个挂起的请求保存上下文连接数一多内存和线程压力立刻上来。更坑的是如果中间有代理或网关它们通常有自己的超时设置长轮询连接很可能在碰到数据之前就被静默掐断了。客户端还得处理“连接断了重新挂”的逻辑复杂度并不比WebSocket低多少。1.3 轮询和WebSocket的第一眼对比能力普通轮询长轮询WebSocket服务端主动推送不支持勉强支持原生支持消息实时性取决于轮询间隔基本实时实时双向无消息时的开销高大量空请求中连接挂起仍占资源低只有心跳连接复用每个请求可能新建连接每个请求挂起一个连接单条长连接实现复杂度最低中中但坑更多轮询不是不能用但如果你要做的是“服务器随时会主动通知客户端”的场景轮询本质上是在用HTTP的短处硬扛扛到一定规模不管性能还是代码复杂度都会很难看。这也是WebSocket被设计出来的直接动机。2. 一次“协议变身”的握手WebSocket到底从哪来的2.1 握手过程还是那个熟悉的HTTP很多人以为WebSocket是一套从头另起炉灶的协议这也是个误会。WebSocket在建立连接之前走的依然是HTTP流程只不过这个HTTP请求比较特殊。客户端会发一个GET请求包含几个关键头Upgrade: websocket告诉服务器“我想把当前连接升级成WebSocket协议”Connection: Upgrade说明这是一个逐跳hop-by-hop的头只影响当前连接Sec-WebSocket-Key一个随机生成的16字节值通常做Base64编码用于验证Sec-WebSocket-Version: 13指定协议版本。服务器如果同意升级会返回101 Switching Protocols同时带上Sec-WebSocket-Accept这个响应头。这个头的计算很有意思把请求里的Sec-WebSocket-Key拼上一个固定的GUID字符串做SHA-1哈希再Base64编码。这个固定值已经写死在RFC 6455里客户端收到后会用同样方式算一遍两边对得上才算握手成功。2.2 为什么要用Key为什么非要走HTTP升级这条路你可能想问握手就握手为什么要搞Sec-WebSocket-Key这么个东西它并不是身份认证更不是密码它的作用是防止缓存代理把这次请求当成普通GET给缓存下来同时也让服务器能证明自己确实理解WebSocket协议。如果中间某台设备傻乎乎地把101响应缓存成普通响应后续客户端和服务器的同一条TCP连接还继续跑两边情绪就完全错位了。更重要的是WebSocket选择复用HTTP而不是直接开一个新端口是经过深思熟虑的。现在的网络环境里防火墙、代理、负载均衡、反向代理几乎默认只对80和443端口网开一面。如果WebSocket自己去开个新端口很多场景下根本连不通。留在HTTP上它至少能像普通请求一样穿过大部分网络设备等握手结束后再切换到帧协议中间设备如果不理解升级机制顶多把它当成一个挂起的请求不会直接掐断。2.3 101状态码和Upgrade机制不是WebSocket发明的HTTP/1.1很早就定义了Upgrade头允许客户端询问服务器“能不能把当前连接切换到另一个协议”101 Switching Protocols就是这个过程的确认。比如有人用它升级到TLS、升级到其他实验协议。但绝大多数协议并没有把这个机制用起来真正让Upgrade成为话题中心的就是WebSocket。所以可以这么理解WebSocket不是从石头缝里蹦出来的新协议它借用了HTTP的门进入房间然后在房间里跑起了自己的游戏规则。握手那几十毫秒是HTTP后面的数据流就和HTTP没有关系了。这一点特别重要因为如果你把WebSocket数据直接丢给HTTP解析器解析器一定会懵因为它压根不是HTTP格式。3. 握完手之后WebSocket在传什么3.1 帧结构消息边界怎么标记的握手结束之后连接上传输的内容就变成了一串帧frame。帧是WebSocket的最小通信单位所有用户消息、控制信号都封装在帧里。帧头不算复杂但你看了之后会对协议设计者的思路更有感觉。帧头最低只要2字节。第一个字节的低1位是FIN表示这是不是消息的最后一帧紧接着3个预留位RSV1到RSV3默认是0只有启用扩展时才用再往后的4位是opcode告诉你这个帧干什么用比如文本帧是0x1二进制帧是0x2关闭帧是0x8Ping是0x9Pong是0xA。第二个字节最高位是MASK表示payload是否被掩码处理低7位是payload长度如果长度不够还会延续到后续字节。这种设计解决了一个HTTP里可能比较绕的问题消息边界。HTTP通常靠Content-Length或chunked编码来确定body在哪结束而WebSocket直接在帧头里标出每帧长度解析器读起来很清爽。文本消息、二进制消息还能通过opcode区分对聊天里的表情包、图片上传、二进制协议交互都很方便。3.2 为什么客户端发消息必须掩码服务器却不用WebSocket协议里有一条规则经常让人费解客户端发给服务器的帧必须设置MASK位并用4字节随机掩码对payload做逐字节异或而服务器发给客户端的帧则禁止掩码。为什么这么不对称不是为了加密也不是为了省那几字节开销而是为了防一种老式安全问题。WebSocket常常要穿过代理设备如果某台代理不理解WebSocket协议却还会把网络流量当作HTTP来解析那么客户端发出来的内容就存在被误判为HTTP请求或响应头的一部分的风险。掩码让payload变成伪随机的极大降低真实内容被中间设备当作可预测HTTP语法读取的概率。这个设计带着浓厚的“历史教训”味道属于那种不在生产环境吃几次亏你很难体会得到的细节。3.3 分片、Ping/Pong和优雅关闭一个大消息可以拆成多个帧发送。发送端在不知道总长度时可以先发一个FIN0的帧最后再发一个FIN1的帧收尾中间还可以穿插控制帧比如Ping。这样设计有一个很实际的好处一帧很大的文本不会一直霸占连接导致心跳发不出去控制帧可以插队。Ping/Pong是连接保活的重要手段。协议规定收到Ping必须回一个PongPong的payload要和Ping一致但协议并没有规定心跳必须多久发一次也没有规定必须发所以这个任务落到了应用层头上。很多人上线后才发现连接明明“活着”但发消息就是没反应十有八九是中间链路已经把空闲连接断掉了而自己从来没发过心跳。关闭也有规范。主动关闭的一方要发一个Close帧带上状态码和原因等待对方回Close再关TCP。如果你不管三七二十一直接断TCP对端只能看到一次异常断开状态码通常是1006排查问题时很难判断是网络故障还是服务端主动干掉你的。4. HTTP轮询、SSE、WebSocket到底选谁4.1 账本延迟、带宽、连接数的一次真实对比拿一个在线聊天室来说假设同时在线1000人。如果采用每2秒一次的轮询每秒钟就是500个HTTP请求每个请求带上几百字节的头一天下来光是请求头就有上百GB的数据被白白消耗。就算服务器响应很快这些高频空请求也会占用大量CPU、内存和日志空间。换成WebSocket之后每个用户只维护一条长连接有消息服务器直接推没消息就静默或心跳服务器的负载主要体现在“挂住的连接数”上而不是“每秒请求数”上。单条连接占用的内存其实不多现代服务器同时挂几十万条长连接是可行的。所以从总资源账单来看在高频实时场景下WebSocket往往比轮询更省而不是更费。4.2 什么场景值得上WebSocket什么场景别凑热闹WebSocket不是银弹。如果一个接口就是“请求一次、返回一次”的普通查询用HTTP是最合适的。HTTP有成熟的状态码、缓存机制、代理缓存、CDN加速这一整套生态WebSocket都不具备。硬给REST接口套WebSocket只会让调试、日志、监控都变得更加别扭。真正适合WebSocket的是这几类聊天、站内信、在线通知服务器随时可能推送客户端也要频繁发送。实时看板、行情报价高频数据需要低延迟到达。协同编辑、白板、在线游戏双方需要持续交换增量数据。远程控制、设备调试指令是双向的且每条指令都要求低延迟。反过来只做单向服务端推送的页面条件允许时可以考虑SSEServer-Sent Events。SSE走的是普通HTTPAPI简单自带断线重连和浏览器原生事件配合很好只是它只支持服务器往客户端推不支持客户端通过同一条连接反向发送任意消息。如果你只有单向广播需求非要去上WebSocket倒也不能说不行但有点用大炮打蚊子。4.3 HTTP/2、SSE能替代WebSocket吗有人会问HTTP/2不是全双工吗是不是可以替代WebSocketHTTP/2确实支持在一个TCP连接上多路复用多个流双向收发数据但它的语义仍然建立在“请求-响应”之上服务端要主动给客户端推送一条消息要么走客户端发起的请求要么依赖曾经被设计出来又废弃掉的Server Push。现在主流浏览器对Server Push的态度已经冷淡了更常见的做法是结合SSE用。SSE和WebSocket之间也不是替代关系而是两个互补的工具。SSE实现简单、自动重连、能穿任意HTTP代理适合行情、日志流、AI生成结果这类单向推送WebSocket适合需要双向实时交互的场景。真正有意思的是WebSocket本身也可以跑在HTTP/2之上草案已经出来了可以把多个WebSocket会话复用到同一条HTTP/2连接上。不过目前实际生产中还是以经典的WebSocket over TCP为主等哪天浏览器和网关的兼容性上来这会是另一个大话题。5. 生产环境里的WebSocket代理、超时与心跳的那点坑5.1 代理空闲超时导致的“假死”我自己的第一个生产级WebSocket服务上线之后很快就接到反馈页面挂一段时间就不实时了刷新页面又正常。当时第一反应是代码有bug后来一查问题根本不在业务代码而在层层的代理和负载均衡。很多代理产品对空闲TCP连接都有默认超时比如60秒内没有任何数据传输就主动断开。WebSocket建立之后通常不会一直在传数据用户可能好几分钟没任何操作此时连接在代理看来就是“空闲”的被掐掉也毫不知情。客户端那边连接还没感知到异常就会出现“以为还在线实际已经断了”的假死状态。排查这种问题有个很实用的路径先看客户端有没有触发心跳再看代理日志里有没有断开记录再用抓包工具看链路有没有FIN包。只要把断开的时间点和日志对上基本就能判断是空闲超时还是代码逻辑问题。5.2 升级请求容易在代理层被过滤WebSocket握手依赖Upgrade和Connection这两个头但在很多代理配置里这两个头默认不会被原样向后传递。如果你只是把普通HTTP请求正常转发WebSocket握手就会在代理那一层被当成普通请求处理拿不到101响应。常见做法是在代理配置里显式放行Upgrade和Connection同时把读超时时间调长别让代理在握手成功后的几秒内又帮你把连接断掉。还有一种容易被忽略的情况HTTP/2协议本身不支持传统的Upgrade机制如果你的代理在最外层是HTTP/2模式那WebSocket升级很可能不生效。解决思路是让WebSocket握手走HTTP/1.1再考虑内层转发。上线之前先和运维确认代理版本、超时参数、是否开启WebSocket支持能省一整天的排查时间。5.3 心跳频率差几秒都可能出事心跳间隔怎么定我一直觉得是门玄学。太密集了浪费带宽、费电特别是移动App后台还被频繁唤醒用户看一眼就想卸载你太稀疏了又刚好卡在代理超时的边缘一个网络抖动就断线。比较通用的做法是让心跳间隔小于代理空闲超时的一半到三分之二。比如你发现链路中最短的空闲超时是60秒那心跳可以设置为30到40秒。同时不要只依赖“发出去”就算完成还要看Pong能不能及时回来。我通常会在Ping发出去之后设置一个超时计时器比如10秒没等到Pong就判断这条连接已经不可用主动关闭然后重连。这样能避免客户端一直握着一根死连接不放。重连策略同样重要。WebSocket没有自动重连你必须自己写逻辑。直接断线立刻重连效果最差服务器压力大的时候容易引发“重连风暴”。推荐指数退避第一次1秒第二次2秒第四次8秒最多到30秒或60秒每次加重试的时候加一点随机抖动。重连成功之后客户端还要补拉一下离线期间的消息否则连接恢复了消息却丢了一截。5.4 关闭连接别直接断TCP服务端需要下线连接时尽量先发Close帧设置合理的错误码比如正常关闭1000或者根据业务原因处理。不要粗暴地直接关闭底层socket那样客户端只会看到一个异常断开无法区分“服务器主动不要你”和“网络炸了”。客户端收到Close事件时也要根据错误码决定是提示用户还是安静重连。如果客户端判断是网络异常导致的断连比如错误码1006自动重连是合理的如果服务器明确返回关闭原因比如认证失效、被踢下线就不该无限重连否则会出现“刚被踢下去又自动连回来”的诡异现象。这些判断虽然简单但代码里没写完整的话用户能把你骂到怀疑人生。最后分享一点我的实际习惯我现在做实时网关一定不会把心跳间隔、代理超时、重连退避这些参数写死在代码里而是全部放进配置中心。不同环境的代理设置可能完全不同本地开发时超时无所谓到测试环境就断到生产又是另一个值。每次联调前我都会先问运维一句中间这层设备的空闲超时是多少这句话帮我省了很多冤枉加班。WebSocket并不神秘它只是把HTTP的接力棒接过来跑完最后那段实时赛道。想用好它关键不是背帧格式而是摸清楚你这条连接从客户端到服务器中间到底要穿过哪些“热心肠”的代理它们会不会在沉默中悄悄把线剪断。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑