Loading

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;
}

参考链接

posted @ 2022-05-13 17:45  jesn  阅读(6866)  评论(0编辑  收藏  举报