概述
使用AI服务的时候,会发现接口的返回看起来内容都是一点一点追加过来的。但增量式更新,通过接口轮询,前端实时状态同步,也是能做的。有什么不同呢?
(1)打开F12观察单个接口返回,二者长得不一样
图1:普通接口示意图:
图2:流式接口介意图:
(2)接口数量不一样
接口轮询每一次向服务端的询问,需要发起一次新的HTTP请求,而且即使有keep-alive的加持,在数据量大的情况下,也需要建立多次tcp连接才能完成数据的获取;而流式请求只需建立一次HTTP连接以及TCP连接,就可以获得所有的数据。在一次连接过程中,持续地与服务端通信,进行数据的接收和交换。
-
为什么传统的HTTP接口不能流式传输
目前项目基于HTTP协议开发,HTTP请求-响应模型是基于客户端发起一次请求,服务器返回一次响应的设计。在标准的HTTP协议中,一次请求只能对应一次响应,且响应内容在请求发出后立即返回给客户端。因此,到达客户端的数据也只能是针对其请求的单次整体数据返回,没法持续向客户端输送数据。可以尝试在服务端的连续send两次数据到客户端,服务端会报错。(项目例子:Render在正常返回和error的处理中,连续两次向客户端send了数据,然后报错了) -
“流式”的接口怎么做
得让服务器能够多次向客户端发送数据,比如:借助一些支持多次数据返回的协议(websocket),或者支持服务端多次发送数据的规范(SSE)。
适用的场景:一些后端数据更新频繁,需要实时向前端反馈的场景,如后台消息定时推送、定向推送等功能就很适配流式传输。一方面前端不用不停轮询,减少不必要的资源浪费和不好把控的轮询粒度,服务端也可以避免不断接收来自各个客户端的查询所带来的巨大的工作负载。
SSE和WebSocket
SSE(Server Send Event)
SSE是一个Web官方提供的,基于HTTP协议的,支持一次请求内,服务端多次向客户端发送数据的API。它发起一个请求很简单:
// 客户端发送请求
// 1. new一个SSE实例
const sse = new SSE('http://localhost:8080/sse'); // 从此刻开始,get请求就发出去了
// 2. 监听来自服务端的响应数据
sse.onmessage((e) => {
console.log(e.data);
})
// 3. 错误处理
sse.onerror((e) => {
console.error(e);
})
然后给出一个简易的服务端的实现:
const express = require('express');
const app = express();
const port = 8080;
const SseStream = require('ssestream').default;
app.get('/sse', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream'); // 响应格式必须是事件流
res.setHeader('Cache-Control', 'no-cache'); // 取消数据缓存
res.setHeader('Connection', 'keep-alive'); // 浏览器默认启用
const sseStream = new SseStream(req);
sseStream.pipe(res);
// 每隔一秒钟向客户端发送一次当前时间
const pusher = setInterval(() => {
sseStream.write({
event: 'server-time',
data: new Date().toTimeString()
})
}, 1000);
})
请求:
- 官网提供的SSE Api只能支持get请求,由于URL有最大长度限制,所以间接的,对请求能携带的参数体量有限制,适用于请求比较简单的接口;
- 不支持配置自定义请求头
- 在单次连接中,只能被动监听服务端发送来的事件,在过程中不能再向服务端发送数据,也不能主动与浏览器断开连接,终止浏览器的响应
响应:
发送事件的服务器端脚本需要使用 text/event-stream MIME 类型响应内容。每个通知以文本块形式发送,并以一对换行符结尾。
- 响应的内容必须要是
text/event-stream
格式,客户端拿到数据之后,如果期望的是结构化数据,需要反序列化;
2.可以返回各种事件,只要返回的数据格式符合:
{
"type": "customEvent", // 事件类型
"data": "[1,2,3]\n\n" // 结构化数据需要序列化
}
WebSocket
前面说到的SSE,是建立在HTTP协议上的。WebSocket与HTTP一样,是一个应用层网络协议。
这个协议是一个全双工通信协议,客户端和服务端在建立连接后,只要没有彻底断开此次TCP链接,在这期间,服务端和客户端任意一方均可向另一方发送数据。
用一张图说明一下HTTP连接、SSE以及WebSocket之间的对比:
WebSocket的客户端的实现和SSE很类似:
const ws = new WebSocket("ws://localhost:8080");
ws.onopen = () => {
console.log("连接成功");
};
ws.onmessage = (e) => {
console.log("收到响应...");
console.log(e.data);
};
服务端没有提供统一的API,需要依赖一些第三方库。
WebSocket子协议:WebSocket子协议是一种扩展WebSocket通信功能的协议。WebSocket本身是一个基于TCP的应用层协议,它允许在单个TCP连接上进行全双工通信。然而,WebSocket并不强制使用特定的消息传递协议,这意味着开发者或开发者社区可以定义和实现各种子协议来增强WebSocket的功能。这些子协议通常作为WebSocket之上的一层,提供额外的功能或特性,如消息路由、消息解释、认证等,以适应不同的应用场景和需求。
SSE和WebSocket的对比
SSE和WebSocket有很多的相似之处:(1)都能做这种流式的传输,支持长连接,(2)都是事件监听的方式,接收和发送的数据基本都是字节流(websocket还支持二进制流)等。除了一些机制上的不同,但是二者在流式传输方面还有一些差异:
SSE的一些不足之处
- 实例不能重复使用,哪怕请求的是同一个资源,每次请求都需要重新new一个SSE实例;
- 虽然目前SSE被广泛用在AI领域,问了问题就能立刻得到答案,看起来减少了用户的等待时间,但是这种只能对后端已有完整数据或者单次数据计算比较简单的场景,当服务端需要大量运算才能得到结果时,SSE的首个字节响应市场同样很长,也需要像传统HTTP请求一样,加一些loading动效来优化用户体验;
- 目前SSE官方提供的Web API有一些明显的限制,很多时候需要二次封装或者模拟实现来达到类似的效果,但现在好用的包不多,现在客户端流传的很广的就是微软开发的那一版,但是那一版对于自定义事件的实现并不贴合MDN的规范。