最近在玩chatGPT的时候发现网页版上它是逐字输出的,有时候还会卡顿一下再接着输出,我就好奇它是使用什么方式进行数据传输的,通过浏览器开发者工具发现,它使用的是EventStream方式持续输出数据到前端的,了解之后发现这种方式在某些特定的场景下非常有用。

EventStream是什么

EventStream是HTML5的新内容,特点就是返回的Content-Typetext/event-stream,其采用的是http协议,这种方式的请求浏览器不会关闭这个http连接,而是一直等待服务器的持续返回,达到一种长连接的效果,不过它是单向的,除了发送请求的时候可以传送一次数据到服务器,后面便只能等待服务器返回数据而不能发送数据,相当于是一个持续了很长时间的普通http请求,因此它需要浏览器的支持(微软的IE和Edge浏览器并未实现,可以采用第三方库实现)

优点

. 因为是http协议,所以无需其余实现即可使用
. 默认支持断线重连,不需要手动维护连接
. 支持自定义发送消息的类型
. 在单项传输的场景里面非常有用

缺点

. 只支持utf-8编码,不支持二进制数据
. 只支持单项传输,客户端不能主动发送数据给服务端
. 浏览器对最大打开连接数有限制

场景示例

异步任务的执行进度

比如某些政府系统会有大量导出报表的需求,这些报表数据量大,服务端采用异步处理的方式,前端通过接口去获取任务进度,这时候通过WebSocket去实现就显得很麻烦,使用轮询处理不仅仅更新数据不及时,而且还需要不断重复建立http请求,这时候使用SSE的方式就显得极其简便和高效

具体实现

服务端代码

在spring boot中实现非常简单,接口内只要通过PrintWriter·flush()即可不断将数据返回给客户端,这里采用PrintWriter.checkError()是为了防止异常无法检测导致资源无法释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping("/sse")  
public void sse(HttpServletResponse response) throws IOException, InterruptedException {
// 设置内容传输方式和编码集
response.setContentType("text/event-stream");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
int i = 0;
while (true){
writer.write("data: number:" + i++ + "\n\n");
// 这里不要采用 writer.flush() 去刷出数据,因为它内部捕获了异常,如果客户端断开就无法感知
if (writer.checkError()){
// 发生异常
return;
}
Thread.sleep(1000);
}
}

客户端代码

其实直接在浏览器访问上面接口就可以看到持续返回的内容:
image.png

不过在实际项目中肯定需要异步处理的方式,这里可以使用EventSource对象,不过浏览器不支持的话就需要使用开头说的第三方库

1
2
3
4
var source = new EventSource('http://localhost:8080/sse')
source.onmessage = function(event){
console.log(event.data)
}

运行结果:
image.png

是不是发现有点不一样,相比于用浏览器直接访问,这种方式少了前面的data:,这是因为EventSource语法约定的,如果服务端没有返回data:格式,那么上面EventSourceonmessage监听函数里就不会有内容返回,而且必须以\n\n表示一句话的结束,否则一直接不到内容,具体的语法使用规则这里不做赘述,可以直接看EventSource文档