我要再看一遍 ChatGPT 打字机流式回复(SSE&WebSocket)
前言
相信我们现在对 ChatGPT 已经非常熟悉了,知道它很强大,能够与我们人类进行自然语言的交流。在 ChatGPT 的网页端,我们最直观能感受到的除了它惊人的"聪明",另一个在视觉上的感受就是,它的回答是一个字一个字的蹦
出来的,这样的效果叫做打字机流式回复
,这种效果能够增强用户体验。本文将介绍打字机流式效果,并说明如何使用WebSocket
和SSE(Server-Send Events)
这两种方式实现前端接收内容的流式效果。
我让 ChatGPT 使用 js 写一个冒泡排序的功能,所呈现出来的打字机效果
,看下图👇
所需技术
为了实现打字机流式效果,我们需要一种与服务器保持持久连接的方法。这就是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.html
和index.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
,然后在浏览器打开,就可以看到我们刚刚写的页面啦🤣
解释
在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
这三个方法,对应创建链接
,主动接收消息
,关闭链接
,这三个层面来学习和使用。就好比怎么把大象🐘放进冰箱。
SSE
后端
SSE对返回的数据有一些格式的要求
- 每个SSE事件以
data:
开始,后跟一个数据字段。
data: 这是一条消息
- 在每个数据字段之间需要使用空行进行分隔。
data: 这是第一条消息
data: 这是第二条消息
- 可以在每个事件中使用其他自定义字段,使用冒号分隔字段名和字段值。
data: 这是一条消息
id: 1
event: customEvent
- 可以通过发送一个空的数据字段来保持连接的活跃状态。
data:
- 如果需要指定事件的标识符、事件类型或重试时间间隔,可以使用
id
、event
和retry
字段。
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>
展示
由于文件过小传输的太快,导致页面上显示太快,没有明细的打字机效果,但是可以通过添加定时器实现,后续大家可以尝试一下哦。
不过在控制台的输出其实可以看到确实是流式
的,一个字一个字的接收的。
在网络中打开
EventStream
标签也能看到类似的数据。
比较
SSE(Server-Sent Events,服务器推送事件)和WebSocket是两种用于实现实时通信的技术,它们相对于传统的HTTP请求-响应模型具有一些不同之处。下面是它们之间的异同点以及与HTTP的比较:
- SSE和WebSocket的异同点:
-
异同点:
- 数据传输方式:SSE和WebSocket都允许服务器主动推送数据给客户端,而不需要客户端显式地发起请求。
- 连接协议:WebSocket使用自定义的协议,在连接建立后提供全双工通信;而SSE仍然使用HTTP协议,在连接建立后也只支持单向的从服务器到客户端的通信。
- 数据格式:WebSocket可以传输各种类型的数据,包括文本和二进制数据;而SSE主要用于传输文本数据,它以文本事件流的形式提供数据。
- 兼容性:WebSocket在现代浏览器中广泛支持,而SSE的支持度稍微较低。
-
WebSocket与SSE的共同之处:
- 实时通信:两者都可用于实现实时通信,例如聊天应用、股票报价等。
- 服务器推送:服务器可以主动推送数据给客户端,而不需要客户端发起请求。
- 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