likes
comments
collection
share

我要再看一遍 ChatGPT 打字机流式回复(SSE&WebSocket)

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

前言

相信我们现在对 ChatGPT 已经非常熟悉了,知道它很强大,能够与我们人类进行自然语言的交流。在 ChatGPT 的网页端,我们最直观能感受到的除了它惊人的"聪明",另一个在视觉上的感受就是,它的回答是一个字一个字的出来的,这样的效果叫做打字机流式回复,这种效果能够增强用户体验。本文将介绍打字机流式效果,并说明如何使用WebSocketSSE(Server-Send Events)这两种方式实现前端接收内容的流式效果。

我让 ChatGPT 使用 js 写一个冒泡排序的功能,所呈现出来的打字机效果,看下图👇

我要再看一遍 ChatGPT 打字机流式回复(SSE&WebSocket)

所需技术

为了实现打字机流式效果,我们需要一种与服务器保持持久连接的方法。这就是WebSocket和SSE技术的用武之地了。

WebSocket是一种协议,它允许客户端和服务器之间建立一个持久的连接,以便双方可以实时进行通信。客户端首先发送一个HTTP请求,然后服务器返回一个101 Switching Protocols响应。之后,客户端和服务器就可以在建立的连接上进行双向通信。通过这种方式,我们可以实现用户和服务端之间的实时交互。

SSE(Server-Sent Events)是另一种与服务器保持持久连接的方法。相比WebSocket,SSE更加简单,它只支持服务器向客户端发送数据,而不支持客户端向服务器发送数据。在SSE中,客户端通过发送一个HTTP请求来建立连接,然后服务器在这个连接上发送消息,直到连接关闭。与WebSocket不同,SSE使用的是基于文本的协议,可以轻松地在浏览器中解析。

为了方便和从原生的角度来演示,这里就使用 html+ts 的方式来演示效果✌️

初始化项目

安装依赖

为了获得 ts 的提示,我们使用 pnpm 进行初始化一个 ts 项目。

  • @types/node:用来获得 ts 类型提示的支持。
  • ts-node-dev:用来启动一个 http 服务,并能够自动监听文件的修改和重启服务,也就是热更新啦。
  • ws:一个 Node.js WebSocket库。
  • @types/ws:获得对 ws 的提示。
pnpm init
pnpm i @types/node -D
pnpm i ts-node-dev -D
pnpm i ws
pnpm i @types/ws

创建文件

然后在根目录下创建index.htmlindex.ts,并写入以下内容👇

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>打字机</title>
    <style>
      button {
        cursor: pointer;
      }
      #msg {
        height: 300px;
        padding: 10px;
        border: 1px solid red;
      }
      .btn {
        margin-top: 10px;
      }
    </style>
  </head>
  <body>
    <div id="msg">还未发送任何请求</div>
    <div class="btn">
      <button id="webSS">链接WebSocket</button>
      <button id="webST">关闭WebSocket</button>
      <button id="sse">使用SSE</button>
    </div>
    <script>
      const msg = document.getElementById('msg');
      const webSS = document.getElementById('webSS');
      const webST = document.getElementById('webST');
      const sse = document.getElementById('sse');
      let ws;

      webSS.addEventListener('click', (e) => {
        e.preventDefault();
        webSFn();
        console.log('webS');
      });
      webST.addEventListener('click', (e) => {
        e.preventDefault();
        ws.close();
        console.log('webST');
      });

      sse.addEventListener('click', (e) => {
        e.preventDefault();
        SSE();
      });

      function webSFn() {
        ws = new WebSocket('ws://localhost:3001');

        ws.addEventListener('open', function (event) {
          console.log('Connection opened.');
          // 在这里可以向服务端发送消息,例如:
          ws.send('打字机');
        });

        ws.addEventListener('message', function (event) {
          console.log(`Received message: ${event.data}`);
          // 在这里可以对服务端发送的消息进行处理
        });

        ws.addEventListener('close', function (event) {
          // 在这里可以对连接关闭进行处理
        });
      }

      function SSE() {
        const eventSource = new EventSource('/sse');
        eventSource.addEventListener('exampleSSE', (event) => {
          console.log(`Received message: ${event.data}`);
          // 在这里可以对服务器发送的消息进行处理
        });

        eventSource.addEventListener('open', function (event) {
          console.log('Connection opened.');
        });

        eventSource.addEventListener('error', function (event) {
          console.error('Error occurred:', event);
          // 在这里可以对连接错误进行处理
        });
      }
    </script>
  </body>
</html>

