惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recent Commits to openclaw:main
Recent Commits to openclaw:main
N
News | PayPal Newsroom
TaoSecurity Blog
TaoSecurity Blog
Google Online Security Blog
Google Online Security Blog
NISL@THU
NISL@THU
T
Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Engineering at Meta
Engineering at Meta
AWS News Blog
AWS News Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
P
Privacy International News Feed
B
Blog
PCI Perspectives
PCI Perspectives
Martin Fowler
Martin Fowler
Spread Privacy
Spread Privacy
P
Proofpoint News Feed
T
Tenable Blog
F
Fortinet All Blogs
G
GRAHAM CLULEY
V2EX - 技术
V2EX - 技术
C
Check Point Blog
Project Zero
Project Zero
P
Palo Alto Networks Blog
J
Java Code Geeks
W
WeLiveSecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
The Exploit Database - CXSecurity.com
博客园 - 司徒正美
P
Privacy & Cybersecurity Law Blog
S
SegmentFault 最新的问题
Last Week in AI
Last Week in AI
Forbes - Security
Forbes - Security
C
Cybersecurity and Infrastructure Security Agency CISA
Security Latest
Security Latest
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Vercel News
Vercel News
Recent Announcements
Recent Announcements
博客园 - Franky
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Recorded Future
Recorded Future
The Last Watchdog
The Last Watchdog
MongoDB | Blog
MongoDB | Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
Cisco Talos Blog
Cisco Talos Blog
量子位
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
基于 Server-Sent Events 实现服务端消息推送 - 元视角
飞鸿踏雪 · 2019-01-18 · via 元视角

前段时间,为客户定制了一个类似看板的东西,用户可以通过看板了解任务的处理情况,通过 APP 扫面页面上的二维码就可以领取任务,而当任务被领取以后需要通知当前页面刷新。原本这是一个相对简单的需求,可是因为 APP 端和 PC 端是两个不同的 Team 在维护,换句话说,两个 Team 各自有一套自己的 API 接口,前端页面永远无法知道 APP 到底什么时候扫描了二维码,为此前端页面不得不通过轮询的方式去判断状态是否发生了变化。这种方式会发送大量无用的 HTTP 请求,因此在最初的版本里,无论是效率还是性能都不能满足业务要求,最终博主采用一种称为 服务器推送事件(Server-Sent Events) 的技术,所以,在今天这篇文章里,博主相和大家分享下关于 服务器推送事件(Server-Sent Events) 相关的内容。

什么是 Server-Sent Events

我们知道,严格地来讲,HTTP 协议是无法做到服务端主动推送消息的,因为 HTTP 协议是一种 “请求-响应” 模型,这意味着在服务器返回响应信息以后,本次请求就已经结束了。可是,我们有一种变通的做法,即首先是服务器端向客户端声明,然后接下来发送的是流信息。换句话说,此时发送的不是一个一次性的数据包,而是以数据流的形式不断地发送过来,在这种情况下,客户端不会关闭连接,会一直等着服务器端发送新的数据过来,一个非常相似而直观的例子是视频播放,它其实就是在利用流信息完成一次长时间的下载。那么,Server-Sent Events(以下简称SSE),就是利用这种机制,使用流信息像客户端推送信息。

说到这里,可能大家会感到疑惑:WebSocket 不是同样可以实现服务端向客户端推送信息吗?那么这两种技术有什么不一样呢?首先,WebSocket 和 SSE 都是在建立一种浏览器与服务器间的通信通道,然后由服务器向浏览器推送信息。两者最为不同的地方在于,WebSocket 建立的是一个全双工通道,而 SSE 建立的是一个单工通道。所谓单工和双工,是指数据流动的方向上的不同,对 WebSocket 而言,客户端和服务端都可以发送信息,所以它是双向通信;而对于SSE而言,只有服务端可以发送消息,故而它是单向通信。从下面的图中我们可以看得更为直观,在 WebSocket 中数据"有来有往",客户端既可以接受信息亦可发送信息,而在 SSE 中数据是单向的,客户端只能被动地接收来自服务器的信息。所以,这两者在通信机制上不同到这里已经非常清晰啦!

WebSocket与SSE对比 WebSocket与SSE对比

SSE 服务端

下面我们来看看SSE是如何通信的,因为它是一个单工通道的协议,所以协议定义的都是在服务端完成的,我们就从服务端开始吧!协议规定,服务器向客户端发送的消息,必须是 UTF-8 编码的,并且提供如下的 HTTP 头部信息:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

