了解ChatGPT流式响应背后的技术,优化数据流处理效率!

2023-05-06 10:40:08 浏览数 (1)

背景

我们知道 ,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请求,就可以看到

0 人点赞