之前用 ChatGPT 的时候,发现它返回的数据是一个字一个字蹦出来的,之前用过 WebSocket,第一次听说 Server-Sent Events,这篇文章记录对其了解学习的过程。
基础教程参考:《Server-Sent Events 教程》
要说 SSE 和 WebSocket 的区别,问 ChatGPT 它回答的更全面,此处列举出 SSE 的特点
- 传输文本数据
- 服务端单向推送
- 基于 HTTP/1.1 的长连接机制
- 兼容性更好
示例代码
server.py
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
class SSEHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/stream':
self.send_response(200)
self.send_header('Content-Type', 'text/event-stream')
self.send_header('Cache-Control', 'no-cache')
self.send_header('Connection', 'keep-alive')
self.end_headers()
count = 0
while True:
message = f"event: foo\ndata: Hello, world! ({count})\n\n"
self.wfile.write(message.encode())
count += 1
time.sleep(1)
else:
self.send_error(404)
if __name__ == '__main__':
server_address = ('', 5000)
httpd = HTTPServer(server_address, SSEHandler)
print('SSE server started on port 5000...')
httpd.serve_forever()
client.py
import requests
response = requests.get('http://127.0.0.1:5000/stream', stream=True)
for line in response.iter_lines():
if line:
print(line.decode())
也可以用 Curl 当作 Client 来发起请求
curl -v http://127.0.0.1:5000/stream
抓包查看 SSE 请求
执行上述代码后,我们可以看到 Wireshark 显示如图,从 5000 端口到 60496 端口的都是服务端发往客户端的数据报文
在 Curl 客户端侧看到的数据
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 5000 (#0)
> GET /stream HTTP/1.1
> Host: 127.0.0.1:5000
> User-Agent: curl/7.64.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: BaseHTTP/0.6 Python/3.11.2
< Date: Fri, 28 Apr 2023 06:54:02 GMT
< Content-Type: text/event-stream
< Cache-Control: no-cache
* HTTP/1.0 connection set to keep alive!
< Connection: keep-alive
<
event: foo
data: Hello, world! (0)
event: foo
data: Hello, world! (1)
event: foo
data: Hello, world! (2)
可以看到 HTTP 请求的回复中包含几个重要 Header,Content-Type
、Cache-Control
、Connection
Content-Type: text/event-stream:表示 HTTP 响应的内容类型为事件流(Event Stream),通常用于实现 Server-Sent Events (SSE) 协议。SSE 是一种基于 HTTP 的轻量级实时通信协议,它允许服务器向客户端推送一系列的事件,客户端通过订阅这些事件来实现实时更新。
Cache-Control: no-cache:表示客户端不应该缓存服务器返回的数据,每次请求都应该重新向服务器请求数据。这个头部可以防止客户端缓存过期的数据,确保客户端获得最新的数据。
Connection: keep-alive:表示客户端和服务器之间的 TCP 连接应该保持持久化,不立即关闭。这个头部可以减少客户端和服务器之间建立和关闭连接的开销,提高请求和响应的效率。
实现差异
在测试的过程中,我还使用了几个 Demo,例如基于 Python 的 Flask,以及 Node.js 版的 Server,他们的结果有一些差别,但在客户端看不到差别。
例如,Flask 的代码会单独发送 0d 0a
回车换行,也有有意义不明的 32 34
数据报文。
from flask import Flask, Response
import time
app = Flask(__name__)
@app.route('/stream')
def stream():
def generate():
count = 0
while True:
yield f"event: foo\ndata: Hello, world! ({count})\n\n"
count += 1
time.sleep(1)
return Response(generate(), mimetype='text/event-stream')
if __name__ == '__main__':
app.run(debug=True)
以下是一个 Node.js 使用 sse-express
库的实例(ChatGPT)提供给我的。
const express = require('express');
const sseExpress = require('sse-express');
const app = express();
app.get('/stream', sseExpress(), function(req, res) {
let count = 0;
setInterval(() => {
res.sse({
event: 'foo',
data: `data: Hello, world! (${count++})\n\n`
});
}, 1000);
});
app.listen(5000, () => {
console.log('SSE server started on port 5000...');
});
运行它之后,抓包发现程序会间隔性的发送 : sse-handshake
,
SSE 服务端可以间隔发送注释来保活,以避免一些 HTTP 代理服务器会主动关闭连接,以 :
开始的是内容是注释。
Legacy proxy servers are known to, in certain cases, drop HTTP connections after a short timeout. To protect against such proxy servers, authors can include a comment line (one starting with a ':' character) every 15 seconds or so.
另外 SSE 保活可以很灵活,例如发送 event: heartbeat\ndata: \n\n
在 SSE 协议中,并没有规定 heartbeat 事件是一种特殊事件。实际上,在 SSE 中,事件的类型是由开发者自己定义的,可以根据实际需求定义任意数量的事件类型。在示例代码中,heartbeat 事件只是一个自定义的事件类型,它的作用是用来维持 SSE 连接的活跃状态。
另外不使用 sse-express 包也可以实现,直接用 Express + setInterval 一把梭,直达 SSE 的本源逻辑,相较于 WebSocket,确实是更简单
const express = require('express');
const app = express();
app.get('/stream', (req, res) => {
// 设置响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 发送 SSE 数据
let count = 0;
setInterval(() => {
res.write(`event: foo\n`);
res.write(`data: Hello, world! (${count++})\n\n`);
}, 1000);
});
app.listen(5000, () => {
console.log('SSE server started on port 5000...');
});
必选的 “Event” 值
在看一些教程时,介绍在不指定 event 时默认是 “message” event, 这是协议规定的,还是一些框架自动实现的特性
SSE 协议并没有规定在不指定 event 字段时默认使用 message 事件类型。这是一些 SSE 实现框架(例如 EventSource API)在解析 SSE 数据时自动进行的处理。在这些实现中,如果事件中没有指定 event 字段,会默认将事件类型设置为 message。
需要注意的是,尽管一些 SSE 实现框架默认将事件类型设置为 message,但这并不是 SSE 协议规定的行为。因此,如果你想要编写符合 SSE 协议标准的代码,建议始终指定事件类型,避免依赖框架自动的默认处理方式。