这里出现了一个一种新的MIME类型,text/event-stream。协议规定,第一行的 Content-Type 必须是text/event-stream,这表示服务端的数据是以信息流的方式返回的,Cache-Control 和 Connection 两个字段和常规的HTTP 一致,这里就不再展开说啦!OK,现在客户端知道这是一个 SSE 信息流啦,那么客户端怎么知道服务端发送了什么消息呢?这就要说到 SSE 的消息格式,在 SSE 中消息的基本格式是:

其中,field 可以取四个值,它们分别是:dataeventidretry,我们来一起看看它们的用法。

data 字段表示数据内容,下面的例子展示 SSE 中的一行和多行数据,可以注意到,当数据有多行时,可以用 \n 作为每一行的结尾,只要保证最后一行以 \n\n 结尾即可。

:这是一行数据内容
data: SSE给你发了一行消息\n\n
:这是多行数据内容
data: {\n
data: "foo": "foolish",\n
data: "bar", 2333\n
data: }\n\n

event 字段表示自定义事件,默认为 message,在浏览器中我们可以用 addEventListener() 来监听响应的事件,这正是为什么SSE被称为服务器推送事件,因为我们在这里既可以发送消息,同样可以发送事件。

: GameStart 事件
event: GameStart\n
data: 敌军还有30秒到达战场\n\n

data: Double Kill\n\n

: GameOver 事件
event: GaneOver\n
data: You Win!\n\n

id 字段是一个数据标识符,相当于我们可以给每一条消息一个编号。

id: 1\n
data: 敌军还有30秒到达战场\n\n
id: 2\n
data: Double Kill\n\n
id: 3\n
data: You Win!\n\n

retry 字段可以指定浏览器重新发起连接的时间间隔,所以,SSE 天生就支持断线重连机制。

SSE 客户端

SSE 目前是 HTML5 标准之一,所以,目前主流的浏览器(除了IE和Edge以外)都天然支持这一特性,这意味着我们不需要依赖 前端娱乐圈 推崇的各种工具链,就可以快速地使用 SSE 来投入开发。这里需要使用地是 EventSource 对象,我们从下面这个例子开始了解:

if ('EventSource' in window) {
  var source = new EventSource(url, { withCredentials: true });
  
  /* open事件回调函数 */
  source.onopen = function(){ 
  	console.log('SSE通道已建立...');
  };
  
  /* message事件回调函数 */
  source.onmessage = function(evt){
  	console.log(evt.data);
  }
  
  /* error事件回调函数 */
  source.onerror = function(evt){
  	console.log('SSE通道发生错误');
  }
  
  /* 自定义事件回调 */
  source.addEventListener('foo', function (event) {
  	var data = event.data;
  	// handle message
  },false);
	
  /* 关闭SSE */
  source.close()
}

和各种各样的 HTML5 接口一样,我们需要判断当前的浏览器环境是否支持SSE。建立 SSE 只需要后端提供一个 Url 即可,当存在跨域时,我们可以打开第二个参数:withCredentials,这样 SSE 会在建立通道时携带 Cookie。我们通过实例化后的 source 对象来判断通道是否建立,该对象有一个重要的属性:readyState。当它的取值为 0 时,表示连接还未建立,或者断线正在重连;当它的取值为 1 时,表示连接已经建立,可以接受数据;当它的取值为 2 时,表示连接已断,且不会重连。

好了,当 SSE 被成功建立以后,首先会触发 open 事件。这里介绍下 SSE 中的关键事件,即 open、message 和 error,我们可以分别通过 onopenonmessageonerror 这三个回调函数来监听相应的事件。对于 SSE 而言,它是一个单工通道,客户端不能主动向服务端发送信息,所以,一旦建立了 SSE 通道,客户端唯一需要关注的地方就是 onmessage 这个回调函数,因为客户端只需要负责处理消息即可,甚至我们可以连 onerror 都不用关注,因为 SSE 自带断线重连机制,当然你可以选择在发生错误的时候关掉连接,此时你需要 close() 方法。

