
1. 先说结论这两个实时通信方案到底是“表面兄弟”还是“真对手”我做后端也有十年了SSE和WebSocket这组对比每隔一段时间就会被拉出来讨论一次。尤其是最近大模型AI应用铺开之后SSE的热度肉眼可见地涨了一大截很多技术群和热搜词里都在问“SSE能不能替代WebSocket”“大模型流式输出到底该用哪个”甚至有人直接用“SSE vs WebSocket”当作选题来写技术测评。先把态度放这里这两个不是谁替代谁的关系而是各自站在实时通信的不同位置上。SSE的本质是“基于HTTP的单向流式推送”WebSocket的本质是“基于TCP的全双工长连接通道”。一个适合服务端向客户端持续推送文本流一个适合高频双向交互和二进制传输。你把它们当成两个工具看而不是两个打架的候选人很多选型纠结就迎刃而解了。这篇内容我会从协议原理出发结合大模型流式输出、Django后台推送、SpringBoot整合、常见的超时断连问题、WebSocket调试工具等实战场景把两者的机制差异、落地步骤和踩坑记录全部拆开讲。适合谁看后端开发、前端工程师、做AI应用对接的技术同学以及正在纠结实时方案选型的技术负责人。看完之后你至少能回答三个问题什么场景用SSE、什么场景用WebSocket、遇到连接断开时该查哪里。2. 原理与机制拆解为什么SSE是“半双工但省心”WebSocket是“全双工但费劲”2.1 从HTTP轮询到长连接这条演进线决定了各自的基因要理解SSE和WebSocket先得看它们从哪来。在没有WebSocket之前浏览器要实现“服务端有变化就通知前端”只能用轮询或者长轮询。轮询就是前端定时发请求问“有数据吗”长轮询是前端发请求后服务端挂起这个请求直到有新数据才返回。这两种方案在WebSocket出现之前占了很长时间但问题很明显轮询的无效请求太多长轮询的挂起机制在并发场景下对服务端压力很大。WebSocket的思路是彻底换一条路客户端发一个HTTP握手请求服务端返回101 Switching Protocols之后双方就在同一个TCP连接上自由收发消息不再受HTTP请求-响应模型的约束。这条路解决了双向实时通信的问题但也带来了新的复杂度——连接需要自己维护心跳、自己处理重连、自己管理会话状态。SSE则是另一条更“保守”的路径。它不改变HTTP协议本身服务端把响应头设置成Content-Type: text/event-stream然后在这个响应体内持续写入数据片段客户端用EventSource接口去读取。代价是它是单向的服务端可以向客户端推数据客户端没法在同一条连接里反向发送数据要发给服务端就得另外发普通HTTP请求。好处是它保留了HTTP的所有特性自动重连、断点续传、事件ID、自定义事件类型这些能力WebSocket都要自己实现。2.2 SSE工作机制EventSource、自动重连、事件ID是怎么配合的SSE的协议格式非常简洁。服务端返回的响应头里有Content-Type: text/event-stream响应体的每一行遵循约定格式data:表示数据内容多个data行拼成一条完整消息event:表示事件类型没有这个字段默认是messageid:表示消息ID客户端断开重连时会带上Last-Event-ID头retry:表示重连间隔毫秒数以:开头的行为注释行常用于制造心跳前端用起来也简单const source new EventSource(/api/news/stream); source.onmessage (event) { console.log(收到消息:, event.data); }; source.addEventListener(customEvent, (event) { // 监听自定义事件类型 }); source.onerror () { // 连接异常时会自动重连EventSource内部帮你处理了 };这个自动重连是SSE最省心的点。WebSocket断线之后你得自己写重连逻辑、退避策略、同步补偿但EventSource在浏览器层面把这件事做了而且支持基于Last-Event-ID的断点续传。也就是说服务端推送到第100条时连接断了重连后浏览器会带上Last-Event-ID: 99服务端看到这个头就知道从第100条继续推。但SSE有一个让不少前端同学头疼的限制普通EventSource无法设置自定义请求头。比如有些服务端接口要求带Authorization认证头原生EventSource做不到。所以现在很多大模型AI应用里大家宁可用fetch配合ReadableStream手写一套SSE客户端也不愿意用EventSource就是为了能自定义请求头、支持POST请求、方便做中断控制。这块后面讲AI场景会详细展开。2.3 WebSocket工作机制从握手升级到帧格式双向通道是怎么建立的WebSocket的建立过程可以拆成三个阶段。阶段一HTTP握手。客户端发一个带Upgrade: websocket头、Sec-WebSocket-Key随机值、Sec-WebSocket-Version: 13的HTTP请求。服务端验证通过后返回101 Switching Protocols并把根据Sec-WebSocket-Key计算出的Sec-WebSocket-Accept响应头带上。这个计算规则是固定的把客户端的Key拼接魔数字符串做SHA-1哈希再Base64编码。连接升级成功。阶段二帧传输。之后双方在这个TCP连接上按WebSocket帧格式通信。一个帧包含FIN标志位、opcode表示文本、二进制、ping、pong、close等类型、掩码、payload长度和payload数据。客户端发往服务端的帧必须掩码服务端发往客户端的帧不需要掩码这是协议规定的目的是防止缓存投毒攻击。阶段三连接管理。这是实战中最容易被低估的部分。WebSocket建立之后中间设备和服务端之间可能因为长时间没有数据交换而被断开——负载均衡器有空闲超时操作系统有TCP空闲超时浏览器中间层也可能断开空闲连接。所以业务层必须定期发ping/pong心跳帧服务端收到ping要回pong客户端也要检测服务端是否已经失联。这些逻辑全是自己写。2.4 协议级对比一张表看清核心差异维度SSEWebSocket协议基础普通HTTPHTTP升级到TCP长连接传输方向服务端到客户端单向双向全双工数据格式文本text/event-stream格式文本帧、二进制帧都支持自动重连EventSource内置支持Last-Event-ID断点续传无内置需自己实现重连逻辑自定义请求头原生EventSource不支持需用fetch模拟握手阶段可带任意HTTP头握手认证与普通HTTP接口一致需要换协议认证逻辑要在握手中处理心跳机制通过注释行或定时data实现需要自行实现ping/pong浏览器连接数限制原生EventSource同域6个连接HTTP/1.1场景连接数限制宽松但每个连接占用独立TCP代理穿透复杂度低和普通HTTP请求一样配置高需要单独调整超时和缓冲参数服务端实现复杂度低普通HTTP接口即可高需要处理会话、并发、帧编解码为什么SSE在HTTP/2时代更值得用了因为HTTP/2允许多个流复用同一个TCP连接之前“同域只能开6个EventSource连接”的限制被大大缓解。而且HTTP/2本身支持流式响应代理层对流的转发也成熟SSE的穿透性比WebSocket好不少。3. 大模型流式输出的实战AI交互里SSE为什么突然成了主角3.1 大模型回答的“打字机效应”和SSE的天然契合如果你对接过OpenAI、通义、文心这类大模型接口会发现它们默认情况下返回的是完整文本但你可以在请求参数里加stream: true让服务端把生成的结果一段一段地推出来。为什么大模型要提供流式接口因为大模型生成一句几百字的回答可能要四五秒甚至更久如果让用户盯着一个转圈等十秒才看到全文体验非常差。改成流式输出后模型每生成一段token就推给前端用户看着文字“打字机”一样蹦出来等待感就消失了。而这个“服务端向客户端单向推送文本流”的场景恰恰就是SSE的主场。大模型服务端不需要实时接收客户端的大量指令只需要持续向客户端推送文本块。用WebSocket当然也能实现但你在协议层面上的成本是不对称的——WebSocket要维护双向通道、处理心跳重连而SSE只需要一个普通HTTP接口。这也是为什么大模型AI应用里SSE流式渲染几乎是事实标准GitHub上各种prompt对话项目都基于SSE封装AI交互逻辑原因很简单轻、快、够用。3.2 用fetch模拟SSE为什么AI场景反而不推荐EventSource前面提过原生EventSource不能设置自定义请求头这是它在AI场景里最大的痛点。大模型API基本都需要带Authorization: Bearer或者api-key这种头普通EventSource根本发不出去。你可能会想那用EventSource连接自己后端再由后端转发大模型API不就行了可以但在很多场景里前端要直连大模型API或者要通过网关转发之后还需要动态设置请求头这时候你只能放弃EventSource。替代方案就是用fetch的ReadableStream来解析流式响应手动实现SSE客户端。基本思路是设置stream: true后端返回Content-Type: text/event-stream前端循环读取流里的每一块数据按SSE格式解析出data:xxx然后更新UI。const controller new AbortController(); const response await fetch(/api/chat/stream, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${token}, }, body: JSON.stringify({ messages }), signal: controller.signal, }); const reader response.body.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { value, done } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const chunks buffer.split(\n); buffer chunks.pop(); for (const line of chunks) { if (line.startsWith(data:)) { const payload line.slice(5).trim(); if (payload [DONE]) return; handleChunk(JSON.parse(payload)); } } }这段代码的关键点在于decoder.decode(value, { stream: true })因为网络包的边界不一定等于数据行的边界一个中文字符可能被拆在两个TCP包里不用stream模式解码就会在边界处产生乱码。buffer的作用是缓存未完成的行等下一块数据来了再拼接解析。这是手写SSE客户端里最容易踩的坑。3.3 中断生成abort机制的前后端完整配合大模型流式输出还有个交互刚需用户点“停止生成”按钮前端要立刻中断请求并且通知后端停止调用大模型API。在WebSocket时代这很顺直接向前端连接发一条关闭消息就行。在SSEfetch模式下靠的是AbortController。前端逻辑很简单创建一个AbortController实例把它的signal传给fetch。用户点击停止时调用controller.abort()浏览器会立即中断这个fetch请求。此时后端正在向这个大模型API拉取流式数据因为HTTP连接被客户端断开服务端的流式响应会抛出一个aborted相关的错误你在后端代码里捕获到这个错误后主动取消对上游大模型API的调用释放连接资源。curl -N -X POST https://api.example.com/chat/completions \ -H Authorization: Bearer sk-xxx \ -H Content-Type: application/json \ -d {model:gpt-xxx,messages:[{role:user,content:你好}],stream:true}后端比如用Node.js的fetch或者Python的httpx时也要把上游请求绑定到当前HTTP请求的生命周期里。Node里可以用AbortSignal.timeout或者把前端传来的abortSignal透传给上游请求fetch(upstreamUrl, { signal: req.signal })。Python里可以用httpx的AsyncClient配合asyncio任务取消或者给requests包加超时中断。总之中断SSE不只是前端一个abort()的事后端要响应这个中断并层层传递到上游才能真正释放大模型的计费资源。3.4 封装AI交互逻辑的技术栈选型建议如果你要在自己的项目里封装一套AI交互逻辑核心需求通常是流式渲染回答、中断生成、多轮对话、错误处理、重试策略。我的建议是前端统一封装一个streamChat函数模块底层用fetchReadableStream接口对外暴露onChunk回调、onDone回调、abort方法。这样上层业务不用关心协议细节。后端起一个轻量SSE代理服务负责鉴权、转发大模型API、处理上游断连、为前端补Content-Type: text/event-stream响应头。后端语言选型上Node.js天然适合这种IO密集转发场景Golang的http.ResponseWriter也原生支持流式写出Python的FastAPI/Flask用StreamingResponse也能做到。关键点是前后端的背压backpressure处理。大模型API推过来的token速度如果远超前端消费速度在固定内存的服务器上会堆积大量数据需要限制缓冲或者尽快写入到网络流里。很多人纠结“AI场景到底用SSE还是WebSocket”我的判断是AI对话这类“单向流式”场景SSE是首选因为实现简单、协议穿透性好、自动重连省心。但如果你的AI应用不只有对话还有类似“正在编辑的多人协作”“文件传输进度任务控制通道”这种双向高频交互那混合方案更合理主通道用WebSocket承载控制消息和协作数据流式文本继续走SSE。4. 后端落地实录SpringBoot整合SSE、Django整合WebSocket4.1 SpringBoot整合SSE从SseEmitter到流式接口的完整步骤SpringBoot里实现SSE有几种姿势最传统的是SseEmitter另一种是用响应式编程的Flux结合text/event-stream还有直接操作HttpServletResponse自己写流。我实际用下来SseEmitter是最容易理解和维护的。步骤一创建SSE接口。RestController RequestMapping(/api/sse) public class SseController { private final CopyOnWriteArrayListSseEmitter emitters new CopyOnWriteArrayList(); GetMapping(/subscribe) public SseEmitter subscribe() { SseEmitter emitter new SseEmitter(0L); // 0L表示不超时 emitters.add(emitter); emitter.onCompletion(() - emitters.remove(emitter)); emitter.onTimeout(() - emitters.remove(emitter)); emitter.onError(e - emitters.remove(emitter)); return emitter; } public void sendToAll(String event, Object data) { ListSseEmitter dead new ArrayList(); for (SseEmitter emitter : emitters) { try { emitter.send(SseEmitter.event().name(event).data(data)); } catch (IOException e) { dead.add(emitter); } } emitters.removeAll(dead); } }SseEmitter要设置超时时间默认是30秒如果业务场景需要长连接必须显式给一个足够大的值或者传0L表示永不超时。但这有个隐患如果用默认的内嵌Tomcat连接可能受容器本身的限制所以生产环境我更推荐把SSE接口放在独立的网关路径上网关侧把读取超时时间调大。步骤二结合流式输出推送。假设你有一个大模型调用的方法返回FluxString你可以把每个token转发到对应SseEmitter上。GetMapping(/chat) public SseEmitter chat(RequestParam String prompt) { SseEmitter emitter new SseEmitter(0L); // 模拟大模型流式返回 new Thread(() - { try { for (int i 0; i 100; i) { emitter.send(SseEmitter.event().name(token).data(token_ i)); Thread.sleep(100); } emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }).start(); return emitter; }这里有个并发问题要提醒多线程往同一个SseEmitter发送数据要避免并发写导致的消息交错。如果推送给同一个连接的多路数据来自不同线程建议用一个队列串行化或者用响应式框架天然的单线程调度。4.2 SpringBoot整合WebSocketWebSocketHandler还是STOMPSpringBoot整合WebSocket有两条路原生ServerEndpoint/WebSocketHandler和基于Spring Messaging的STOMP协议。选型逻辑是如果你的场景是简单的点对点消息用原生handler足够如果需要“客户端订阅某个topic/群组”那STOMP的MessageMapping和/topic广播模型会省很多事。用WebSocketHandler的例子Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new ChatWebSocketHandler(), /ws/chat) .setAllowedOrigins(*); } } public class ChatWebSocketHandler extends TextWebSocketHandler { private final MapString, WebSocketSession sessions new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) { sessions.put(session.getId(), session); } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload message.getPayload(); // 解析消息转发给其他会话 for (WebSocketSession s : sessions.values()) { if (s.isOpen() !s.getId().equals(session.getId())) { s.sendMessage(new TextMessage(payload)); } } } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { sessions.remove(session.getId()); } }几个坑ConcurrentHashMap管理会话是基础操作但要注意并发遍历时调sendMessage如果目标连接已经关闭会抛异常得用isOpen()判断加异常捕获。另外SpringBoot默认支持一个应用同时处理多个WebSocket会话但每个会话的业务状态用户ID、房间ID等建议挂在session.getAttributes()里不要额外维护映射表否则断线清理容易漏。4.3 Django Channels后台有数据就往前面推的完整链路热搜词里有一条“python django websocket实现后台有数据前端推送”这是Django项目里很常见的需求。比如后台任务跑完了一个结果要实时推给页面上对应的人。Django本身是同步WSGI模型不支持WebSocket所以要用Channels这个库切换成ASGI模式配合channel layer做跨进程通信。基础搭建流程安装channels和channels-redis把项目的ASGI配置从wsgi.application改成ProtocolTypeRouter。# asgi.py import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack from django.urls import path from myapp.consumers import NotificationConsumer os.environ.setdefault(DJANGO_SETTINGS_MODULE, myproject.settings) application ProtocolTypeRouter({ http: get_asgi_application(), websocket: AuthMiddlewareStack(URLRouter([ path(ws/notifications/, NotificationConsumer.as_asgi()), ])), })写一个Consumer注册到group_add里这样后台数据通过channel layer的group_send就能推给这个用户所有已连接的WebSocket会话。# consumers.py from channels.generic.websocket import AsyncWebsocketConsumer class NotificationConsumer(AsyncWebsocketConsumer): async def connect(self): self.room_group_name user_%s % self.scope[user].id await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard( self.room_group_name, self.channel_name ) async def notify(self, event): await self.send(text_dataevent[message])在视图或后台任务里调用async_to_sync把数据推送到group里。from channels.layers import get_channel_layer from asgiref.sync import async_to_sync def push_notification(user_id, message): channel_layer get_channel_layer() async_to_sync(channel_layer.group_send)( user_%s % user_id, { type: notify, message: json.dumps({msg: message}), } )这里要注意group_send的type字段对应Consumer里的方法名。比如type: notify会调用notify方法方法名用下划线channel layer内部会转成点号查找不要写错。另一个大坑是Redis channel layer的配置。如果用redis作为channel layer后端必须保证WebSocket连接和业务进程都能连同一个Redis实例且Redis版本要支持流类型。还要注意async_to_sync在异步视图里别乱用会阻塞事件循环。4.4 Python反向WebSocket的桥接玩法热搜词里的“python反向websocket”我理解的是Python服务作为WebSocket客户端去主动连接一个外部的WebSocket服务端再把收到的数据转发给前端。这在大模型AI场景里很常见——比如你的Python后端要监听某个上游平台的实时推送上游只提供WebSocket接口你要在Django里起一个异步任务用websockets库连上去然后把消息通过Channels推到前端页面。import asyncio import websockets from channels.layers import get_channel_layer from asgiref.sync import async_to_sync async def forward_ws_to_django(): uri wss://upstream.example.com/feed async with websockets.connect(uri) as ws: while True: message await ws.recv() channel_layer get_channel_layer() await channel_layer.group_send( feed_listeners, {type: forward, message: message} )这种反向WebSocket桥接玩法本质是把“外部Push”和“内部Push”串联起来。需要重点考虑的是断线重连和幂等注册外部WebSocket断开了要自动重连内部group里的会话掉了要从group里移除否则消息会堆积在Redis里没人消费撑爆内存。5. 躺坑实录这些报错和超时问题我全遇过5.1 stream disconnected before completion: idle timeout waiting for sse这个报错是很多人在对接SSE时第一次遇到的噩梦。英文意思很直白流还没结束连接就被“空闲超时”掐断了。原因通常不是服务端代码出问题而是中间的代理层或负载均衡器把空闲连接给关了。比如Nginx的proxy_read_timeout默认是60秒如果你两条SSE消息之间的间隔超过60秒Nginx就觉得这个连接空闲了主动断开。解决办法是三层齐下服务端定期发心跳。SSE协议里的注释行:就可以充当心跳或者定时发一个空data事件。这样连接里一直有数据流动就不算空闲。调整代理层超时参数。Nginx里设置proxy_read_timeout 3600s; proxy_buffering off;。如果前面还有云负载均衡也要把idle timeout调大。像AWS ALB默认60秒不调的话同样会断。还有个容易被忽略的点proxy_buffering off要关掉。因为Nginx默认会缓冲上游响应流式数据在Nginx层攒到一定大小才往下发导致前端拿到数据有明显的延迟。开SSE时必须关掉缓冲让数据边到边转。5.2 用WebSocket Test Client调试时遇到的3个坑微博上有人搜“websocket test client”说明很多人调试WebSocket还是不太顺。我常用的调试方式分三类浏览器控制台里直接写new WebSocket()、在线WebSocket测试工具、IDE插件比如Postman和Apifox都支持WebSocket请求。我遇到过的坑有这么几个在线工具连不上本地服务。很多在线工具跑在HTTPS页面上你本地如果起的是ws://localhost:8080浏览器会拦截混合内容连不上。这种时候直接用本地IDE或者改成本地测试页面。测试工具无法设置子协议和自定义Header。部分工具不支持在握手阶段带Sec-WebSocket-Protocol或Authorization导致线上带鉴权的连接在测试工具里永远403。建议用Postman这类支持自定义Header的客户端或者直接用Python脚本模拟握手。分不清“连接成功”和“消息到达”。WebSocket Test Client显示connected不代表你的业务逻辑正常只能说明握手升级成功。很多人在test client里发消息后发现服务端日志没有收到以为是工具问题其实是消息格式不匹配服务端Handler根据消息类型拆分失败。5.3 心跳、重连、负载均衡下的长连接存活问题长连接的存活管理是WebSocket运维的核心问题也是SSE容易在网关侧被误杀的原因。我从生产事故里总结出的一套配置模型WebSocket服务端每隔30秒发一次ping客户端收到ping后回pong。如果客户端连续三次没有回复pong服务端认为连接已死主动关闭。浏览器端的WebSocket客户端要监听onclose事件实现指数退避重连1秒、2秒、4秒、8秒……最高30秒避免断线后雪崩式重连打崩服务。负载均衡层如果是四层的要注意TCP的空闲时间如果是七层的要做WebSocket协议的会话保持sticky session不能让同一个WebSocket连接在多次请求之间被转发到不同后端实例。多实例部署时WebSocket会话信息不能只放在单机内存里要放到Redis或消息队列里共享否则某个实例重启后连在它上面的用户全掉线。SSE的心跳则简单得多。服务端定时输出一行注释即可: ping注释行不会被EventSource当作消息解析但能维持连接活性让代理层永远认为连接是活跃的。这是最廉价的SSE保活手段。5.4 奇奇怪怪的需求“通过WebSocket发送POST请求”网上有个热搜词叫“通过websocket发送post请求”。我第一次看到这个需求时愣了一下WebSocket不是HTTP哪来的POST后来理解了两层含义第一层含义是在WebSocket消息里模拟HTTP语义。比如有的老系统内部API只支持HTTP POST你想用WebSocket连接在前后端之间传输数据但消息里带了一个JSON体内部需要知道“这是一次POST请求”。这时你可以在WebSocket消息里设计一个协议头{ type: request, method: POST, path: /api/users, requestId: abc123, body: { name: 张三 } }后端解析这个消息转成对内部HTTP API的调用再把响应通过同一个WebSocket连接返回。这里的“POST”其实是被模拟出来的语义。第二层含义是真有人问WebSocket能不能发HTTP POST请求不能。WebSocket协议本身和HTTP是两套东西握手之后传输的就是WebSocket帧不是HTTP请求。如果你需要在一个WebSocket连接里同时传控制指令和HTTP调用那就自己设计应用层协议把“请求方法”作为消息字段来标识。业务层看到method: POST时就按New POST的语义处理。这种设计模式并不罕见很多实时协作工具都是“WebSocket做通道HTTP做业务明细”的混合架构。难点在于请求-响应关联客户端发了多个请求响应先后返回前端要靠requestId把响应和原始请求匹配上本质上是一个在WebSocket之上实现的RPC层。6. 选型建议和个人体感6.1 决策清单按业务场景打钩听完这么多原理和实战最后给一张我在项目里实际使用的决策清单帮你快速判断只需要服务端单向推送文本/JSON不需要客户端在同一连接里反向高频发消息 →选SSE需要把数据逐步推给前端且推完连接就结束大模型对话、日志流、文件解析进度 →选SSE需要双向高频交互比如在线游戏、协作编辑、聊天室、实时表格 →选WebSocket需要传输二进制数据图片、音频、文件分片 →选WebSocket需要兼容老浏览器IE等已经不在讨论范围内但一些老旧WebView内核确实连EventSource都不支持 →选WebSocket希望享受HTTP自带的代理穿透、简单鉴权、自动重连 →选SSE项目里已经有成熟的WebSocket基础设施团队成员对长连接管理有经验 →选WebSocket如果产品经理说“我们要做实时通知 AI流式对话 在线多人协作”那基本就是SSE和WebSocket同时上一个管流式文本一个管双向交互两者并不互斥。6.2 混合使用用WebSocket做通道用SSE细分事件我自己在做过的一个AI工场项目里就是这么混的全局有一个WebSocket连接负责用户状态同步、协作编辑消息、断线重连的会话恢复AI对话的流式输出全部走独立的SSE接口因为对话流和分析任务流的生命周期和全局连接不同混在WebSocket里会让消息类型复杂到难以维护。这个方案跑了一年后稳定性最让我省心的反而是SSE那边——只要挂了Nginx心跳配置基本不用管断线问题。WebSocket那边则经历了好几次Redis channel layer的同步问题才把多实例会话共享跑稳。6.3 几个后来才想明白的运维要点最后分享几个课上学不到、踩坑才能记住的经验一是线上日志里要多打“连接生命周期”的关键节点。SSE接口建立、断开、心跳超时、WebSocket握手成功、关闭原因这些都值得打日志。别小看这些日志排查线上连接被谁切断时它们能帮你快速定位是服务端主动关的还是代理层断的还是客户端自己关的。二是压测别再只用HTTP那套工具了。SSE和WebSocket都要测“长连接消息频率”不是测单次请求的QPS。长连接数量上去之后文件描述符、TCP缓冲区、内存占用才是瓶颈。我以前上线WebSocket服务前用ab压了一遍觉得没问题结果真实场景5000并发连接直接把服务内存干满教训惨痛。三是设计时一定要考虑“连接断开后怎么办”。不管是SSE还是WebSocket断线不等于业务失败但你的业务数据要有能力做补偿。SSE有Last-Event-ID先天支持断点续传WebSocket则要靠自己在业务层实现消息确认和重发。选型时如果发现业务必须要断点续传能力SSE会省一大半心。我个人现在的体感是能用SSE解决的需求我不会轻易上WebSocket。在AI流式输出这类场景里SSE的简单就是最大的稳定性。WebSocket不是不好而是它的复杂性需要足够的业务场景来支撑。换句话说只要你想明白了你的数据流方向、生命周期和断线补偿策略选哪个都不会错怕的是还没想清楚就跟着热搜词走。