likes
comments
collection
share

基于 NestJs 使用 SSE 实现GPT打字 🐔 效果

作者站长头像
站长
· 阅读数 3

SSE 本质

HTTP 协议本身是基于请求-响应模型的,这意味着客户端发起请求后,服务器响应该请求,然后通信就结束了。按照传统的 HTTP 模型,服务器无法主动向客户端发送信息。然而,通过将 HTTP 响应保持打开状态并发送数据流,我们可以实现类似于服务器主动推送的效果。

在流信息或流媒体(Streaming)的上下文中,服务器开始响应 HTTP 请求后,不是发送一个完整的响应后就关闭连接,而是保持连接打开状态,并持续发送数据片段。这些数据片段可以是文件的一部分、实时生成的数据或任何连续的数据流。客户端接收并处理这些连续的数据片段,而不需要每次需要新数据时都发起一个新的 HTTP 请求。

长轮询是流信息的一种特殊形式,客户端发送 HTTP 请求到服务器,服务器保持请求打开,直到有新数据可发送。发送数据后,连接关闭,客户端立即再次发起新的请求。这种方式虽然可以实现实时通信,但效率较低,因为每次数据传输结束后都需要重新建立连接。

SSE 通过持久 HTTP 连接高效实现单向实时数据流,适合简单推送如新闻和股票,且自带重连;而 WebSocket 支持复杂的双向交互,如在线游戏和聊天,能处理多种数据类型,但需自设重连策略。简言之,SSE 优化单向通信,WebSocket 强化双向互动。

基于 NestJs 使用 SSE 实现GPT打字 🐔 效果

总的来说,SSE 主要有以下的特点:

  1. SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。

  2. SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。

  3. SSE 默认支持断线重连,WebSocket 需要自己实现。

  4. SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。

  5. SSE 支持自定义发送的消息类型。

客户端 API

EventSource 对象允许浏览器从服务器接收通过 HTTP 协议发送的 SSE(Server-Sent Events)实时更新。

使用 SSE 时,浏览器首先生成一个 EventSource 实例,向服务器发起连接。

const eventSource = new EventSource(url);

EventSource 实例会自动监听来自服务器的消息,并通过事件来处理这些消息。可以为 EventSource 实例添加事件监听器来响应服务器发送的不同类型的事件。

基于 NestJs 使用 SSE 实现GPT打字 🐔 效果

  1. message 事件:这是最常见的事件类型,用于接收服务器发送的不确定类型的消息。

  2. open 事件:当与服务器的连接成功建立时触发。

  3. error 事件:当连接出错或无法建立连接时触发。

eventSource.onmessage = function (event) {
  console.log("新消息:", event.data);
};

eventSource.onopen = function () {
  console.log("连接成功");
};

eventSource.onerror = function () {
  console.log("连接错误.");
};

另外 close 方法用于关闭 SSE 连接。

eventSource.close();

EventSource 实例的 readyState 属性,表明连接的当前状态。该属性只读,可以取以下值:

  1. 0:相当于常量 EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。

  2. 1:相当于常量 EventSource.OPEN,表示连接已经建立,可以接受数据。

  3. 2:相当于常量 EventSource.CLOSED,表示连接已断,且不会重连。

服务端实现

服务器向浏览器发送的 Server-Sent Events(SSE)数据必须是 UTF-8 编码的文本。SSE 协议是基于文本的,它要求传输的数据使用 UTF-8 编码,这确保了数据的国际化和兼容性,允许消息内容包含任何 Unicode 字符。

具有如下的 HTTP 头信息:

基于 NestJs 使用 SSE 实现GPT打字 🐔 效果

数据内容用 data 字段表示。

data:  message\n\n

如果数据很长,可以分成多行,最后一行用\n\n 结尾,前面行都用\n 结尾。

data: begin message\n
data: continue message\n\n

下面是一个发送 JSON 数据的例子。

data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