我们在上面提到,SSE 在服务端可以定义自定义事件,那么,在浏览器中我们该如何接收这些自定义事件呢?这当然要提到无所不能的 addEventListener,在人肉操作 DOM 的 jQuery 时代,jQuery 中提供的大量 API 在协调不同浏览器间差异的同时,让我们离这些底层的知识越来越远,时至今日,当 qerySelector/querySelectorAll 完全可以替换 jQuery 的选择器的时候,我们是不是可以考虑重新把某些东西捡起来呢?言归正传,在 SSE 中,我们只需要像注册普通事件一样,就可以完成对自定义事件的监听,只要客户端和服务端定好消息的协议即可。

在 .NET 中集成 Server-Sent Events

OK,说了这么多,大家一定感觉有一个鲜活的例子会比较好一点,奈何官方提供的示例都是 PHP 的,难道官方默认 PHP 是世界上最好的编程语言了吗?所谓万变不离其宗",下面我们以 .NET 为例来快速集成 Server-Sent Events,这里需要说明的是,博主下面的例子采用 ASP.NET Core 2.0 版本编写,首先,我们建一个名为 SSEController 的控制器,在默认的 Index() 方法中,按照 SSE 规范,我们首先组织 HTTP 响应头,然后发送了一个名为 SSE_Start 的自定义事件,接下来,我们每隔 10 秒钟给客户端发送一条消息,请原谅我如此敷衍的 Sleep():

[Route("api/[controller]")]
[ApiController]
public class SSEController : Controller
{
    [HttpGet]
    public IActionResult Index()
    {
    	//组织HTTP响应头
    	Response.Headers.Add("Connection", "keep-alive");
    	Response.Headers.Add("Cache-Control", "no-cache");
    	Response.Headers.Add("Content-Type", "text/event-stream");

    	//发送自定义事件
        var message = BuildSSE(new { Content = "SSE开始发送消息", Time = DateTime.Now }, "SSE_Start");
        Response.Body.Write(message, 0, message.Length);

        //每隔10秒钟向客户端发送一条消息
        while (true)
        {
            message = BuildSSE(new { Content = $"当前时间为{DateTime.Now}" });
            Response.Body.Write(message, 0, message.Length);
            Thread.Sleep(10000);
       }
    }
}

我们提到,SSE 的数据是按照一定的格式,由 id、event、data 和 retry 四个字段构成的,那么,织消息格式的代码我们放在了 BuildSSE() 方法中,我们来一起看看它的实现:

private byte[] BuildSSE<TMessage>(TMessage message, string eventName = null, int retry = 30000)
{
    var builder = new StringBuilder();
    builder.Append($"id:{Guid.NewGuid().ToString("N")}\n");
    if (!string.IsNullOrEmpty(eventName))
        builder.Append($"event:{eventName}\n");
    builder.Append($"retry:{retry}\n");
    builder.Append($"data:{JsonConvert.SerializeObject(message)}\n\n");
    return Encoding.UTF8.GetBytes(builder.ToString());
} 

可以看到,完全按照 SSE 规范来定义的,这里每次生成一个新的 GUID 来作为消息的 ID,客户端断线后重连的间隔为 30 秒,默认发送的是 “消息",当指定 eventName 参数时,它就表示一个自定义事件,这里我们使用 JSON 格式来传递信息。好了,这样我们就完成了服务端的开发,怎么样,是不是感觉非常简单呢?我们先让它跑起来,下面着手来编写客户端,这个就非常简单啦!

<!DOCTYPE html>
<html>
<body>
<h1>DotNet-SSE</h1>
<div id="result"></div>
<script>
if ('EventSource' in window) {
  var source = new EventSource('http://localhost:5000/api/SSE/');
  
  /* open事件回调函数 */
  source.onopen = function(){ 
  	document.getElementById("result").innerHTML+= "SSE通道已建立...<br/>";
  };
  
  /* message事件回调函数 */
  source.onmessage = function(evt){
  	document.getElementById("result").innerHTML+= "Message: " + event.data + "<br/>";
  }
  
  /* error事件回调函数 */
  source.onerror = function(evt){
      document.getElementById("result").innerHTML+= "SSE通道发生错误<br/>";
  }
  
  /* SSE_Start事件回调 */
  source.addEventListener('SSE_Start', function (event) {
  	document.getElementById("result").innerHTML += "SSE_Start: " + event.data + "<br/>";
  },false);
}
</script>
</body>
</html>

此时,不需要任何现代前端方面的技术,我们直接打开浏览器,就可以看到:

SSEDemo SSEDemo

