Server-Sent Events 详解及实战
SSE介绍
Server-Send Events 服务器发送事件,简称SSE。服务器主动向客户端推送消息,我们常见的有 WebSocket (SignalR) ,SSE 也是其中一种。
SSE 是HTML5规范的一部分,该规范非常简单,主要由两部分组成:第一部分是服务端与浏览器端的通讯协议(Http协议),第二部分是浏览器端可供JavaScript使用的EventSource对象。
严格意义上来说,Http协议是无法做到服务器主动想浏览器发送协议,但是可以变通下,服务器向客户端发起一个声明,我下面发送的内容将是 text/event-stream
格式的,这个时候浏览器就知道了。响应文本内容是一个持续的数据流,每个数据流由不同的事件组成,并且每个事件可以有一个可选的标识符,不同事件内容之间只能通过回车符\r
和换行符\n
来分隔,每个事件可以由多行组成。目前除了IE和Edge,其他浏览器均支持
WebSocket和SSE对比
同为服务端推送技术,WebSocket是比较常见的,SSE就比较冷门了,具体被使用的也更加少了
- WebSocket比SSE功能更加强大,WebSocket是在服务端和客户端建立的双向实时数据通道,而SSE只支持服务端想客户端的单向通讯
- 浏览器对WebSocket的支持也更加广泛,IE、Edge几乎不支持SSE
- WebSocket有一套独立的标准协议,在使用过程中必须按照标准协议来,而SSE使用的是Http协议,只需要更改
Context-Type
为"text/event-stream; charset=utf-8"
即可,这里需要特殊注意的一点,必须是utf-8
- SSE 属于轻量级,使用特别简单,WebSocket协议相对复杂些
- SSE 内置断线重连和消息追踪的功能,WebSocket的也能实现,但是不在协议范围内,需要手动实现
- SSE 只支持纯文本传送(如果需要发送二进制文本的话,需要先编码下然后再传送),WebSocket不仅支持文本还支持二进制数据传送
- SSE 支持自定义发送的消息类型(Type)
- SSE 适合服务器发送单向事件,心跳之类的简单数据,WebSocket试用于前后端通讯,例如聊天服务等,具体场景具体对待
协议、格式、事件
协议
SSE 协议非常简单,正常的Http请求,更改请起头相关配置即可
Content-Type: text/event-stream,utf-8
Cache-Control: no-cache
Connection: keep-alive
基础格式
1、文本流基础格式如下,以行为单位的,以冒号分割 Field 和 Value,每行结尾为 \n,每行会Trim掉前后空字符,因此 \r\n 也可以。
每一次发送的信息,由若干个message组成,每个message之间用\n\n
分隔。每个message内部由若干行组成,每一行都是如下格式。
field: value\n
field: value\r\n
Field是有5个固定的name
data // 数据内容
event // 事件
id // 数据标识符用id字段表示,相当于每一条数据的编号
retry // 重试,服务器可以用retry字段,指定浏览器重新发起连接的时间间隔
: //冒号开头是比较特殊的,表示注释
2、注释
注释行以冒号开头
: 当前行是注释
事件
1、事件
事件之间用\n\n
隔断,一般一个事件一行,也可以多行
# 一个事件一行
data: message\n\n
data: message2\n\n
# 一个事件多行
data: {\n
data: "name": "zhangsan",\n
data: "age", 25\n
data: }\n\n
# 自定义事件
event: foo\n // 自定义事件,名称 foo,触发客户端的foo监听事件
data: a foo event\n\n // 内容
data: an unnamed event\n\n // 默认事件,未指定事件名称,触发客户端 onmessage 事件
event: bar\n // 自定义时间,名称 bar,触发客户端bar监听事件
data: a bar event\n\n // 内容
2、事件唯一标识符
每个事件可以指定一个ID,浏览器会跟踪事件ID,如果发生了重连,浏览器会把最近接收到的时间ID放到 HTTP Header Last-Event-ID
中,作为一种简单的同步机制。
id: eef0128b-48b9-44f7-bbc6-9cc90d32ac4f\n
data: message\n\n
3、重连事件
中断连接,客户端一般会3秒重连,但是服务端也可以配置
retry: 10000\n
服务端实现
服务端实现SSE需要注意一点,SSE为每个客户端分配一个TCP连接,这就意味着Apache之类的基于线程/进程的服务器引擎不适合这个工作。
SSE本身是HTML5协议,因此NodeJS是最佳实现,Nodejs实现具体可以参考:http://cjihrig.com/blog/server-sent-events-in-node-js/,
但是本次我们是以C# 来实现服务端
private readonly IHttpContextAccessor _httpContextAccessor;
public GatewayController(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
[HttpGet]
[Route("event")]
public async Task GetEvent(CancellationToken cancellationToken)
{
var httpContext = _httpContextAccessor.HttpContext;
httpContext.Response.ContentType = "text/event-stream; charset=utf-8";
var data =
$"id:{GuidGenerator.Create().ToString()}\n" +
$"retry:1000\n" +
$"event:message\n" +
$"data:{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n\n";
var bytes = Encoding.UTF8.GetBytes(data);
await httpContext.Response.Body.WriteAsync(bytes);
await httpContext.Response.Body.FlushAsync();
using (var consumer = new BlockingCollection<string>())
{
var eventGeneratorTask = EventGeneratorAsync(consumer, cancellationToken);
foreach (var @event in consumer.GetConsumingEnumerable(cancellationToken))
{
var payload =
$"id:{GuidGenerator.Create().ToString()}\n" +
$"retry:1000\n" +
$"event:message\n" +
$"data:{@event}\n\n";
bytes = Encoding.UTF8.GetBytes(payload);
await httpContext.Response.Body.WriteAsync(bytes);
await httpContext.Response.Body.FlushAsync(cancellationToken);
}
await eventGeneratorTask;
}
}
private async Task EventGeneratorAsync(BlockingCollection<string> eventData, CancellationToken cacellationToken)
{
try
{
ConcurrentQueue<string> query = new ConcurrentQueue<string>();
// EventBus消息订阅
_distributedEventBus.Subscribe<EventStreamHandlerArgs>(data =>
{
var payload= Newtonsoft.Json.JsonConvert.SerializeObject(data);
query.Enqueue(payload);
return Task.CompletedTask;
});
if (!cacellationToken.IsCancellationRequested)
{
while (!eventData.IsCompleted)
{
var item = string.Empty;
if (query.TryDequeue(out item))
{
eventData.Add(item);
}
await Task.Delay(1000, cacellationToken).ConfigureAwait(false);
}
}
}
finally
{
eventData.CompleteAdding();
}
}
客户端实现
1、检测客户端是否支持SSE
function supportsSSE() {
return !!window.EventSource;
}
2、创建客户端连接
客户端实现就比较简单了,实例化一个EventSource
对象,url 可以和服务端同域,也可以跨域,如果跨域的话,需要指定第二个参数withCredentials:true
,表示发送Cookie到服务端
EventSource
实例的readyState
表示当前连接状态,该属性只读
0:EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。
1:EventSource.OPEN,表示连接已经建立,可以接受数据。
2:EventSource.CLOSED,表示连接已断,且不会重连。
var source= new EventSource(url);
var source= new EventSource(url,{withCredentials:true});
事件源连接后会发送 “open” 事件,可以通过以下两种方式监听
# 方式一:
source.onopen = function(event) {
// handle open event
};
## 方式二:
source.addEventListener("open", function(event) {
// handle open event
}, false);
3、接收事件
接收事件同样和上面同样有两种方式。浏览器会自动把一个消息中的多个分段拼接成一个完整的字符串,因此,可以轻松地在这里使用 JSON 序列化和反序列化处理。
# 方式一:
source.onmessage = function(event) {
var data = event.data;
var lastEventId = event.lastEventId;
// handle message
};
## 方式二:
source.addEventListener("message", function(event) {
var data = event.data;
var lastEventId = event.lastEventId;
// handle message }, false);
4、自定义事件
默认情况下,服务器发送过来的消息,都是默认事件格式的数据,这个时候都会触发onmessage
,如果后端自定义事件的话,则不会触发onmessage
,这个是否我们需要添加对应的监听事件
source.addEventListener('foo', function (event) {
var data = event.data;
// handle message
}, false);
5、错误处理
# 方式一:
source.onerror = function(event) {
// handle error event
};
## 方式二:
source.addEventListener("error", function(event) {
// handle error event
}, false);
5、主动断开连接
source.close()
6、连接状态
switch (source.readyState) {
case EventSource.CONNECTING:
// do something
break;
case EventSource.OPEN:
// do something
break;
case EventSource.CLOSED:
// do something
break;
default:
// this never happens
break;
}
参考链接
- 【阮一峰】Server-Sent Events 教程 https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html
- 【Guo YK】Server-Sent Events 的协议细节和实现 https://zhuanlan.zhihu.com/p/21308648