从两个 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 响应,所以一个普通路由就能搞定,返回一个「可读流」:
conststream=newReadableStream({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)=>{buffer+=decoder.decodeToString(newUint8Array(data),{stream:true})constparts=buffer.split('\n\n')// 按 \n\n 切出完整帧buffer=parts.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,自己想说话就send:
constws=webSocket.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的升级请求:
constwss=newWebSocketServer({noServer:true})server.on('upgrade',(req,socket,head)=>{if(newURL(req.url,'http://x').pathname==='/api/ws'){wss.handleUpgrade(req,socket,head,ws=>wss.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',raw=>ws.send(JSON.stringify({type:'echo',message:raw.toString()})))// 收到再回})「SSE 一个路由就行,WebSocket 得单开服务器」——这句话你要是记住了,这俩的本质区别其实就懂了一大半。
四、并排对比一下
| SSE | WebSocket | |
|---|---|---|
| 通信方向 | 单向(只能服务器 → 客户端) | 双向(两头都能发) |
| 底层协议 | 就是普通 HTTP | HTTP 握手后「升级」成ws:// |
| 客户端能发消息吗 | 不能(要发只能另外再发普通请求) | 能,随时send |
| 数据类型 | 只能文本 | 文本 + 二进制都行 |
| 断线自动重连 | 浏览器原生EventSource自带(鸿蒙用裸流要自己管) | 都得自己写重连 |
| 后端实现成本 | 低,一个路由返回流就行 | 高,通常要独立的 WebSocket 服务 |
| 典型场景 | AI 逐字输出、消息通知、股票行情、进度条 | 聊天室、多人协作、在线游戏、双向实时 |
五、那我到底该用哪个?
别纠结,一句话判断:
只要「服务器单方面往下推」就够了 → 用 SSE,更简单。
需要「两头频繁你来我往」→ 用 WebSocket。
拿我那两个 demo 对号入座,特别清楚:
- AI 对话:你的问题用一个普通请求发出去就完事了,剩下的全是 AI 单方面把回复一个字一个字推给你——纯单向。这种用 WebSocket 属于杀鸡用牛刀,SSE 正合适,后端还省一个服务器。
- 实时待办:服务端要主动推在线人数(服务器说),我也要随时发消息让它 echo(我也说)——双向都要。这就是 WebSocket 的主场,SSE 干不了「客户端主动发」这件事。
一个朴素但好用的经验法则:能用 SSE 解决的,就别上 WebSocket。双向能力听着很美,但它带来的连接管理、重连、心跳、单独部署的服务……都是实打实的成本。按需选型,不要因为 WebSocket「更高级」就无脑选它。
一句话总结
SSE 是「订电台」——服务器单向广播,本质还是个没结束的 HTTP 响应,轻量;WebSocket 是「打电话」——两头随时对讲,要专门的长连接和服务器伺候。先问自己「需不需要客户端也能主动说话」,答案就出来了。