SSE(Server-Sent Events)是一种在网页和服务器之间建立一个单向通信通道的技术。在这种通信中,服务器可以主动向客户端发送数据,而无需客户端不断请求。以下是关于SSE的简要说明,按照您提供的分类:### 服务器端(Server)在SSE中,
摘要:1 序 近期学习 MCP 开发时,其中有一种MCP通信模式为 基于HTTP的SSE的模式,故此研究一二。 MCP ServerTool 开发指南 - 博客园数据知音 本文主要参考自 Server Send Events教程 - 阮一峰
1 序
近期学习 MCP 开发时,其中有一种MCP通信模式为 基于HTTP的SSE的模式,故此研究一二。
MCP Server/Tool 开发指南 - 博客园/数据知音
本文主要参考自 Server Send Events教程 - 阮一峰 ,并做有一定的调整和追加。
2 概述
服务器向浏览器推送信息,除了 WebSocket,还有一种方法:Server-Sent Events(以下简称 SSE)。本文介绍它的用法。
2.1 SSE 的本质
严格地说,HTTP 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。
也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。
SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前(2026年3月)除了IE浏览器之外的所有主流浏览器都支持。
2.2 SSE 的特点
SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。
总体来说,WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP 请求。
但是,SSE 也有自己的优点。
SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
SSE 默认支持断线重连,WebSocket 需要自己实现。
SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
SSE 支持自定义发送的消息类型。
因此,两者各有特点,适合不同的场合。
特点总结:
SSE(Server-Sent Events,服务器发送事件)是一种基于HTTP协议的服务器向客户端单向实时推送数据的技术。它的核心特点是:
单向通信:仅支持服务器→客户端的数据推送
自动重连:浏览器内置断线重连机制
基于HTTP:兼容现有HTTP生态,易于部署
2.3 SSE vs WebSocket 选型对比
特性
SSE
WebSocket
通信方向
单向(服务器→客户端)
双向(服务器↔客户端)
协议基础
HTTP/HTTPS
独立的WS/WSS协议
自动重连
✅ 原生支持
❌ 需手动实现
开发复杂度
极低
较高
二进制支持
❌ 仅文本
✅ 文本+二进制
网络兼容性
优秀(穿透性好)
可能被代理阻挡
选型建议:
只需服务器"说话" → 选SSE
需要双方"对话"(如在线游戏、视频聊天、协同编辑)→ 选WebSocket
偶尔查一次数据 → 选轮询/长轮询
2.4 适用场景
SSE最适合的业务场景可以归纳为:单向、文本、实时、低频交互的数据推送需求。
它的核心优势在于实现简单、自动重连、与HTTP生态完美兼容。
如果你的场景不需要客户端频繁向服务器发送数据,SSE往往是比WebSocket更轻量、更易维护的选择
案例共性总结:
例子
数据流向
更新频率
用户体验提升
股票行情
服务器→用户
高频
不用刷新页面
AI打字
服务器→用户
流式
不用等全部生成
上传进度
服务器→用户
中频
知道还要等多久
消息通知
服务器→用户
低频
即时感知,不用轮询
1. AI大模型流式输出 / MCP Tool流式输出(最热门场景)
这是SSE目前最火爆的应用场景,典型代表就是ChatGPT的"打字机"式逐字输出效果
大模型生成内容时,通过SSE将token逐字推送给用户
ChatGPT式打字效果(最热门)
场景:AI回答问题时,一个字一个字显示
效果
用户: 讲个笑话
AI: 今...天...有...只...小...猪...走...路...被...撞...了...
↑ 文字像打字机一样逐字出现,不用等全部生成完
前端
const source = new EventSource('/chat?question=讲个笑话');
source.onmessage = (event) => {
// 每次收到一个字/词,立即追加显示
document.getElementById('answer').innerText += event.data;
};
后端
# AI生成内容,逐token推送
def chat_stream(question):
for token in ai_model.generate(question): # "今" → "天" → "有" → ...
yield f"data: {token}\n\n"
提供流畅的实时交互体验,减少用户等待感
文件上传进度条
场景 = 用户上传大文件,实时显示进度
效果
上传中...
████████████████░░░░ 80% ← 进度条实时走动
前端
const source = new EventSource('/upload-progress?id=123');
source.onmessage = (event) => {
const percent = event.data;
document.getElementById('bar').style.width = percent + '%';
document.getElementById('text').innerText = percent + '%';
if (percent === '100') {
source.close(); // 上传完成,关闭连接
}
};
后端
def upload_progress(file_id):
while not upload_complete(file_id):
progress = get_upload_percent(file_id) # 0, 10, 25, 60...
yield f"data: {progress}\n\n"
time.sleep(0.5)
2. 实时通知系统
新订单提醒
用户消息推送
审核状态更新
系统公告通知
3. 实时数据看板/监控
服务器日志流:实时查看应用日志
实时仪表盘:业务指标动态更新
设备监控数据:IoT设备状态实时展示
股票行情:实时股价变动推送
实时股票行情(最经典) :
场景=用户打开股票页面,股价每秒更新
用户看到的页面:
┌─────────────────┐
│ 茅台股票 │
│ 当前价格: 1688.50 ← 每秒自动跳动
│ 涨跌: +1.2% │
└─────────────────┘
前端
const eventSource = new EventSource('/stock-price');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
document.getElementById('price').innerText = data.price;
};
后端(实时推送)
# 每秒推送最新股价
def stream_price():
while True:
price = get_stock_price("茅台")
yield f"data: {json.dumps({'price': price})}\n\n"
time.sleep(1)
4. 进度跟踪类场景
文件上传/下载进度
数据处理任务进度(如批量导入、报表生成)
AI模型训练进度
视频转码进度
5. 社交媒体与内容推送
社交媒体动态更新(如Twitter时间线)
新闻实时推送
直播弹幕(单向接收场景)
6. MCP协议远程通信
在MCP(Model Context Protocol)架构中,SSE被用于客户端直连远程服务器的场景
SaaS应用
轻量级客户端
公共云服务
3 客户端 API
3.1 EventSource 对象
SSE 的客户端 API 部署在EventSource对象上。下面的代码可以检测浏览器是否支持 SSE。
if ('EventSource' in window) {
// ...
}
使用 SSE 时,浏览器首先生成一个EventSource实例,向服务器发起连接。
var source = new EventSource(url);
上面的url可以与当前网址同域,也可以跨域。跨域时,可以指定第二个参数,打开withCredentials属性,表示是否一起发送 Cookie。
var source = new EventSource(url, { withCredentials: true });
EventSource实例的readyState属性,表明连接的当前状态。该属性只读,可以取以下值。
0:相当于常量EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。
1:相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。
2:相当于常量EventSource.CLOSED,表示连接已断,且不会重连。
3.2 基本用法
连接一旦建立,就会触发open事件,可以在onopen属性定义回调函数。
source.onopen = function (event) {
// ...
};
// 另一种写法
source.addEventListener('open', function (event) {
// ...
}, false);
客户端收到服务器发来的数据,就会触发message事件,可以在onmessage属性的回调函数。
source.onmessage = function (event) {
var data = event.data;
// handle message
};
// 另一种写法
source.addEventListener('message', function (event) {
var data = event.data;
// handle message
}, false);
上面代码中,事件对象的data属性就是服务器端传回的数据(文本格式)。
如果发生通信错误(比如连接中断),就会触发error事件,可以在onerror属性定义回调函数。
source.onerror = function (event) {
// handle error event
};
// 另一种写法
source.addEventListener('error', function (event) {
// handle error event
}, false);
close方法用于关闭 SSE 连接。
source.close();
3.3 自定义事件
默认情况下,服务器发来的数据,总是触发浏览器EventSource实例的message事件。开发者还可以自定义 SSE 事件,这种情况下,发送回来的数据不会触发message事件。
source.addEventListener('foo', function (event) {
var data = event.data;
// handle message
}, false);
上面代码中,浏览器对 SSE 的foo事件进行监听。如何实现服务器发送foo事件,请看下文。
4 服务器实现
4.1 数据格式
服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息。
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
上面三行之中,第一行的Content-Type必须指定 MIME 类型为event-steam。
每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式。
[field]: value\n
上面的field可以取四个值。
data
event
id
retry
此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。
: This is a comment
下面是一个例子。
: this is a test stream\n\n
data: some text\n\n
data: another message\n
data: with two lines \n\n
4.2 data 字段
数据内容用data字段表示。
data: message\n\n
如果数据很长,可以分成多行,最后一行用\n\n结尾,前面行都用\n结尾。
data: begin message\n
data: continue message\n\n
下面是一个发送 JSON 数据的例子。
data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n
4.3 id 字段
数据标识符用id字段表示,相当于每一条数据的编号。
id: msg1\n
data: message\n\n
浏览器用lastEventId属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。
4.4 event 字段
event字段表示自定义的事件类型,默认是message事件。浏览器可以用addEventListener()监听该事件。
event: foo\n
data: a foo event\n\n
data: an unnamed event\n\n
event: bar\n
data: a bar event\n\n
上面的代码创造了三条信息。第一条的名字是foo,触发浏览器的foo事件;第二条未取名,表示默认类型,触发浏览器的message事件;第三条是bar,触发浏览器的bar事件。
下面是另一个例子。
event: userconnect
data: {"username": "bobby", "time": "02:33:48"}
event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}
event: userdisconnect
data: {"username": "bobby", "time": "02:34:23"}
event: usermessage
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}
4.5 retry 字段
服务器可以用retry字段,指定浏览器重新发起连接的时间间隔。
retry: 10000\n
两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。
5 案例实践
5.1 Node 服务器实例
SSE 要求服务器与浏览器保持连接。对于不同的服务器软件来说,所消耗的资源是不一样的。Apache 服务器,每个连接就是一个线程,如果要维持大量连接,势必要消耗大量资源。Node 则是所有连接都使用同一个线程,因此消耗的资源会小得多,但是这要求每个连接不能包含很耗时的操作,比如磁盘的 IO 读写。
下面是 Node 的 SSE 服务器实例。
var http = require("http");
http.createServer(function (req, res) {
var fileName = "." + req.url;
if (fileName === "./stream") {
res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive",
"Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n");
res.write("event: connecttime\n");
res.write("data: " + (new Date()) + "\n\n");
res.write("data: " + (new Date()) + "\n\n");
interval = setInterval(function () {
res.write("data: " + (new Date()) + "\n\n");
}, 1000);
req.connection.addListener("close", function () {
clearInterval(interval);
}, false);
}
}).listen(8844, "127.0.0.1");
请将上面的代码保存为server.js,然后执行下面的命令。
$ node server.js
上面的命令会在本机的8844端口,打开一个 HTTP 服务。
然后,打开这个网页,查看客户端代码并运行。
5.2 基于Python+SSE的简易股票行情实时推送服务
项目结构
stock_sse/
├── app.py # 主服务
├── stock_data.py # 模拟股票数据
└── index.html # 前端页面
stock_data.py - 股票数据模拟器
import random
import time
from dataclasses import dataclass
@dataclass
class Stock:
code: str # 股票代码
name: str # 股票名称
price: float # 当前价格
base_price: float # 基准价格(用于计算涨跌幅)
class StockMarket:
"""模拟股票市场,生成实时行情数据"""
def __init__(self):
# 初始化几只热门股票
self.stocks = {
"600519": Stock("600519", "贵州茅台", 1688.00, 1680.00),
"000858": Stock("000858", "五粮液", 145.50, 142.00),
"300750": Stock("300750", "宁德时代", 185.30, 188.00),
"000333": Stock("000333", "美的集团", 65.80, 64.50),
"00700": Stock("00700", "腾讯控股", 320.50, 315.00),
}
def update_prices(self):
"""模拟价格波动"""
for stock in self.stocks.values():
# 随机波动 -2% ~ +2%
change = random.uniform(-0.02, 0.02)
stock.price = round(stock.price * (1 + change), 2)
def get_all_quotes(self) -> dict:
"""获取所有股票行情"""
result = {}
for code, stock in self.stocks.values():
change = stock.price - stock.base_price
change_pct = (change / stock.base_price) * 100
result[code] = {
"code": code,
"name": stock.name,
"price": stock.price,
"change": round(change, 2),
"change_pct": round(change_pct, 2),
"timestamp": time.strftime("%H:%M:%S")
}
return result
def get_single_quote(self, code: str) -> dict:
"""获取单只股票行情"""
if code not in self.stocks:
return None
stock = self.stocks[code]
change = stock.price - stock.base_price
change_pct = (change / stock.base_price) * 100
return {
"code": code,
"name": stock.name,
"price": stock.price,
"change": round(change, 2),
"change_pct": round(change_pct, 2),
"timestamp": time.strftime("%H:%M:%S")
}
# 全局市场实例
market = StockMarket()
app.py - Flask SSE服务
import json
import time
from flask import Flask, Response, render_template_string
from flask_cors import CORS
from stock_data import market
app = Flask(__name__)
CORS(app) # 允许跨域
# ============== HTML模板(嵌入式) ==============
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>实时股票行情 - SSE演示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f6fa;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #2c3e50;
font-size: 28px;
}
.header p {
color: #7f8c8d;
margin-top: 8px;
}
.status {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
margin-top: 10px;
}
.status.connected {
background: #d4edda;
color: #155724;
}
.status.disconnected {
background: #f8d7da;
color: #721c24;
}
.stock-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
max-width: 1200px;
margin: 0 auto;
}
.stock-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
transition: transform 0.2s;
}
.stock-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
}
.stock-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.stock-name {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
.stock-code {
font-size: 12px;
color: #95a5a6;
background: #ecf0f1;
padding: 2px 8px;
border-radius: 4px;
}
.stock-price {
font-size: 32px;
font-weight: 700;
margin: 10px 0;
}
.stock-price.up {
color: #e74c3c; /* A股:红涨绿跌 */
}
.stock-price.down {
color: #27ae60;
}
.stock-change {
display: flex;
gap: 10px;
font-size: 14px;
}
.change-box {
padding: 4px 10px;
border-radius: 4px;
font-weight: 500;
}
.change-box.up {
background: #ffebee;
color: #c62828;
}
.change-box.down {
background: #e8f5e9;
color: #2e7d32;
}
.timestamp {
text-align: right;
font-size: 11px;
color: #bdc3c7;
margin-top: 10px;
}
.flash-up {
animation: flashRed 0.5s;
}
.flash-down {
animation: flashGreen 0.5s;
}
@keyframes flashRed {
0% { background-color: #ffebee; }
100% { background-color: white; }
}
@keyframes flashGreen {
0% { background-color: #e8f5e9; }
100% { background-color: white; }
}
.log {
max-width: 1200px;
margin: 30px auto;
background: white;
border-radius: 8px;
padding: 15px;
font-family: monospace;
font-size: 12px;
color: #666;
height: 150px;
overflow-y: auto;
}
.log-entry {
padding: 2px 0;
border-bottom: 1px solid #f0f0f0;
}
</style>
</head>
<body>
<div class="header">
<h1>📈 实时股票行情监控</h1>
<p>基于 Server-Sent Events (SSE) 技术</p>
<span id="status" class="status disconnected">● 连接中...</span>
</div>
<div class="stock-grid" id="stockGrid">
<!-- 股票卡片动态生成 -->
</div>
<div class="log" id="log">
<div style="color: #999; margin-bottom: 10px;">📡 实时数据日志:</div>
</div>
<script>
// 初始化股票卡片
const stocks = [
{code: '600519', name: '贵州茅台'},
{code: '000858', name: '五粮液'},
{code: '300750', name: '宁德时代'},
{code: '000333', name: '美的集团'},
{code: '00700', name: '腾讯控股'}
];
const stockGrid = document.getElementById('stockGrid');
const logDiv = document.getElementById('log');
const statusSpan = document.getElementById('status');
// 存储上一次价格,用于判断涨跌
const lastPrices = {};
// 创建股票卡片
stocks.forEach(s => {
const card = document.createElement('div');
card.className = 'stock-card';
card.id = `card-${s.code}`;
card.innerHTML = `
<div class="stock-header">
<span class="stock-name">${s.name}</span>
<span class="stock-code">${s.code}</span>
</div>
<div class="stock-price" id="price-${s.code}">--.--</div>
<div class="stock-change">
<span class="change-box" id="change-${s.code}">--</span>
<span class="change-box" id="pct-${s.code}">--%</span>
</div>
<div class="timestamp" id="time-${s.code}">--:--:--</div>
`;
stockGrid.appendChild(card);
});
// 添加日志
function addLog(message) {
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logDiv.appendChild(entry);
logDiv.scrollTop = logDiv.scrollHeight;
}
// 更新股票显示
function updateStock(data) {
const priceEl = document.getElementById(`price-${data.code}`);
const changeEl = document.getElementById(`change-${data.code}`);
const pctEl = document.getElementById(`pct-${data.code}`);
const timeEl = document.getElementById(`time-${data.code}`);
const card = document.getElementById(`card-${data.code}`);
const oldPrice = lastPrices[data.code];
lastPrices[data.code] = data.price;
// 价格
priceEl.textContent = data.price.toFixed(2);
priceEl.className = 'stock-price ' + (data.change >= 0 ? 'up' : 'down');
// 涨跌额
const changeSign = data.change >= 0 ? '+' : '';
changeEl.textContent = changeSign + data.change.toFixed(2);
changeEl.className = 'change-box ' + (data.change >= 0 ? 'up' : 'down');
// 涨跌幅
const pctSign = data.change_pct >= 0 ? '+' : '';
pctEl.textContent = pctSign + data.change_pct.toFixed(2) + '%';
pctEl.className = 'change-box ' + (data.change_pct >= 0 ? 'up' : 'down');
// 时间戳
timeEl.textContent = data.timestamp;
// 价格变动闪烁效果
if (oldPrice !== undefined) {
if (data.price > oldPrice) {
card.classList.remove('flash-down');
card.classList.add('flash-up');
} else if (data.price < oldPrice) {
card.classList.remove('flash-up');
card.classList.add('flash-down');
}
setTimeout(() => {
card.classList.remove('flash-up', 'flash-down');
}, 500);
}
}
// 建立SSE连接
function connectSSE() {
const evtSource = new EventSource('/stream');
evtSource.onopen = () => {
statusSpan.textContent = '● 已连接';
statusSpan.className = 'status connected';
addLog('SSE连接已建立');
};
evtSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
updateStock(data);
addLog(`${data.name}: ¥${data.price} (${data.change >= 0 ? '+' : ''}${data.change_pct}%)`);
} catch (e) {
addLog('数据解析错误: ' + e.message);
}
};
evtSource.onerror = () => {
statusSpan.textContent = '● 已断开';
statusSpan.className = 'status disconnected';
addLog('SSE连接断开,5秒后重试...');
evtSource.close();
setTimeout(connectSSE, 5000);
};
}
// 启动
connectSSE();
</script>
</body>
</html>
"""
# ============== 路由 ==============
@app.route('/')
def index():
"""首页"""
return render_template_string(HTML_TEMPLATE)
@app.route('/stream')
def stream():
"""
SSE核心接口:实时推送股票行情
"""
def generate():
while True:
# 更新市场价格
market.update_prices()
# 推送所有股票数据
for code in market.stocks.keys():
quote = market.get_single_quote(code)
if quote:
# SSE格式:data: {...}\n\n
yield f"data: {json.dumps(quote)}\n\n"
# 每2秒推送一轮
time.sleep(2)
return Response(
generate(),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' # 禁用Nginx缓冲
}
)
@app.route('/api/stocks')
def api_stocks():
"""REST API:获取当前所有行情(非实时)"""
return market.get_all_quotes()
if __name__ == '__main__':
# threaded=True 支持多客户端并发
app.run(host='0.0.0.0', port=5000, threaded=True, debug=True)
安装依赖 & 运行
# 安装依赖
pip install flask flask-cors
# 运行服务
python app.py
running log:
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.202.153:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 148-258-324
127.0.0.1 - - [26/Mar/2026 13:08:35] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2026 13:08:35] "GET /stream HTTP/1.1" 200 -
运行效果
访问 http://localhost:5000
要点总结
组件
作用
关键代码
EventSource
浏览器建立SSE连接
new EventSource('/stream')
text/event-stream
MIME类型标识SSE
mimetype='text/event-stream'
data: ...\n\n
SSE标准格式
yield f"data: {json}\n\n"
自动重连
断线后自动恢复
浏览器原生支持,无需代码
多客户端
支持多人同时查看
threaded=True
提示:实际生产环境建议用 gunicorn + gevent 部署,支持更高并发。
X 参考文献
Server Send Events教程 - 阮一峰 2017.5.27 【推荐】
Colin Ihrig, Implementing Push Technology Using Server-Sent Events
Colin Ihrig,The Server Side of Server-Sent Events
Eric Bidelman, Stream Updates with Server-Sent Events
MDN,Using server-sent events
Segment.io, Server-Sent Events: The simplest realtime browser spec
