背景
我们知道 ,ChatGPT API是一个OpenAI 的聊天机器人接口,它可以根据用户的输入生成智能的回复。为了提高聊天的流畅性和响应速度,ChatGPT API采用了SSE作为服务端推送技术。SSE是一种HTML5技术,它允许服务器向客户端发送事件,从而实现服务器端推送。相对于WebSockets或长轮询技术,SSE提供了更简单的方式来实现服务器端推送,并且支持更广泛的客户端和服务器端。
SSE在ChatGPT API中的应用如下:
- 客户端通过一个HTTP GET请求建立与服务器的连接,并指定接收text/event-stream类型的数据。
- 服务器在收到请求后,不立即返回响应,而是保持连接打开,并根据用户的输入生成回复。
- 服务器在生成回复后,将回复作为一个事件发送给客户端,并保持连接打开,等待下一个输入。
- 客户端在收到事件后,解析事件中的数据,并显示在聊天界面上。
- 客户端和服务器之间可以通过同一个连接持续交换数据,直到客户端关闭连接或者服务器出现异常。
通过SSE技术,ChatGPT API可以实现流式响应,即服务器不需要等待客户端的请求,就可以主动发送数据给客户端。这样可以减少网络延迟和资源消耗,提高聊天的效率和质量。
在Web开发中,有时我们需要从服务器端实时地向浏览器端发送数据,以提高用户体验和交互效果。例如,聊天应用、股票行情、新闻更新等场景都需要服务器端主动地推送数据给浏览器端。那么,如何实现这样的功能呢?没错,依然是SSE。
SSE相比于其他技术方案,SSE有以下几个优势:
- SSE使用更简单,不需要添加任何新组件,只需使用现有的后端语言和框架即可。
- SSE完全复用现有的HTTP协议,因此可以直接运行于现有的代理服务器和认证技术。
- SSE在浏览器端提供了原生的EventSource对象,可以方便地监听和处理服务器发送的事件。
- SSE支持断线重连和消息追踪的功能,可以保证数据的完整性和一致性。
以下是一个使用EventSource实现实时推送消息的例子:
HTML代码:
代码语言:javascript复制html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>EventSource Example</title>
</head>
<body>
<div id="messages"></div>
<script>
var source = new EventSource('/message-stream');
source.onmessage = function(event) {
var message = JSON.parse(event.data);
var div = document.createElement('div');
div.innerHTML = message.user ': ' message.text;
document.getElementById('messages').appendChild(div);
};
</script>
</body>
</html>
Node.js服务器端代码:
代码语言:javascript复制var http = require('http');
http.createServer(function(request, response) {
response.writeHead(200, {
'Content-Type': 'text/event-stream',// 这个表示服务端流式响应
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
setInterval(function() {
var message = {
user: 'John',
text: 'Hello, world!'
};
response.write('data: ' JSON.stringify(message) 'nn');
}, 1000);
}).listen(3000);
当然,SSE也有一些局限性,比如:
- SSE只支持从服务器到客户端的单向数据传输,如果需要双向通信,还需要使用Ajax或WebSocket等技术。
- SSE只支持文本格式的数据,如果需要传输二进制数据,还需要使用Base64等编码方式。
- SSE在浏览器方面支持不够广泛,IE和Edge几乎不支持SSE。
因此,在选择使用SSE技术之前,需要根据具体的应用场景和需求进行权衡。如果只需要从服务器向客户端发送更新频繁、低延迟的文本数据,并且不考虑IE和Edge浏览器的兼容性问题,那么SSE是一个很好的选择。
下面我们来具体介绍一下SSE的技术细节和实现方法。
SSE的通信协议
要使用SSE技术,首先需要了解它的通信协议。SSE通信协议很简单,本质上就是一个客户端发起的HTTP GET请求,服务器在接收到该请求后,返回200 OK状态,并附带以下响应头:
代码语言:javascript复制Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
这些响应头的含义分别是:
- Content-Type: text/event-stream 表示响应的内容类型是SSE格式的文本流。
- Cache-Control: no-cache 表示响应的内容不应该被缓存,以保证实时性。
- Connection: keep-alive 表示响应的连接应该保持打开,以便服务器端持续发送数据。
在返回响应头之后,服务器端就可以开始向客户端发送数据了。SSE格式的数据是由一系列的事件组成的,每个事件都有以下几个部分:
- 一个或多个字段,用冒号和空格分隔字段名和字段值,每个字段占一行。
- 一个空行,表示事件的结束。
SSE支持以下几种字段:
- data: 表示事件的数据内容,可以有多行,每行都以data: 开头。
- id: 表示事件的唯一标识符,用于断线重连和消息追踪。
- event: 表示事件的类型,用于区分不同的事件。
- retry: 表示断线重连的时间间隔,单位是毫秒。
例如,一个简单的SSE事件可以写成这样:
代码语言:javascript复制data: Hello, world!
id: 1
event: message
或者这样:
data: Hello,
data: world!
id: 1
event: message
注意,每个事件必须以一个空行结束,否则客户端无法识别事件的边界。另外,如果一个字段没有值,那么只写字段名即可,例如:
代码语言:javascript复制event:
表示一个没有类型的事件。
服务器端可以根据需要发送任意数量和类型的事件,客户端会按照接收到的顺序处理这些事件。如果客户端在接收数据过程中发生了断线或错误,那么它会尝试重新连接服务器,并发送上次接收到的事件id作为Last-Event-ID请求头。服务器端在收到这个请求头后,可以根据id判断是否需要重发之前的事件。
SSE的浏览器实现
要在浏览器端使用SSE技术,只需要使用原生的EventSource对象即可。EventSource对象是一个封装了SSE通信协议的对象,它提供了以下几个属性和方法:
- url: 表示SSE服务端的URL地址。
- withCredentials: 表示是否携带cookie等认证信息。
- readyState: 表示SSE连接的状态,有三种可能的值:0(CONNECTING),1(OPEN),2(CLOSED)。
- close(): 表示关闭SSE连接。
- onopen: 表示SSE连接打开时触发的回调函数。
- onmessage: 表示接收到默认类型(没有event字段)的事件时触发的回调函数。
- onerror: 表示发生错误时触发的回调函数。
除了这些属性和方法外,EventSource对象还支持addEventListener方法,可以用来监听自定义类型(有event字段)的事件。例如:
代码语言:javascript复制var source = new EventSource('http://example.com/sse');
source.onopen = function() {
console.log('SSE connection opened');
};
source.onmessage = function(e) {
console.log('Received default event:', e.data);
};
source.onerror = function(e) {
console.log('SSE error:', e);
};
source.addEventListener('message', function(e) {
console.log('Received message event:', e.data);
});
source.addEventListener('update', function(e) {
console.log('Received update event:', e.data);
});
使用EventSource对象非常简单,只需要创建一个实例,并传入SSE服务端的URL地址即可。然后就可以通过onopen、onmessage、onerror等属性或addEventListener方法来监听和处理服务器发送的事件了。
和websock的优缺点对比
WebSockets是一种双向通信协议,它允许客户端和服务器之间建立一个全双工的TCP/IP连接,并在连接上交换二进制或文本数据。WebSockets相比于SSE有以下优缺点:
优点:
- WebSockets是真正的双向通信协议,客户端和服务器可以随时向对方发送数据,而不需要等待对方的请求或响应。
- WebSockets支持二进制数据传输,这对于传输图片、音频、视频等大量数据非常有利。
- WebSockets可以绕过HTTP协议的限制,例如缓存、代理、头部等,实现更高效和灵活的通信。
缺点:
- WebSockets相对于SSE更复杂,需要额外的组件和库来支持,在一些老旧的浏览器或服务器上可能不兼容。
- WebSockets需要占用一个独立的端口号,这可能会导致一些防火墙或安全策略的问题。
- WebSockets由于是二进制协议,调试起来比较困难,需要专门的工具或库来解析数据。
安全性
服务端推送技术涉及到客户端和服务器之间的数据传输,因此需要考虑安全性问题。不同的服务端推送技术有不同的安全性特点:
- Ajax短轮询和长轮询和基于iframe的流都是基于HTTP协议的,因此可以使用HTTPS协议来加密数据,防止中间人攻击或数据泄露。但是,这些技术都需要频繁地发送请求和响应,这可能会增加服务器的负载和网络的拥塞,也可能会被一些恶意的请求或响应干扰。
- SSE也是基于HTTP协议的,因此也可以使用HTTPS协议来保证数据的安全性。SSE相比于Ajax轮询技术,只需要建立一次连接,就可以持续地接收服务器的事件,这样可以减少网络开销和服务器压力。但是,SSE只支持单向的通信,即服务器向客户端发送数据,客户端不能向服务器发送数据。这可能会限制一些交互功能的实现。
- WebSockets是基于TCP/IP协议的,因此可以使用WSS协议来加密数据,防止数据被窃取或篡改。WebSockets支持双向的通信,客户端和服务器可以随时互相发送数据,这样可以实现更丰富和灵活的交互功能。但是,WebSockets需要额外的端口号和组件来支持,在一些环境中可能会遇到兼容性或安全性的问题。
综上所述,服务端推送技术在ChatGPT API中有着重要的应用,它可以提高聊天机器人的响应速度和用户体验。不同的服务端推送技术有各自的优缺点和安全性特点,需要根据具体的场景和需求来选择合适的技术。
koa接口封装为 流式响应demo
代码语言:javascript复制/*
* 使用 koa 实现一个 post 的 sse 请求
请求方式 post
请求 path /api
参数straem 控制是否流式响应,stream=true 表示流式响应,否则普通响应
*/
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-bodyparser");
const app = new Koa();
const router = new Router();
// Use bodyParser middleware to parse request body
app.use(bodyParser());
router.post("/api", async (ctx) => {
const { stream } = ctx.request.body;
if (stream == true) {
// Set response headers for SSE
ctx.response.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
// Send SSE header
ctx.res.statusCode = 200;
ctx.res.write(":oknn");
let count = 0;
// Set up SSE interval for streaming response
// Generate SSE data
while (count < 5) {
const data = {
message: "Hello, world!",
timestamp: Date.now(),
};
count ;
await new Promise((resolve) => setTimeout(resolve, 1000));
// Send SSE event
ctx.res.write(`data: ${JSON.stringify(data)}nn`);
}
ctx.res.write(":donenn");
// Handle closing of SSE connection
ctx.req.on("close", () => {
ctx.response.end();
});
} else {
// Send non-streaming response
ctx.body = { message: "Hello, world!" };
}
});
app.use(router.routes());
app.listen(3000);
使用postman来发起post请求,就可以看到