WebSocket 不是“实时系统”的答案:理解连接、时序与背压
很多团队第一次认真讨论 WebSocket,通常不是因为他们想研究协议本身,而是因为系统里出现了几个很具体的诉求:
- 页面想收到服务端主动推送
- 用户操作之后,希望另一端立刻看到变化
- 轮询开始显得笨重,空请求越来越多
- 大家直觉上觉得:“要实时,那就上 WebSocket”
这条路走得太自然了,自然到很容易把一件事想简单:
既然
HTTP的问题在于“一问一答”,那只要换成“一直连着、双方都能说话”,实时性问题就差不多解决了。
我们后来踩坑,基本都从这个直觉出发。
所以这篇文章不讲 API,也不讲握手细节。我更想复盘的是:为什么这个直觉模型会吸引人,它从哪里开始失效,最后我们又是怎么把判断模型重新搭起来的。
一、先摆出那个最容易把人带偏的直觉模型
如果把很多人脑子里的 WebSocket 画成一句话,通常会是这样:
HTTP = 短连接 / 请求响应 / 不适合实时
WebSocket = 长连接 / 双向通信 / 适合实时
这个模型的问题不在于它完全错误,而在于它只描述了“通道形态”,却没有描述“系统语义”。
顺着这个模型继续往下想,很容易再推出三个看起来也很顺的判断:
- 连接一直在,所以延迟应该会更低。
- 底层是 TCP,所以消息可靠且有序,业务状态自然不会乱。
- 服务端能主动推,所以轮询带来的各种复杂度都没了。
这几个判断单看都不算离谱。
但如果把它们直接翻译成工程决策,后面就会连续遇到四类问题:
- 消息明明“按序到达”,页面状态却越来越旧
- 服务端明明“发成功了”,业务上却还是丢动作
- 连接明明“没断”,用户却已经失去有效会话
- 长连接明明“少了很多请求”,系统压力却反而更难控
也就是说,问题并不在 WebSocket 本身,而在于我们一开始把“连接持续存在”误当成了“业务持续正确运作”。
二、为什么系统会自然走到 WebSocket
如果只看表象,WebSocket 的出现确实像是在修补 HTTP 的天然别扭。
假设有一个页面,希望尽可能快地看到服务端状态变化。
如果我们用轮询,判断会很直接:
- 每隔
T秒请求一次 - 服务端有变化就返回
- 没变化也得回一个“没有变化”
这时延迟上界大致会变成:
数据变更到用户看到 ≈ 轮询间隔 T + 一次 RTT + 服务端处理时间
于是我们会自然做两件事:
- 想让页面更新更快,就把
T调小 T一调小,空请求和连接开销就上来
如果继续往下推,就会发现轮询的问题并不只是“浪费几次请求”,而是谁有话要说这件事,需要不断重新协商。
客户端每次都在问:“现在有东西了吗?”
服务端哪怕明明没有事,也要被迫回答一次。
顺着这个推导,长轮询会出现。
长轮询比短轮询聪明一点:没有新消息就先挂着,等有变化再回。
但它仍然保留着一个前提:主导节奏的还是请求,而不是连接。
当系统开始出现下面这些特征时,WebSocket 就会变得很有吸引力:
- 会话持续时间长
- 服务端主动推送很多
- 客户端也会频繁上行
- 单次消息很轻,但交互次数很多
推到这里,WebSocket 的价值会慢慢清楚一些。
它更像是在解决这个问题:
当双方都可能随时说话时,别再让每次发言都重新走一遍“请求是谁发起的、响应什么时候结束”的协商。
所以如果一定要先压一句话,我现在更愿意把它理解成:
WebSocket解决的不是“实时”本身,而是“高频双向协作时,反复协商一次性请求边界”的成本。
这个说法一变,后面的判断就会稳很多。
因为一旦承认它解决的是“协作成本”,而不是“时效保证”,我们就会继续追问:那时效、顺序、可靠性、背压,到底是谁负责?
三、直觉模型是怎么一步步失效的
1. “有序到达”为什么不等于“状态正确”
我们一开始很容易有一个朴素判断:
WebSocket跑在TCP上TCP保证字节流有序- 那我按发送顺序处理消息,状态自然不会乱
这个推理的问题,在“消息”和“状态”没有区分开的时候不明显。
但只要系统里传的是高频状态更新,它就会开始失效。
比如一个在线白板、一个行情面板,或者一个设备监控页面。
服务端连续产生三个状态:
S1: 温度 30
S2: 温度 31
S3: 温度 32
如果网络和消费速度都很好,按顺序到达当然没有问题。
但只要接收端稍微慢一点,或者中间出现一点积压,事情就变了:
队列里先堆着 S1,再堆 S2,再堆 S3
客户端忠实地按序处理
结果用户先看到 30,再看到 31,最后才看到 32
这里协议其实没有做错任何事。
它确实维持了顺序。
可业务真正关心的,往往不是“旧状态有没有被完整经历一遍”,而是“此刻界面是不是尽快收敛到最新状态”。
推到这里就会发现:
当数据本质上是“可覆盖状态”时,严格按序重放旧状态,反而可能变成时效性的敌人。
于是原来的模型就得改:
- 对“事件”来说,按序往往很重要
- 对“状态”来说,最新值往往比完整回放更重要
如果这两类东西在同一条 WebSocket 通道里被一视同仁,系统迟早会在“完整性”和“新鲜度”之间打架。
2. “发送成功”为什么不等于“业务成功”
第二个很常见的误判,是把通道层成功,当成业务层成功。
我们以前写过一种很顺手的逻辑:
服务端写出消息 -> 底层没有报错 -> 认为消息已经送达
这在日志里看起来很合理,因为你确实“发出去了”。
但继续往下拆,会发现里面至少隔着三层完全不同的语义:
- 字节是否离开了发送端应用进程
- 字节是否进入了对端内核缓冲区
- 对端应用是否真的解析、校验并处理了这条业务消息
WebSocket 能帮你走通前两层。
第三层,它并不替你定义。
这时如果消息是“刷新页面上的在线人数”,问题可能不大。
可如果消息是“扣减库存”“撤销订单”“开始执行任务”,事情立刻就敏感起来。
因为你需要知道的不是“线通不通”,而是“业务动作到底有没有落地”。
所以后来我们才慢慢把两个东西拆开:
- 传输确认:链路有没有明显失败
- 业务确认:对端是否按预期处理了这条命令
一旦这两个确认被混成一个,WebSocket 看上去很顺,系统语义却会变得很虚。
3. “连接还活着”为什么不等于“会话还有效”
长连接另一个非常会骗人的地方,是它特别容易让人产生“只要不断,就说明双方还在”的错觉。
可线上环境里,连接活着这件事远没有想象中单纯:
- 移动网络切换了,旧连接可能变成半开状态
- NAT 映射过期了,双方都以为自己还连着
- 浏览器标签页挂起了,应用层心跳节奏被打乱
- 服务端实例重启了,TCP 断开只是最轻的一种情况
如果系统拿“socket 还没报错”去等同“用户会话仍然有效”,就很容易出现这种怪事:
- 页面上显示还在线,但消息已经不再可靠到达
- 服务端还保留订阅关系,但用户其实早就切网了
- 重连之后新旧会话重叠,状态同步开始打架
所以继续往下推,会发现长连接系统一定会长出额外的一层:
- 心跳不是为了“显得完整”
- 重连不是为了“恢复 TCP”
- 会话恢复也不是为了“重新连上”
这些动作真正要解决的是:
当连接这件事不再是一问一答,而是持续存在时,系统必须自己判断“谁还活着、谁该作废、状态应该从哪里继续”。
4. “少请求”为什么不等于“更省系统资源”
很多人选择 WebSocket 时还有一个很自然的成本模型:
轮询 = 很多请求
WebSocket = 一条长连接
一条总比很多条便宜
这句话只在“只统计建连次数”时成立。
可只要把资源账本算全,长连接的成本结构就完全不是那个样子。
每个在线连接背后,通常都至少挂着这些东西:
- 读写缓冲区
- 定时器和心跳状态
- 用户身份与订阅关系
- 待发送消息队列
- 连接所属节点上的内存占用
如果消息很稀疏,这些状态大多只是安静地躺着。
可一旦进入推送高峰,最先膨胀的通常不是 CPU,而是每条连接上的等待发送数据。
于是你会看到一种很反直觉的场景:
- 请求量比轮询时期少了
- 服务端 QPS 看起来也没爆
- 但内存持续上涨,尾延迟越来越差,慢客户端越来越多
推到最后,问题会落在一个更本质的地方:
WebSocket没有让压力消失,它只是把压力从“请求洪峰”转移成了“连接生命周期管理”和“持续流量不匹配”。
四、极端情况一来,问题会暴露得更快
上面的推导在普通网络里已经成立。
一旦把场景推到边界条件,很多误判会更明显。
1. 高延迟:连接在,交互却还是慢
很多人第一次遇到高延迟网络,会下意识说:
“都上 WebSocket 了,为什么操作反馈还是拖?”
如果继续拆,会发现高延迟从来不是“有没有长连接”能消掉的。
假设一个业务交互是这样的:
客户端发一个动作 -> 服务端校验 -> 服务端回一个确认 -> 客户端再允许下一步
这本质上仍然是一个应用层的请求-响应回路。
只要这个回路存在,每次交互就仍然受 RTT 约束。
也就是说,WebSocket 省掉的是反复建边界的成本,
它并不会把“跨网络往返一次”变成“本地函数调用”。
高延迟一来,两个问题就会被放大:
- 心跳超时阈值如果还按局域网经验设置,就会误判掉线
- 如果通道里还堆着大量旧状态,真正重要的新状态到达得更晚
所以在高延迟场景里,系统更该关心的通常不是“有没有长连接”,而是:
- 哪些消息必须一来一回确认
- 哪些消息可以乐观更新
- 哪些旧状态应该直接被覆盖掉
2. 高丢包:可靠传输有时会把旧消息保护得太好
WebSocket 跑在 TCP 上,这通常会带来一种安全感。
但在高丢包场景里,这种“可靠”有时会把问题换个形式放大。
因为只要底层有一段数据没到,后面的数据哪怕更“新”,也得先等前面的缺口补上。
这就是为什么很多高频状态流在丢包时,会出现一种很别扭的体验:
- 不是彻底收不到
- 而是先卡一小段
- 然后突然补回来一串已经过时的消息
如果业务传的是聊天消息、审计日志、交易指令,这种保护通常值得。
可如果业务传的是鼠标轨迹、光标位置、行情瞬时快照,这种保护就未必还划算。
因为用户需要的不是“旧轨迹绝不缺一段”,而是“现在的位置尽快准确”。
推到这里,问题就不再是“WebSocket 靠不靠谱”,而会变成:
对当前这类数据,完整性和新鲜度,到底谁排在前面?
只要这个优先级没想清楚,技术选型就会一直摇摆。
3. 高负载:真正先爆的常常不是协议,而是扇出
WebSocket 一旦进入高并发场景,很多团队一开始盯着的是“连接数上限”。
可线上更容易先出问题的,往往是扇出成本。
比如某个热点房间有十万订阅者。
服务端来了一个更新,如果做法是“来一条推一条,给每个连接都写一份”,那系统压力会迅速变成:
一次业务更新 -> N 条连接写操作 -> N 份排队缓冲
如果更新频率再高一点,问题不会停在网络带宽上,还会继续往系统内部渗透:
- 事件循环更忙
- 节点之间负载更不均
- 某些热点连接的发送队列开始积压
- 连接粘在某个实例上,实例级热点变得明显
这时你会慢慢意识到,WebSocket 系统真正要管的从来不只是“连上多少人”,而是:
- 谁订阅了谁
- 一条消息要扇出到哪里
- 慢消费者出现时,压力在哪一层被拦住
4. 系统背压:慢消费者最终会逼你表态
背压这个词,很多文章一提就过去,但它恰好是 WebSocket 长连接系统最绕不开的问题。
只要生产速度长期大于消费速度,系统就一定得回答一个很现实的问题:
多出来的那部分消息,最后准备放哪?
通常只有四种去处:
- 放进内存队列里,等消费者慢慢追
- 阻塞生产者,让上游也慢下来
- 丢掉一部分消息
- 直接断开慢消费者
这四种没有一种是“协议自动帮你选好”的。
而且不同业务里,答案完全不同。
如果是交易指令,你通常不敢轻易丢。
如果是在线人数、设备状态、光标位置,你又很难接受无限排队。
如果是一个公共直播房间,你也不太可能让一个慢用户拖住整个上游。
也就是说,一旦系统进入背压,团队最终一定得对外表态:
- 我们更怕丢,还是更怕旧?
- 我们更怕阻塞全局,还是更怕牺牲局部用户?
- 我们愿不愿意让“跟不上节奏”的连接被淘汰?
很多 WebSocket 系统真正的稳定性分水岭,不在建连成功那一刻,而在这里。
五、工程 trade-off:不是“要不要 WebSocket”,而是“拿它承载什么语义”
如果只问“实时推送用什么”,答案很容易流于口号。
可只要把语义拆开,选择就会清楚很多。
方案一:轮询 / 长轮询
如果业务满足下面几个条件,轮询其实没有那么差:
- 更新不频繁
- 能容忍秒级甚至更长的陈旧
- 客户端主要是读,很少主动上行
- 希望尽量复用现有 HTTP 基础设施、缓存和鉴权链路
这时继续用轮询,系统虽然不优雅,但边界很清楚。
服务端不用长期维护大量会话状态,负载均衡也更简单,排障手段也更成熟。
它的问题也很明确:
一旦交互密度升高,请求协商成本会越来越像系统噪音。
所以不选它,通常不是因为“它落后”,而是因为它让高频协作这件事显得越来越笨重。
方案二:SSE
如果业务本质是“服务端持续推,客户端偶尔用普通 HTTP 上报动作”,SSE 往往比很多人想象中更合适。
顺着语义来看,它有一个天然优势:
- 既然主要是单向推送,那就没必要为了“可能的双向”引入一整套双向连接管理复杂度
也就是说,如果你只是做通知流、日志流、状态流,客户端上行并不重,
那 SSE 往往比 WebSocket 更贴合问题本身。
不选 SSE 的原因也很具体:
- 客户端需要高频上行
- 双向交互需要共享一个连续会话
- 你不想把“推送”和“上报”拆成两套通道管理
方案三:WebSocket
当系统满足下面这些条件时,我们通常才会更坚定地选 WebSocket:
- 双向都很活跃
- 会话持续时间长
- 交互很多,而且消息普遍很轻
- 我们愿意显式承担连接管理、会话恢复、背压控制这几类复杂度
注意这里最关键的一句不是“它最先进”,而是“我们愿意承担它暴露出来的复杂度”。
因为 WebSocket 真正换来的,不只是能力,还有责任:
- 你要自己定义业务确认
- 你要自己决定慢消费者怎么处理
- 你要自己把状态流和事件流分开
- 你要自己做断线恢复和重同步
所以后来我们在很多系统里更倾向于一个更保守的判断:
只有当“持续双向会话”本身就是业务核心,而不是一种实现偏好时,
WebSocket才值得被选上来。
在 WebSocket 内部,还要再做一次 trade-off
真正难的地方还不止于“选不用它”,而在于“用了之后,怎么承载消息”。
我们至少试过两种做法。
第一种做法是最顺手的:
所有变化都当事件
按产生顺序逐条发送
客户端按到达顺序逐条应用
它的好处很诱人:
- 模型简单
- 调试直观
- 看起来最“可靠”
但它后来暴露的问题也最集中:
- 高频状态会把旧值一条条排队
- 慢消费者会不断积压
- 重连之后补历史成本很高
第二种做法,是后来慢慢修出来的:
把消息分成“命令 / 事件 / 状态”
命令重确认、重幂等
事件按序消费
状态允许合并、覆盖、降采样
它实现上更麻烦,因为通道不再是一个“什么都按同一规则处理”的盒子。
但越往后做,越会觉得这个复杂度是值得的。
因为真正复杂的不是代码,而是业务本身的语义差异。
如果系统不在设计上承认这种差异,复杂度也不会消失,只会在线上以积压、错序、假成功的形式出现。
六、一次很典型的错误实践,以及它是怎么被修正的
我们以前做过一种非常典型、也非常容易写出来的 WebSocket 方案:
数据库状态有变化
-> 服务端把变化事件原样推给客户端
-> 客户端收到什么就按顺序改本地状态
-> 写出去没报错就算成功
-> 谁慢谁自己追
刚开始这套方案甚至看起来挺漂亮:
- 没有轮询
- 页面刷新很快
- 开发量也不大
问题是流量一上来,系统就开始出现三种很烦的现象:
- 页面偶尔会“回跳”
新状态刚展示出来,旧消息又把它覆盖回去。 - 某些用户越挂越慢
发送队列积压之后,他们收到的都是越来越旧的消息。 - 重连之后风暴明显
因为之前没有版本和恢复点,只能粗暴全量同步。
后来回头看,这个方案的问题并不是“写得不够仔细”,而是它把三个本来就不同的问题硬压成了一个问题:
- 状态同步
- 事件通知
- 命令执行
修正过程反而不是一步到位,而是被线上现象一点点逼出来的。
第一步,我们先给状态加了版本号。
这样客户端至少可以知道:如果后来的消息版本更旧,就不该再覆盖当前状态。
第二步,我们开始允许某些状态型消息只保留最新值。
比如在线人数、设备状态、光标位置,这些都不值得在发送队列里无限排队。
第三步,我们把命令的“发出”和“处理完成”分开。
服务端写出去只代表链路没立即失败;真正的业务完成,要靠应用层回执、幂等键、超时重试策略来兜。
第四步,我们给每条连接设置发送队列上限。
一旦慢到超过阈值,要么降采样,要么踢掉重连,而不是让整台机器替它无上限垫内存。
第五步,我们在重连后优先恢复“最新快照”,而不是盲目补整段历史。
因为对很多状态型页面来说,追旧账并不能带来更正确的结果,只会延长恢复时间。
如果把这次修正压成一句话,后来越来越像这样:
我们不是在修一个协议实现,而是在承认:不同语义的数据,根本不该共享同一种传输策略。
七、最后沉淀出来的,不只是 WebSocket 经验,而是一个通用判断模型
后来再看 WebSocket,我们越来越少问“这个技术适不适合实时”,而会先问下面五个问题。
这五个问题不只适用于 WebSocket,拿去看 SSE、消息队列、gRPC stream,甚至很多异步系统都一样有用。
1. 你传输的到底是“事件”,还是“状态”
如果是事件,通常更怕丢、更怕乱。
如果是状态,通常更怕旧、更怕排队。
这一步不分清,后面所有“可靠性”讨论都会混乱。
2. 业务真正优先的是“完整性”,还是“新鲜度”
只要完整性排第一,重传、按序、回放就更值得。
只要新鲜度排第一,合并、覆盖、降采样、丢旧保新就会更重要。
很多技术争论,本质上都只是这两个优先级没有先说清楚。
3. 当生产速度大于消费速度,谁来为差额买单
是排队,是阻塞,是丢弃,还是断连。
系统迟早得选一个。
如果设计阶段不选,线上最终也会替你选,只是那时选项通常最差。
4. 你说的“成功”,到底停在哪一层
- 发出成功
- 送达成功
- 处理成功
- 持久化成功
这几个如果不拆开,重试、幂等、告警都会变得非常模糊。
5. 断连之后,你想恢复的是“连接”,还是“语义”
有些系统只需要重建通道。
有些系统需要恢复订阅。
有些系统要从某个版本继续。
还有些系统根本不值得补历史,直接给最新快照就够了。
这一步想清楚之后,再去看任何通信技术,心里都会稳很多。
因为你不再是在问“它支不支持长连接”,而是在问“它把哪类复杂度暴露给了我,而我愿不愿意接住”。
八、复盘到最后,WebSocket 更像什么
如果还沿用最早那个直觉模型,WebSocket 很容易被理解成:
让系统变实时的一根网线
可只要把前面的推导走完,那个印象就会慢慢变掉。
它更像是一种持续协商中的会话通道:
- 它让双方不用每次重新建立一次性请求边界
- 它适合承载持续、频繁、双向的交互
- 它把很多原本藏在请求-响应模型背后的问题,直接摊到系统设计面前
也正因为这样,WebSocket 既不该被神化,也不该被轻视。
如果业务核心真的是持续双向协作,它会非常顺手。
可如果团队只是把它当成“实时性开关”,那它多半不会消掉复杂度,只会把复杂度从表面挪到更深的地方:
- 从请求数,挪到连接管理
- 从接口设计,挪到消息语义
- 从单次响应时延,挪到持续背压治理
所以后来再有人问我,WebSocket 到底值不值得上,我心里的起点通常已经不是“它快不快”。
我更想先追问一句:
你想解决的,到底是“请求太频繁”,还是“系统真的需要一条持续、双向、可管理的会话通道”?
这两个问题表面很像,工程答案通常完全不同。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)