为什么最后还是使用了 Server-Sent Events 来实现即时动态?
一开始设计我的个人主页时,我就给自己挖了个坑——好歹得有个“即时动态”功能啥的,用来展示我此时此刻正在干嘛,是在写代码、听歌、还是在玩游戏。😵💫 这听起来像个花活,但毫无疑问的是,它确实是一个花活。
这份执着来自于此前我为 Mix-Space 的 Shiro 当中的 我的动态 开发上报软件所埋下的。现在我依旧在维护 AlienFamilyHub/Kizuna: 基于 Tauri 的动态信息上报程序 实现动态上报。🫣
在网上冲浪的时候,我偶然发现了一些很好玩的东西
其一
编码时长统计插件 codetime 会将编辑器正在编辑的信息通过 api 返回给前端,它会返回实时的编码状态信息。
实际上就是发现了这个然后打开控制台才发现了这个好玩的 api 的
{
"id": number,
"uid": number,
"eventTime": number,
"language": "typescript",
"project": "Kizuna",
"relativeFile": "src/stores/eventStore.ts",
"absoluteFile": "c:\\Users\\tianx\\Desktop\\Kizuna\\src\\stores\\eventStore.ts",
"editor": "VSCode",
"platform": "Windows 11",
"gitOrigin": "https://github.com/AlienFamilyHub/Kizuna.git",
"gitBranch": "master"
}
通过这个 API,我可以获取当前用户正在编辑什么、在哪个平台上写的、用的是什么编辑器、具体写的是哪个项目甚至哪个文件。
其二
网易云的“在听状态”是我最想展示的元素之一。我思考它如果能做到多用户同步的话肯定会有能获取到播放状态的接口使用。
它虽然并没有公开 API,但总归还是有大佬去做了抓包等逆向操作将相关的内容开放出来供开发者使用。
在 XiaoMengXinX/Music163Api-Go: 网易云音乐 API Golang 实现 这个项目中,我发现了该功能的详细实现,最终便可以将其封装为可单文件调用的形式。于是 ProcessReporterWingo/core/NcmNowPlay/main.go at master · TNXG/ProcessReporterWingo 诞生了。
于是我就在想,这些是否能放在博主动态中呢?
答案是当然可以,但是使用什么方式传递这些信息呢?
轮询 (敲——,有新消息吗?
起初,我用的是最朴素的方法——轮询。前端每隔几秒请求一次 API,问服务器:“你那边有啥新动态没?”
这种方式是最简单的,但是其缺点也显而易见(ai 老是和我说浪费流量,虽然这确实是轮询的主要问题,但是谁现在还在用流量计费的设备看你的网站啊(划掉,不过能优化还是优化一下吧
会污染控制台的“网络”栏、会造成网络资源的浪费。而且明明很长时间都没有新消息,它还在每五秒钟咣咣敲门,热情得令人感动,但是它一点都不懂体谅服务器,也不懂得体谅开发者的鼠标滚轮(不是
时效也是一个问题,你状态一更新,它不一定能马上知道,因为“我刚轮询完你就变了,咱下次再见吧。”
有人会说,调短轮询时间不就行了吗?这就又引出了轮询的另外一个问题,如果轮询间隔太短,那么服务器压力会更大,而且大部分请求都是无效的——“没有新消息”。
如果轮询间隔太长,那么实时性又会大打折扣。这就像是一个永远无法两全的矛盾。
WebSocket(我们来建立一个长连接吧!)
既然轮询这么麻烦,那么我们为什么不直接建立一个长连接呢?WebSocket 就是为此而生的。
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。
它允许服务器主动向客户端推送数据,这样一来,当服务器有新消息时,可以立即推送给客户端,而不需要客户端不断地询问。它也允许客户端主动向服务器上报目前的数据以及运行情况。
WebSocket 的好处大家都知道:双向通信、延迟低、实时性强,是构建 IM、在线协作编辑这类场景的首选。和轮询比,它简直是把牛刀。
也正因为它是一把牛刀,对于我这个简单的 “即时动态” 功能来说,有些大材小用了,也正所谓“杀鸡用牛刀”。
双向通信用不上:我的“即时动态”其实是单向的,后端告诉前端状态就完了,前端并不会反过来告诉后端“我看到了”。WebSocket 就像是你买了对讲机结果只用来听广播。
连接管理更复杂:保持连接活跃、心跳包、断线重连等逻辑,要写的代码明显比 SSE 多,维护成本高。
资源占用偏高:浏览器要维持长连接,服务器端也得一直 hold 着 socket,会话多了压力也不小。
说实话,我只想告诉访客我正在写 TypeScript,不是开一个聊天室。况且再说了,WebSocket 还有一个现实问题——
很多 CDN 服务商对它的支持并不友好:WebSocket 协议因其全双工通信特性,理论上可以被用来构建代理等 “特殊用途”。这导致部分体量小的云厂商会直接禁用对其的支持。
相比之下,SSE 基于纯 HTTP 协议,数据流向明确(仅服务器到客户端),被滥用的可能性大大降低。企业防火墙和安全设备通常会对 WebSocket 流量进行深度检测,而 SSE 作为标准 HTTP 流,通常被视为普通网页流量处理。这也是为什么在一些特殊的网络环境下,WebSocket 连接容易被中断,而 SSE 却能稳定工作。
Server-Sent Events(嘿,我有新消息要告诉你!)
终于轮到我们的主角 SSE 登场了!它就像是一个单向的广播站,服务器可以随时向客户端推送消息,而客户端只需要乖乖听着就好。
SSE 的工作方式特别简单:
- 客户端发起一个普通的 HTTP 请求
- 服务器保持连接不断开,并设置
Content-Type: text/event-stream
- 当有新消息时,服务器就往这个连接里写数据
// 前端代码简单到令人发指
const eventSource = new EventSource('/api/status');
eventSource.onmessage = (event) => {
console.log('收到新动态啦!', event.data);
};
它就像是给浏览器装了个收音机,调好频道后就一直收听着服务器的“广播”。而且这个“收音机”还特别贴心,如果信号断了(比如网络抖动),它会自动重新连接,完全不用我操心。不过,为了防止连接被网络设备误判为闲置而关闭(虽然被关闭了也会自动重连就是了),我会让服务器定期发送带冒号的注释消息,保持连接活跃。🎵
相比 WebSocket 的“大炮打蚊子”,SSE 简直就是为我这种单向推送场景量身定制的。它不需要特殊的协议支持,不用处理复杂的连接状态,连断线重连都是浏览器自动帮我搞定的。
而且最妙的是,它特别省心:
- 不用处理断线重连(浏览器自带)
- 不用担心防火墙问题(标准 HTTP 协议)
- 代码量比 WebSocket 少得多( 懒人的福音 )
为数不多的“缺点”可能就是它是单向的——但这恰恰是我想要的!我又不是要和访客聊天,我只是想告诉他们:“oi,我刚刚在用 VS Code 写了个 Bug!🐛”
SSE 不喧宾夺主、不拖沓废话,只是静静地等后端“有话要说”,然后送达前端。对我这种只想展示“当前状态”的项目来说,正好、刚好、够用。
所以最终我选择了 SSE 来实现即时动态功能。它就像是一个不会喋喋不休的好朋友,在你想知道我在干什么的时候,它会第一时间告诉你,但绝不会问你“看到了吗?”——因为它压根儿就问不了。😆
如果你也对 SSE 感兴趣,想要深入了解或实践这项技术,推荐阅读以下资料:
这些教程提供了更详细的技术实现细节和最佳实践,相信能帮助你更好地理解和应用 SSE。
当然,也有一些需要注意的事情!
单向数据流特点
这可以说是SSE的缺点,也可以是其的优点。
它的数据单向性保证其轻量的主要特征,但也导致了它无法处置复杂情况的局面。
数据类型限制
SSE 只支持纯文本数据,无法传输二进制数据。
对于图像、音频、视频流等多媒体内容的场景,SSE 不太适用。
你不能通过 SSE 直接传输实时的视频流或者音频——不过,这个限制其实也可以用一些巧妙的方法来绕过,比如通过编码将图片数据转为文本格式(比如 Base64 编码),然后通过 SSE 发送。
不过,这种做法虽然能传输图片,但绝对不是高效的方式。
有时候,图片串联起来看,倒像是视频了,只是传输速度要慢得多了。🎥(日本程序员.jpg(笑
浏览器并发限制
当不通过 HTTP/2 使用时,SSE会受到最大连接数的限制,这在打开多个选项卡时特别麻烦,因为该限制是针对每个浏览器的,并且被设置为一个非常低的数字:6。
该问题在 Chrome 和 Firefox 中被标记为“不会解决”。
不过,此限制是针对每个浏览器 + 域的,因此这意味着你可以跨所有选项卡打开 6 个 SSE 连接到 www.example1.com
,并打开 6 个 SSE 连接到 www.example2.com
。(来自 Stackoverflow)。
幸好现代浏览器普遍支持 HTTP/2 协议。在 HTTP/2 中,连接数的限制被大大改善,因为 HTTP/2 支持多路复用。
在 HTTP/1.x 中,一个 TCP 连接一次只能处理一个请求。这意味着你每发一个请求,都得建立一个新的连接,导致资源浪费和页面加载变慢。就像你要去超市购物,每次都得排队一个接一个地进门,效率超级低。
而 HTTP/2 则解决了这个问题,通过 多路复用,允许在 同一个 TCP 连接上同时处理多个请求和响应。这就像你进入超市后,可以直接去多个货架,随时拿东西,不需要排队。🍏🥖
简而言之,HTTP/2 让你可以用更少的TCP连接处理更多的 SSE 流,避免了因连接数限制而带来的困扰。😎
但需要注意的是,这并不代表 HTTP/2 就是万能的,如果你的应用仍然需要高频率或低延迟的双向通信(比如在线聊天、多人游戏等),那么可能还需要考虑 WebSocket 等其他协议来满足更复杂的需求。
错误处理和重连机制有限
SSE 自带自动重连机制,这确实能帮助你处理一些网络断开问题,但是它的错误处理和连接恢复机制相对较为 基础。
如果你遇到网络不稳定的环境,SSE 可能会表现得 略显不稳定,需要你手动去处理一些高级错误场景,比如数据丢失、重试逻辑、断线重连的时机等等。
换句话说,当网络异常时,SSE 会很“懒”,只是简单的重连,不会很“聪明”地判断什么情况应该马上恢复连接。
性能和服务器压力
SSE 是通过 HTTP 协议不断向客户端推送数据的,这意味着每次客户端连接成功后,服务器得保持连接持续不断地推送数据。
当客户端连接数较多,或者数据量较大时,服务器的压力会显得相当大,性能表现可能会下降。尤其在需要频繁更新的数据场景下,SSE 可能并不是最好的选择。
另外,SSE 的数据传输速度相对较慢,对于对实时性要求极高的应用,可能得考虑 WebSocket 或其他协议来提高效率。
消息可靠性问题
SSE 的 单向数据流 特点使得客户端接收到的数据是 不可靠的流式数据,也就是说,消息传输过程中可能会出现 数据丢失或错位 的情况。
举个例子,如果一个推送消息在传输过程中丢失了,客户端并不会主动请求重传,而是直接跳过。这就意味着,你的动态信息有可能在一些极端情况下会丢失一部分。
这对于某些高重要性的数据传输场景(比如涉及金钱或状态同步的系统)来说,就不太适用了。对于个人主页这样“低风险”的应用,丢掉一些小动态倒无所谓,但对于一些高频交易、实时协作的系统,可靠性就显得尤为重要了。
尾语
如果各位在阅读过程中发现内容错误或描述不清晰的地方,请及时指出!
拜托了
我写这篇文章的初衷就是希望能把我个人主页有关“博主动态”的数据传输的技术细节讲清楚,也欢迎大家来交流、讨论。以及各位在实际开发遇到的问题和见解也能发在评论区!🤓
另外就是这篇文章的slug我并没有采用此前的命名方式,也算是一个新的开始!以上。