Server-Send Event

0. 概念

Server-Sent Events(SSE)是一种用于在服务器和客户端之间实现实时单向通信的技术,建立在HTTP协议的基础上,最简单的标志就是其Reponse Header中的Content-Typetext/event-stream,我们熟知的ChatGPT来回答用户的问题使用的就是SSE

该类型的响应弥补了传统的HTTP请求无法达到的服务器主动推送数据,其返回的数据是以数据流的方式进行,同时与客户端所建立的连接为持久连接

Server-sent event overview of the communicaiton between the server and the client

1. 特点

  • SSE是单向数据通道的,数据源仅能从服务端流向客户端,无法由客户端发送数据到服务端,相比较之下,WebSocket则是全双工通道
  • SSE的响应有特定的格式:data: {text_content}\n\n数据结束时必须保证末尾有2个回车,同时数据响应仅支持字符串,也就是纯文本的数据流,每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔,每个message内部由若干行组成
  • SSE如果涉及到二进制数据则需要编码后传送,WebSocket默认支持传送二进制数据
  • SSE轻量级,使用简单,而WebSocket协议相对来说比较复杂
  • SSE默认支持断线重连,而关于断线重连WebSocket则需要自定义实现
  • SSE支持自定义发送的消息类型,再由客户端监听对应的消息类型
  • SSE建立长时间的连接,使得服务器可以实时地将数据推送给客户端,而无需客户端频繁地发起请求
  • 由于SSE会保持长时间的连接,因此如果是Tomcat的服务器,一个长连接会占用一个线程数,可能会导致资源浪费,可考虑Node这类服务器,来确保所有的连接用的都是一个线程,资源消耗相对少一些

2. 使用方式

Tips

  • 本文所使用的客户端是支持JavaScript的浏览器

  • SSE API只支持GET读请求,不支持POST请求

a. 检测是否支持SSE API

目前除了老版本的 IE/Edge,其他浏览器都支持,可通过以下代码进行检查,兼容性也可以通过Mozilla文档进行查看

1
if ('EventSource' in window) {...}

b. 初始化EventSource实例

可通过以下方法初始化EventSource实例,该实例用于监听SSE连接的消息事件

1
var source = new EventSource(url);

c. EventSource实例的状态

该实例存在一个readyState属性,该属性只可读,用于判断该EventSource实例的状态,值为以下:

  • 0: 相当于常量EventSource.CONNECTING,表示连接还未建立,或者断线正在重连
  • 1: 相当于常量EvnetSource.OPEN,表示连接已经建立,可以接受数据
  • 2: 相当于常量EventSource.CLOSED,表示连接已断,且不会重连

d. 事件监听

可以对EventSource实例进行事件监听,事件监听指的是服务器推送数据到客户端,同时支持对不同的事件进行监听:

连接一旦建立,就会触发open事件,可以在onopen属性定义回调函数,可参照以下2种方式:

1
2
3
4
5
6
7
source.onopen = function (event) {
// do something...
};

source.addEventListener('open', function (event) {
// do something...
}, false);

默认情况下,服务端返回的都是message类型的事件,因此可根据以下代码对message类型的事件进行监听:

1
2
3
source.addEventListener('message', (e) => {
const data = e.data;
}, false)

e. 断开EventSource连接

可通过source.close()方法断开EventSource的连接

f. SSE服务端响应头

服务端发送SSE数据时,必须带有以下响应头,其中Content-Type的类型必须是text/event-stream

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

g. SSE服务端响应内容

响应内容统一为以下格式,其中需要注意的是,响应内容的最后一行必须以2个回车符号\n表示

[类型]: value,其中类型包含以下四种类型:

  • data: 表示该行为数据行,其内容为要返回的数据
  • event: 表示该响应内容的事件类型,可自定义事件类型,默认情况下该类型是message
  • id: 表示事件ID,可以简单地理解成是数据的编号
  • retry: 用于指定浏览器重新发起连接的时间间隔,两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错

h. 注释

服务器可以通过发送以下格式的数据来表示注释,可以通过间断性地发送注释来确保连接不中断,注释一般是冒号开头的行:

1
: This is a comment

i. EventSource增强

由于原版的EventSource API不支持POST方法,默认发起的是GET方法,因此建议使用基于Fetch API开源的增强包: @microsoft/fetch-event-source来更好地解决以上问题

3. 样例

以下案例采用的是Python语言 + Flask框架 + HTML进行打样,其中,页面会初始化EventSource实例,并由后端提供一个事件推送API,该API会间隔500毫秒轮换推送一个receiveTextMessage和一个receiveJsonMessage事件,其中receiveTextMessage事件负责推送带有字符串的内容,receiveJsonMessage事件负责推送带有JSON的内容,该接口一共会推送20个事件,如下所示:

  • index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE Example</title>
</head>
<body>
<h1>Random Letter Stream:</h1>
<div id="events"></div>

<script>
const eventSource = new EventSource('/events');
const eventsDiv = document.getElementById('events');

eventSource.addEventListener('receiveTextMessage', (e) => {
const textData = e.data
eventsDiv.innerHTML += textData
})

eventSource.addEventListener('receiveJsonMessage', (e) => {
const jsonData = JSON.parse(e.data)
eventsDiv.innerHTML += JSON.stringify(jsonData)
})
</script>
</body>
</html>
  • app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import json
import random
import string
import time

from flask import Flask, render_template, Response

app = Flask(__name__)

counter = 1


# 响应模板
def event_stream():
global counter
while counter < 20:
time.sleep(0.5)
counter += 1
if counter % 2 == 0:
random_letter = random.choice(string.ascii_letters)
event_data = f"event:receiveTextMessage\ndata: {random_letter}\n\n"
else:
json_data = json.dumps({"username": "zchengb"})
event_data = f"event:receiveJsonMessage\ndata: {json_data}\n\n"
yield event_data


@app.route('/')
def index():
return render_template('index.html')


@app.route('/events')
def sse():
return Response(event_stream(), content_type='text/event-stream')


if __name__ == '__main__':
app.run(debug=True)

页面输出效果如下所示:

image-20230826014350063