更为直观的,我们可以通过Chrome开发者工具观察到实际的请求情况,相比普通的 HTTP 请求,SSE 会出现一个名为 EventStream 的选项卡,这是因为我们在服务端设置的 Content-Type 为 text/event-stream 的缘故,可以注意到,我们定义的id(GUID)会在这里显示出来:

有点与众不同的SSE 有点与众不同的SSE

同类技术优劣对比

OK,这篇文章写到这里,相信大家已经对 SSE 有了一个比较具体的概念,那么,我们不妨来梳理下相关的同类技术。一路走过来,我们大体上经历了 (短)轮询长轮询/CometSSEWebSocket

(短)轮询这个比较容易理解了,它从本质上来讲,就是由客户端定时去发起一个 HTTP 请求,这种方式是一种相对尴尬的方式,为什么这样说呢?因为时间间隔过长则无法保证数据的时效性,而时间间隔过短则会发送大量无用的请求,尤其是当客户端数量比较多的时候,这种方式很容易耗尽服务器的连接数。

而长轮询 则是(短)轮询的一个变种,它和(短)轮询最大的不同在于,服务端在接收到请求以后,并非立即进行响应,而是先将这个请求挂起,直到服务器端数据发生变化时再进行响应。所以,一个明显的优势是,它相对地减少了大量不必要的HTTP请求,那么,它是不是就完美无暇了呢?当然不是,因为服务端会将客户端发来的请求挂起,因此在挂起的那些时间里,服务器的资源实际上是被浪费啦!

严格地说,SSE 并不是一门新技术,为什么这样说呢?因为它和我们基于 HTTP 长连接的 Push 非常相似。这里又提到一个新概念,HTTP 长连接,其实,这个说法病逝非常严谨,因为我们知道 HTTP 最早就是一个请求-响应模型,直到 HTTP1.1 中增加了持久连接,即 Connection:keep-alive 的支持。所以,我们这里说的长连接、短链接实际上都是指 TCP 的长连接还是短连接,换句话说,它和客户端没有关系,只要服务端支持长连接,那么在某个时间段内的 TCP 连接实际上复用的,进而就能提高 HTTP 请求性能,曾经我们不是还用 iframe 做过长连接吗?

WebSocket 作为构建实时交互应用的首选技术,博主曾经在《基于WebSocket和Redis实现Bilibili弹幕效果》一文中有所提及,WebSocket 相比前面这些技术,最大的不同在于它拥有专属的通信通道,一旦这个通道建立,客户端和服务端就可以互相发送消息,它沿用了我们传统的 Socket 通讯的概念和原理,变被动为主动,无论是客户端还是服务端,都不必再被动地去”" 或者 “” 。在这个过程中,出现了像 SignalR/SocketIO 等等的库,它们主打的兼容性和降级策略,曾经一度让我们感到亲切,不过随着 WebSocket 标准化的推进,相信这些最终都会被原生 API 所替代吧,也许是有生之年呢?谁知道未来是什么样子呢?

下面给出针对以上内容的 “简洁” 版本:

(短)轮询长轮询/CometSSEWebSocket
浏览器支持全部全部除IE/Edge现代浏览器
是否独立协议HTTPHTTPHTTPWS
是否轻量
断线重连
负载压力占用内存/请求数同(短)轮询一般同SSE
数据延迟取决于请求间隔同(短)轮询实时实时

本文小结

正如本文一开始所写,博主使用SSE是因为业务上的需要,在经历了轮询带来的性能问题以后,博主需要一款类似 WebSocket 的东西,来实现服务端主动向客户端推送消息,究其原因,是因为浏览器永远都不知道,App 到底什么时候会扫描二维码,所以,从一开始我们试图让网页去轮询的做法,本身就是不太合理的。那么,为什么没有用 WebSocket 呢?因为 WebSocket 需要一点点 框架 层面的支持,所以,我选择了更为轻量级的 SSE,毕竟,这比让其它 Team 的同事去调整他们的后端接口要简单的多。我之前参与过一部分 WebSocket 相关的项目,我深切地感受到,除了在浏览器的兼容性问题以外,因为 WebSocket 使用的是独有的 WS 协议,所以,我们常规的 API 网关其实在这方面支持的都不是很好,更不用说鉴权、加密等等一系列的问题啦,而 SSE 本身是基于 HTTP 协议的,我们目前针对 HTTP 的各种基础设施,都可以直接拿过来用,这应该是我最大的一点感悟了吧,好了,这篇文章就是这样啦,谢谢大家,新的一年注定要重新开始的呢……

参考文章