Franz`s blog

SSE 的原理以及java下的简单实现

前段时间看到gpt返回时的部分输出的效果,觉得很有意思,所以研究了下是怎么实现的。

主要是通过sse(Server-Sent Events)技术实现的这种效果

简介

SSE(Server-Sent Events)和WebSocket都是用于实现服务器和客户端之间的实时通信的技术,但它们有一些区别。

SSE是一种单向通信协议,它允许服务器向客户端发送事件流。客户端通过一个持久化的HTTP连接接收事件流,这个连接可以保持打开状态,直到客户端关闭连接或服务器关闭连接。

WebSocket是一种双向通信协议,它允许服务器和客户端之间进行实时双向通信。WebSocket通过一个持久化的TCP连接实现双向通信,客户端和服务器可以随时发送消息。

sse

1.如何保持长连接

在http 1.1下Keep-Alive模式默认开启,服务端会通过tcp协议发送一个不带数据的ack请求,确保客户端的在线。

2.如何判断数据完整性

当客户端接收到数据后会通过判断Content-Length或者Transfer-Encoding响应头判断哪一部分是完整数据。

3.客户端如何判断是sse

通过客户端设置响应头Content-typetext/event-stream

image-20230519105824786

java简单代码实现

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
@WebServlet(urlPatterns = "/streaming",asyncSupported = true)
public static class StreamingServlet extends HttpServlet {

private static final long serialVersionUID = 1L;
private ExecutorService executorService = Executors.newCachedThreadPool();

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 设置响应内容类型为SSE
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
// 开启异步上下文
final AsyncContext asyncContext = request.startAsync();
// 使用线程池执行任务
executorService.execute(() -> {
try {
PrintWriter writer = response.getWriter();
for (int i = 0; i < 10; i++) {
// 发送数据
writer.write("data: " + i + "\n\n");
writer.flush();

// 模拟数据产生的延迟
Thread.sleep(1000);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
asyncContext.complete();
}
});
}
@Override
public void destroy() {
// 关闭线程池
executorService.shutdown();
}
}

效果

image-20230519110903114

AsyncContext

看到别人实现sse时都使用的AsyncContext,但是并不知道有啥用,查询资料后得知

使用final AsyncContext asyncContext = request.startAsync();可以实现Servlet 线程将请求转交给一个异步线程来执行业务处理,线程本身返回至容器,等待异步线程任务结束后可以直接生成响应数据。