贫瘠之地

华北无浪漫,死海扬起帆
多少个夜晚,独自望着天

0%

HTTP Server-Sent Events 简单使用

SSE

介绍

Server-Sent Events(SSE)是一种基于 HTTP 的服务器推送技术,它允许服务器向客户端实时发送数据

通过SSE,服务器可以主动推送数据给客户端,而无需客户端发起请求

SSE 建立在 HTTP 协议之上,使用了长连接(持久连接)来实现服务器和客户端之间的实时通信

与传统的请求-响应模型不同,SSE 允许服务器在事件发生时主动向客户端发送数据。这使得服务器可以实时地向客户端推送更新的信息、通知、状态变化等

例如 GPT 等 AI 的问答窗口,往往就是用 SSE 协议,这样可以减少用户等待响应的时间;其次模拟打字机等效果也可以增强用户体验

对比

HTTP

标准的 HTTP 协议中,每个 HTTP 响应都会在发送完毕后关闭连接,HTTP/1.1 协议默认采用短连接(short-lived connections)方式

HTTP 响应的头部中会包含一个 Connection 字段,用于指示连接的状态,当该字段的值为 close 时,表示服务器会在发送完响应后主动关闭连接

也可以设置为 Keep-Alive,服务器可以自行决定是否在每个响应后关闭连接,在响应体中同样设置 ConnectionKeep-Alive 可以让客户端保持连接

基于这个机制就可以实现长轮询、SSE 等效果

WebSocket

WebSocket 是一种浏览器和服务器之间进行全双工通信的协议,同样基于 TCP

它提供了一种实时、高效的双向通信机制,允许服务器主动向客户端推送数据,而不需要客户端发起请求,WebSocket 协议相对于传统的 HTTP 协议更适合实时通信和实时更新的应用场景

WebSocket 不是 HTTP 协议,但是需要先使用 HTTP 协议进行协议升级

对比表格

协议 通信模式 场景
HTTP 单向短连接 请求-响应
HTTP SSE 单向长连接 服务器推送
WebSocket 双向长连接 双向通信

协议细节

要订阅服务器事件,客户端发出 GET 请求带有指定的 header:

  • Accepttext/event-stream 表示可接收事件流类型
  • Cache-Controlno-cache 禁用任何的事件缓存
  • Connectionkeep-alive 表示正在使用持久连接

服务器应该使用相应的响应来确认订阅:

  • Content-Typetext/event-stream;charset=UTF-8 表示标准要求的事件的媒体类型和编码
  • Transfer-Encodingchunked 表示服务器流式传输动态生成的内容,因此内容大小事先未知
  • Connectionkeep-alive 表示正在使用持久连接,
  • Keep-Alive:例如 timeout=60,服务端告知客户端保持连接的时长

数据内容

事件之间由两个换行符分隔 \n\n

每个事件由一个或多个 名称:值 字段组成,由单个换行符 \n 分隔

comment 属性则只会有 :值,客户端可以根据特征判断

1
2
3
4
5
6
7
8
9
id:this is id
event:this is event name
:this is comment
data:this is data 1

id:this is id
event:this is event name
:this is comment
data:this is data 2

简单实现

Controller

使用 Spring 实现一个 SSE 接口,Spring MVC 已经提供了对 SSE 的支持

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
@RestController
@Slf4j
public class SSEController {

@Autowired
private SSEService sseService;

@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamEvents() {
SseEmitter emitter = new SseEmitter();

// 发送事件
new Thread(() -> {
String sentence = sseService.giveMeASentence();
Stream.of(sentence.split("")).forEach(c -> {
try {
emitter.send(SseEmitter.event().data(String.valueOf(c)));
TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(30, 100));
} catch (Exception e) {
emitter.completeWithError(e);
log.error("SSE error", e);
}
});
emitter.complete();
}).start();

return emitter;
}
}

基本步骤:

  1. 使用 @RestController 注解创建一个 Controller
  2. REST 方法返回一个 SseEmitter,处理 GET 请求并产生文本/事件流 text/event-stream
  3. 创建一个新的 SseEmitter,保存它并从方法中返回
  4. 异步线程操作 SseEmitter,实现服务端的多次响应和对连接的关闭

这里为了模拟打字机效果,操作 SseEmitter 时做了一个 sleep 操作,获取文案后拆分调用 SseEmitter,响应完成后关闭连接

Service

SSEService 主要是提供一段文案,这个不重要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class SSEService {

private static final List<String> SENTENCES = new ArrayList<>();

static {
SENTENCES.add("成功不是最终的,失败不是致命的,勇气继续前进才是最重要的。 ———— 丘吉尔");
SENTENCES.add("生活就像骑自行车,你必须保持前进才能保持平衡。 ———— 爱因斯坦");
SENTENCES.add("成功的关键是把握机会的能力 ———— 爱迪生");
SENTENCES.add("不管你走了多远,决定转身总是你自己 ———— 马丁·路德·金");
}

public String giveMeASentence() {
return SENTENCES.get(RandomUtil.randomInt(SENTENCES.size()));
}
}

客户端

比如前端等,可以制作出打字机的效果

这里简单使用 Java 的 print 操作来模拟出这种效果

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
40
41
42
43
44
45
46
47
48
public class SSEClient {

public static void main(String[] args) throws IOException {
// SSE 接口的 URL
String sseUrl = "http://localhost:8080/sse";

// 创建 URL 对象
URL url = new URL(sseUrl);

// 打开 HTTP 连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

// 设置请求方法为 GET
connection.setRequestMethod("GET");

// 设置请求头
connection.setRequestProperty("Accept", "text/event-stream");

// 发送请求
connection.connect();

// 检查响应码
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// 从连接中获取输入流
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));

// 读取 SSE 事件数据
String line;
while ((line = reader.readLine()) != null) {
// 处理 SSE 事件数据
if (line.length() < 5) {
continue;
}
String eventData = line.substring(5).trim();
System.out.print(eventData);
}

// 关闭输入流
reader.close();
} else {
System.out.println("Failed to connect to SSE endpoint. Response code: " + responseCode);
}

// 断开连接
connection.disconnect();
}
}

效果

参考

SSE(Server-Send Events)实战 - 知乎 (zhihu.com)