从两个 demo 说起WebSocket 和 SSE 到底差在哪我先后写了两个鸿蒙小 demo。一个是AI 对话你发一句话AI 的回复不是「啪」一下整段蹦出来而是一个字一个字往外冒像有人在那头实时打字。另一个是实时待办页面上挂着一个「在线人数」我啥也没干那个数字自己每隔两秒跳一下我在输入框发条消息服务端立刻原样回给我。这俩有个共同点数据是服务器「主动」推给客户端的不是客户端傻乎乎地一直问。但我一个用了SSE一个用了WebSocket。那问题就来了都是「服务器往客户端推数据」为什么要两套东西它俩到底差在哪、我该用哪个这篇就把这事儿掰开揉碎讲清楚。看完你不用去背参数只要记住两个画面就行。一、先搞明白为什么普通 HTTP 不够用我们平时发请求都是这个套路客户端「给我用户列表。」服务器「给这是用户列表。」—— 然后连接就断了。这叫请求-响应。它有个天生的脾气必须客户端先开口服务器才能说话。服务器再想跟你说点啥比如「有新消息了」它没法主动找你只能干等着你下次来问。那「在线人数变了要实时更新」这种需求怎么办最朴素的想法是轮询——客户端每隔几秒就问一次「变了吗」「没有。」3 秒后「变了吗」「没有。」3 秒后「变了吗」「变了现在 8 个人。」说实话这就跟你点了外卖每隔十秒给商家打一次电话问「做好了没」一样——绝大多数电话都是白打的又费电又费流量消息还总慢半拍最坏要等一整个轮询间隔。更聪明的做法显然是别让我一直问你做好了主动通知我。SSE 和 WebSocket就是实现「服务器主动通知」的两种方式。区别在于——这通「电话」是单向的还是双向的。二、SSE一个「只能听、不能回」的电台SSE 全称 Server-Sent Events直译就是「服务器发送的事件」。类比它就像你订阅了一个电台。电台一直在播你打开收音机就能源源不断地听到内容但你没法对着收音机说话——信息只能从电台流向你单向的。它最妙的一点是SSE 根本不是什么新协议它就是一个「迟迟不肯结束」的普通 HTTP 响应。平时的 HTTP 响应是「把数据一次性给你然后结束」。SSE 是「保持这个响应不结束每有新数据就往这条管子里塞一段」。约定也很简单每条消息长这样data: {chunk:你} data: {chunk:好} data: {done:true}就是data:开头、\n\n两个换行结尾。客户端这边收到后按\n\n切一刀就是一条完整消息。demo 里的 SSE 长什么样后端我用的 Next.js——因为 SSE 本质就是个 HTTP 响应所以一个普通路由就能搞定返回一个「可读流」conststreamnewReadableStream({asyncstart(controller){for(constchofreplyText){// 每个字塞一帧中间睡 50ms前端就有了「逐字蹦」的效果controller.enqueue(encoder.encode(data:${JSON.stringify({chunk:ch})}\n\n))awaitsleep(50)}controller.enqueue(encoder.encode(data:${JSON.stringify({done:true})}\n\n))controller.close()}})returnnewResponse(stream,{headers:{Content-Type:text/event-stream}})客户端鸿蒙——用的还是发 HTTP 请求那套http只不过换成「流式」的requestInStream然后监听dataReceive一段段接req.on(dataReceive,(data:ArrayBuffer){bufferdecoder.decodeToString(newUint8Array(data),{stream:true})constpartsbuffer.split(\n\n)// 按 \n\n 切出完整帧bufferparts.pop()??// 最后一段可能不完整留着下次拼for(constpartofparts){// 解析 data: 后面的 JSON是 chunk 就往屏幕上拼字是 done 就收尾}})req.requestInStream(url,{method:http.RequestMethod.POST,/* ... */})注意那个buffer网络是按「包」来的一帧消息可能被拆到两个包里也可能两帧挤在一个包里。所以不能收到就直接用得先攒进 buffer按\n\n切切剩下的零头留到下次再拼。这是写 SSE 客户端最容易踩的坑。三、WebSocket一条「两边都能说话」的电话线WebSocket 就不一样了。类比它像打电话。接通之后两头都能随时开口你一句我一句不用挂了重拨。这就是所谓的「全双工」——一条线双向跑。它的建立过程有点意思先用一个普通 HTTP 请求去敲门请求头里带一句「我想把这条连接升级成 WebSocket」Upgrade: websocket。服务器同意了回一个「101 切换协议」这条 TCP 连接就从 HTTP「变身」成了 WebSocket之后地址也从http://变成ws://。握手用 HTTP握完手就不再是 HTTP 了。demo 里的 WebSocket 长什么样客户端鸿蒙——用的是专门的webSocket模块事件驱动连上open、来消息message、断了close、出错error自己想说话就sendconstwswebSocket.createWebSocket()ws.on(open,(){/* 接通了 */})ws.on(message,(err,data){/* 服务端推来的数据比如在线人数 */})ws.on(close,(){/* 挂断了 */})ws.connect(ws://192.168.x.x:3000/api/ws)// 任何时候都能主动发ws.send(hello)后端——这里有个特别值得记的坑WebSocket 不能像 SSE 那样写在一个普通路由里。为啥因为 SSE 只是「一个 HTTP 响应」而 Next.js 的路由处理函数本来就是处理 HTTP 响应的天作之合。但 WebSocket 需要在握手时接管底层的 socket去做「协议升级」而路由函数拿不到那个底层 socket。所以我后端不得不单开一个自定义服务器server.js用ws这个库去接管/api/ws的升级请求constwssnewWebSocketServer({noServer:true})server.on(upgrade,(req,socket,head){if(newURL(req.url,http://x).pathname/api/ws){wss.handleUpgrade(req,socket,head,wswss.emit(connection,ws,req))}})wss.on(connection,(ws){ws.send(JSON.stringify({type:welcome}))setInterval(()ws.send(JSON.stringify({type:tick,onlineCount})),2000)// 主动推ws.on(message,rawws.send(JSON.stringify({type:echo,message:raw.toString()})))// 收到再回})「SSE 一个路由就行WebSocket 得单开服务器」——这句话你要是记住了这俩的本质区别其实就懂了一大半。四、并排对比一下SSEWebSocket通信方向单向只能服务器 → 客户端双向两头都能发底层协议就是普通 HTTPHTTP 握手后「升级」成ws://客户端能发消息吗不能要发只能另外再发普通请求能随时send数据类型只能文本文本 二进制都行断线自动重连浏览器原生EventSource自带鸿蒙用裸流要自己管都得自己写重连后端实现成本低一个路由返回流就行高通常要独立的 WebSocket 服务典型场景AI 逐字输出、消息通知、股票行情、进度条聊天室、多人协作、在线游戏、双向实时五、那我到底该用哪个别纠结一句话判断只要「服务器单方面往下推」就够了 → 用 SSE更简单。需要「两头频繁你来我往」→ 用 WebSocket。拿我那两个 demo 对号入座特别清楚AI 对话你的问题用一个普通请求发出去就完事了剩下的全是 AI 单方面把回复一个字一个字推给你——纯单向。这种用 WebSocket 属于杀鸡用牛刀SSE 正合适后端还省一个服务器。实时待办服务端要主动推在线人数服务器说我也要随时发消息让它 echo我也说——双向都要。这就是 WebSocket 的主场SSE 干不了「客户端主动发」这件事。一个朴素但好用的经验法则能用 SSE 解决的就别上 WebSocket。双向能力听着很美但它带来的连接管理、重连、心跳、单独部署的服务……都是实打实的成本。按需选型不要因为 WebSocket「更高级」就无脑选它。一句话总结SSE 是「订电台」——服务器单向广播本质还是个没结束的 HTTP 响应轻量WebSocket 是「打电话」——两头随时对讲要专门的长连接和服务器伺候。先问自己「需不需要客户端也能主动说话」答案就出来了。