id 字段用于设置事件的唯一标识符。这对于客户端在断开连接后重连时,请求从上次接收的最后一个事件之后继续接收事件很有用。

客户端可以在重连时发送 Last-Event-ID 头,其中包含上次接收到的事件 ID,从而允许服务器只发送新的事件。

基于 NestJs 使用 SSE 实现GPT打字 🐔 效果

event 字段用于指定事件的类型。客户端可以根据事件类型来决定如何处理接收到的数

这允许一个 SSE 连接用于传输多种类型的消息,而客户端可以通过监听特定类型的事件来进行响应。

服务器可以用 retry 字段,指定浏览器重新发起连接的时间间隔。

retry: 10000\n

两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。

实现打字 🐔 效果

import { Controller, Sse } from "@nestjs/common";
import { Observable } from "rxjs";

@Controller("events")
export class EventsController {
  @Sse("sse")
  typing(): Observable<any> {
    const text = "PHP是世界上最好的语言吗";

    return new Observable<any>((observer) => {
      let currentIndex = 0;
      const intervalId = setInterval(() => {
        if (currentIndex < text.length) {
          // 发送正常消息
          observer.next({
            data: JSON.stringify({
              data: text.substring(0, ++currentIndex),
              isEnd: false,
            }),
          });
        } else {
          // 发送结束信号
          observer.next({ data: JSON.stringify({ data: "", isEnd: true }) });
          clearInterval(intervalId);
          observer.complete();
        }
      }, 100);
    });
  }
}

在上面的代码中,我们使用了 @Sse 装饰器在 EventsController 控制器中定义了一个名为 typing 的 SSE 端点。当客户端向/events/sse 发起 GET 请求时,将触发这个 typing 方法。@Sse 装饰器标记的方法预期返回一个 Observable 对象,这个对象用于向客户端推送消息。

typing 方法内部创建了一个 Observable 对象,这个对象用 setInterval 函数定时每 100 毫秒向客户端发送一段文本的增长部分,模拟打字效果。

并且定义了一段初始文字,然后逐字发送这段文本,每次发送时文本长度增加一个字符,直到整个文本被发送完毕。

当整个文本发送完毕后(即 currentIndex 等于文本长度时),再发送一个消息,其 isEnd 标记为 true,表示消息发送完毕。随后清除定时器并调用 observer.complete()结束 Observable 流。

<body>
  <div id="message"></div>

  <script>
    const eventSource = new EventSource(
      "http://localhost:8080/api/v1/events/sse"
    );

    eventSource.onmessage = function (event) {
      const { data, isEnd, ...rest } = JSON.parse(event.data);
      console.log(event);
      if (!isEnd) {
        document.getElementById("message").innerHTML = data;
      } else {
        console.log("Message complete");
        eventSource.close();
      }
    };
  </script>
</body>

上面这段前端代码就没啥好说的了,之前的已经讲过了,当接收到 isEnd 为 true 的时候则中断连接。

具体效果如下图所示:

基于 NestJs 使用 SSE 实现GPT打字 🐔 效果

参考链接

总结

SSE (Server-Sent Events) 技术是基于 HTTP 协议的轻量级实时通信解决方案。它主要优点包括服务端推送功能、自动断线重连机制以及其轻量级特性。然而,SSE 也存在一些局限性,例如无法支持双向通信、同时打开的连接数量有限制,以及仅支持 GET 请求等。

在 Web 应用中,SSE 能够有效实现股票市场数据实时更新、日志信息推送、实时显示聊天室人数等功能。但是,SSE 并非适用于所有实时数据传输场景。对于那些对高并发、高吞吐量以及极低延迟有严格要求的应用场景,WebSocket 技术可能是更好的选择。相对而言,在对系统资源消耗有较低要求的轻量级推送场景中,SSE 显示出了其独特的优势。因此,在选择实时数据更新方案时,开发者需要针对应用的具体需求和场景特点做出恰当的选择。