抓包学习 Server-Sent Events(SSE)

Published: 2023-04-28

Tags: Wireshark

本文总阅读量

之前用 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-TypeCache-ControlConnection

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 协议标准的代码,建议始终指定事件类型,避免依赖框架自动的默认处理方式。

参考