在 http 服务中,我们把根路径/作为 index.html输出的路由,/sse作为 SSE 请求的路由

import { createServer } from 'http';
import WebSocket, { createWebSocketStream } from 'ws';
import fs from 'fs';
const server = createServer((request, response) => {
  const url = new URL(request.url as string, 'http://127.0.0.1');
  if (url.pathname === '/') {
    const stream = fs.createReadStream('./index.html');
    stream.pipe(response);
  } else if (url.pathname === '/sse') {
    response.end('sse');
  } else {
    response.end('');
  }
});

server.listen('3000', () => {
  console.log('http://127.0.0.1:3000');
});

const ws = new WebSocket.Server({ port: 3001 });

ws.on('connection', function (socket) {
  console.log('A client connected.');

  socket.on('message', function (message) {
    console.log(`Received message: ${message}`);
    // 在这里可以对客户端发送的消息进行处理
  });

  socket.on('close', function () {
    console.log('Client disconnected.');
  });
});

在项目根目录下创建一个test.txt文件,为了后续能读取一些内容返回给前端,写入以下内容👇

你好我是ChatGPT100😎

效果

ts-node-dev index.ts启动服务。

每当我们修改了 index.ts 文件然后保存,它可以自动更新服务。

在terminal点击输出的http://127.0.0.1:3000,然后在浏览器打开,就可以看到我们刚刚写的页面啦🤣

我要再看一遍 ChatGPT 打字机流式回复(SSE&WebSocket)

解释

在html页面中放置了三个按钮,分别是启动和关闭WebSocket链接,使用SSE。然后分别给这三个按钮进行点击事件的绑定。前端页面和SSE使用3000端口,因为只是使用了不同的路径而已,ws是不同的服务,所以需要一个单独的端口启动它,这里使用3001。

webSFn函数中,[WebSocket](WebSocket - Web API 接口参考 | MDN (mozilla.org))方法是浏览器原生的方法,通过new方法可以得到一个ws的实例。参数就是地址,这里的ws://127.0.0.1:3001,WebSocket(简称ws)和HTTP(超文本传输协议)是两种不同的协议,WebSocket可以提供更高效、实时的双向通信,而HTTP主要用于请求-响应的单向通信。

SSE函数中,[EventSource](EventSource - Web API 接口参考 | MDN (mozilla.org))也是浏览器原生的方法,参数也是一个url,可以简写为路径,而不用写协议、主机和端口,例如http://127.0.0.1:3000/sse简写为/see

❗️特别注意:在监听sse的事件的时候,监听的事件名称为"exampleSSE",这里需要和后端返回的event字段要保持一致。

如果后端返回的数据中,event字段为空或者为"message",那么在前端进行监听的事件名称默认为"message"。

WebSocket

后端

在链接完成并接收到前端Websocket发送过来的消息后,我们从本地新建的txt文档中读取部分内容返回给前端👇,当文件读取完成,调用close关闭WebSocket。

  socket.on('message', function (message) {
    if (message.toString() === '打字机') {
      const readStream = fs.createReadStream('./test.txt', 'utf-8');
      readStream.on('data', (data) => {
        socket.send(data);
      });
      readStream.on('close', () => {
        socket.close();
      });
    }
  });

前端

前端接收内容并展示在页面上

<script>
        ...
        ws.addEventListener('message', function (event) {
          msg.innerHTML = '';  // 清空默认内容
          msg.innerHTML += event.data;
        });
        ...
</script>

展示

刷新页面,点击链接WebSocket按钮,在网络中可以看到确实如前言所说,服务端会返回一个101状态的响应。事实上,你只需要搞清楚ws的几个方法就能够很好的去使用它了。在html中实例化ws后,通过监听open,message,close这三个方法,对应创建链接主动接收消息关闭链接,这三个层面来学习和使用。就好比怎么把大象🐘放进冰箱。

我要再看一遍 ChatGPT 打字机流式回复(SSE&WebSocket)

SSE

后端

我要再看一遍 ChatGPT 打字机流式回复(SSE&WebSocket) SSE对返回的数据有一些格式的要求

  1. 每个SSE事件以data:开始,后跟一个数据字段。
data: 这是一条消息
  1. 在每个数据字段之间需要使用空行进行分隔。
data: 这是第一条消息

data: 这是第二条消息
  1. 可以在每个事件中使用其他自定义字段,使用冒号分隔字段名和字段值。
data: 这是一条消息
id: 1
event: customEvent
  1. 可以通过发送一个空的数据字段来保持连接的活跃状态。
data:
  1. 如果需要指定事件的标识符、事件类型或重试时间间隔,可以使用ideventretry字段。
id: 1
event: customEvent
retry: 5000
data: 这是一条有标识符、事件类型和重试时间间隔的消息

每个事件都会作为一个单独的消息发送到客户端。

首先我们需要定义返回的响应头为SSE格式,这里就简单了自定义了一个event字段为"exampleSSE"。

Content-Type 设置为 text/event-stream 表示返回的内容是 SSE 格式,Cache-Control 设置为 no-cache 表示不缓存该响应,Connection 设置为 keep-alive 表示保持长连接。

使用fs模块的createReadStream创建一个流式的对象。

    response.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive'
    });
    // 创建文件读取流
    const readStream = fs.createReadStream('test.txt', {
      encoding: 'utf-8',
      highWaterMark: 1 // 每次读取一个字母
    });
    // 监听数据事件
    readStream.on('data', (chunk) => {
      // 将每个字母作为一个事件发送到客户端
      console.log('读取');
      response.write(`event:exampleSSE\ndata: ${chunk}\n\n`);
      if (!chunk) {
        response.end();
      }
    });

前端

在监听的exampleSSE事件中获取数据。

<script>
      function SSE() {
        const eventSource = new EventSource('/sse');
        eventSource.addEventListener('exampleSSE', (event) => {
          console.log(event.data);
          if (msg.innerHTML === '还未发送任何请求') {
            msg.innerHTML = '';
            msg.innerHTML += event.data;
          } else {
            msg.innerHTML += event.data;
          }
        });

        eventSource.addEventListener('open', function (event) {
          console.log('Connection opened.');
        });

        eventSource.addEventListener('error', function (event) {
          console.error('Error occurred:', event);
          // 在这里可以对连接错误进行处理
        });
      }
</script>

展示

由于文件过小传输的太快,导致页面上显示太快,没有明细的打字机效果,但是可以通过添加定时器实现,后续大家可以尝试一下哦。

不过在控制台的输出其实可以看到确实是流式的,一个字一个字的接收的。

我要再看一遍 ChatGPT 打字机流式回复(SSE&WebSocket) 在网络中打开EventStream标签也能看到类似的数据。

我要再看一遍 ChatGPT 打字机流式回复(SSE&WebSocket)

比较

SSE(Server-Sent Events,服务器推送事件)和WebSocket是两种用于实现实时通信的技术,它们相对于传统的HTTP请求-响应模型具有一些不同之处。下面是它们之间的异同点以及与HTTP的比较:

  1. SSE和WebSocket的异同点:
  • 异同点:

    • 数据传输方式:SSE和WebSocket都允许服务器主动推送数据给客户端,而不需要客户端显式地发起请求。
    • 连接协议:WebSocket使用自定义的协议,在连接建立后提供全双工通信;而SSE仍然使用HTTP协议,在连接建立后也只支持单向的从服务器到客户端的通信。
    • 数据格式:WebSocket可以传输各种类型的数据,包括文本和二进制数据;而SSE主要用于传输文本数据,它以文本事件流的形式提供数据。
    • 兼容性:WebSocket在现代浏览器中广泛支持,而SSE的支持度稍微较低。
  • WebSocket与SSE的共同之处:

    • 实时通信:两者都可用于实现实时通信,例如聊天应用、股票报价等。
    • 服务器推送:服务器可以主动推送数据给客户端,而不需要客户端发起请求。
  1. SSE和HTTP的比较:
  • SSE相比于HTTP:

    • 实时性:SSE允许服务器实时向客户端推送数据,而HTTP则需要客户端定期轮询或发起请求来获取新数据。
    • 效率:SSE采用长连接,减少了请求和响应的开销,可以更高效地传输数据。
    • 服务器推送:SSE支持服务器主动推送数据给客户端,而HTTP只能在客户端主动发起请求时才能获取数据。
  • HTTP相比于SSE:

    • 请求-响应模型:HTTP是一种请求-响应模型的协议,客户端需要每次显式地发起请求才能获取数据。
    • 连接状态:每次请求都需要建立和断开连接,相对于SSE的长连接方式,可能存在更多的连接开销。
    • 实时性:HTTP并不适用于实时通信,因为它需要客户端主动发起请求以获取更新的数据。

从上面的比较我们可以看出,SSE和WebSocket都提供了更高效、实时的数据传输方式,相比之下,HTTP是一种传统的请求-响应模型,不适用于实时通信。具体选择使用哪种技术取决于具体的需求和场景。

Link

[Github](shylock-wu/daziji (github.com))

转载自:https://juejin.cn/post/7248453787215495229
评论
请登录