大模型问答助手前端实现打字机效果 | 京东云技术团队

1. 背景

随着现代技术的快速发展,即时交互变得越来越重要。用户不仅希望获取信息,而且希望以更直观和实时的方式体验它。这在聊天应用程序和其他实时通信工具中尤为明显,用户习惯看到对方正在输入的提示。

ChatGPT,作为 OpenAI 的代表性产品之一,不仅为用户提供了强大的自然语言处理能力,而且关注用户的整体交互体验。在使用 ChatGPT 进行交互时,用户可能已经注意到了一个细节:当它产生回复时,回复会像人类逐字输入的方式逐渐出现,而不是一次性显示完整答案。

这种打字效果给人一种仿佛与真人对话的感觉,进一步增强了其自然语言处理的真实感。一开始,许多开发者可能会误以为这是通过 WebSockets 实现的,这是因为 WebSockets 是一种常用于实时通信的技术。然而,仔细研究后,我们发现 ChatGPT 使用了一种不同的技术:基于 EventStream 的方法。更具体地说,它似乎是通过 SSE (Server-Sent Events) 来实现逐个字地推送答案的。

此外,考虑到 ChatGPT 的复杂性和其涉及的大量计算,响应时间可能会长于其他基于数据库的简单查询。因此,采用 SSE 逐步推送结果的方式可以帮助减少用户感到的等待时间,从而增强用户体验。

ChatGPT Typing Effect

2. SSE 简介

Server-Sent Events(通常简称为SSE)是一种允许服务器向Web页面发送实时更新的技术。与WebSocket技术相比,SSE专门设计用于从服务器到客户端的单向通信。这种单向性使其在某些场景中更为简单和直观。

2.1 主要特点

  1. 单向通信:SSE 专为从服务器到客户端的单向通信设计。客户端不能通过SSE直接发送数据到服务器,但可以通过其他方法如AJAX与服务器进行交互。

  2. 基于HTTP:SSE 基于 HTTP 协议运行,不需要新的协议或端口。这使得它能够轻松地在现有的Web应用架构中使用,并且通过标准的HTTP代理和中间件进行支持。

  3. 自动重连:如果连接断开,浏览器会自动尝试重新连接到服务器。

  4. 格式简单:SSE 使用简单的文本格式发送消息,每个消息都以两个连续的换行符分隔。

  5. 原生浏览器支持:许多现代浏览器(如 Chrome、Firefox 和 Safari)已原生支持SSE,但需要注意的是,某些浏览器,如Internet Explorer和早期的Edge版本,不支持SSE。

2.2 SSE 与 WebSockets

虽然 SSE 与 WebSockets 在某种程度上有些相似,但它们之间还存在一些关键差异,如下所示:

对比项Server-Sent Events (SSE)WebSockets
基于协议基于 HTTP,简化了连接和交互的过程通常基于 WS/WSS(基于TCP),更为灵活
通信能力单向通信:仅服务器向客户端发送消息双向通信能力
配置配置简单,易于理解和使用需要更复杂的配置和理解
断线与消息追踪自带的断线重连和消息跟踪功能通常需要手动处理或使用额外库
数据格式通常为文本,但可以发送经过编码/压缩的二进制消息支持文本和原始二进制消息
事件处理支持多种自定义事件基本消息机制,不能像SSE那样自定义事件类型
连接并发性连接数可能受到 HTTP 版本的限制,尤其是在HTTP/1.1中WebSocket被设计为支持更高的连接并发性
安全性仅支持HTTP和HTTPS的安全机制支持WS和WSS,可以在WSS上实现更强大的加密
浏览器兼容性大部分现代浏览器支持,但不是所有浏览器几乎所有现代浏览器都支持
开销由于基于HTTP,每次消息可能有较大的头部开销握手后,消息头部开销相对较小

3. 服务端深入解析

3.1 SSE 的协议机制

Server-Sent Events(SSE)是一个基于 HTTP 的协议,允许服务器单向地向浏览器推送信息。为了成功地使用 SSE,服务器和客户端都必须遵循一定的规范和流程。

当客户端(例如浏览器)发出请求订阅 SSE 服务时,服务器需要通过设置特定的响应头部信息来确认该请求。这些头部信息包括:

  • Content-Type: text/event-stream: 这表示返回的内容为事件流。

  • Cache-Control: no-cache: 这确保服务器推送的消息不会被缓存,以保障消息的实时性。

  • Connection: keep-alive: 这指示连接应始终保持开放,以便服务器可以随时发送消息。

3.2 消息的格式和结构

SSE 使用简单的文本格式来组织和发送消息。基本的消息结构是由一系列行组成,每一行由字段名、一个冒号和字段值组成。

以下是消息中可以使用的一些字段及其用途:

  • event: 定义了事件的类型。这可以帮助客户端确定如何处理接收到的消息。

  • id: 提供事件的唯一标识符。如果连接中断,客户端可以使用最后收到的事件 ID 来请求服务器从某个点重新发送消息。

  • retry: 指定了当连接断开时,客户端应等待多少毫秒再尝试重新连接。这为连接中断和重连提供了一种机制。

  • data: 这是消息的主体内容。它可以是任何 UTF-8 编码的文本,而且可以跨多行。每行数据都会在客户端解析时连接起来,中间使用换行符分隔。

为了确保消息的正确和完整传输,服务器通常在消息的末尾添加一个空行,表示消息的结束。

示例:

id: 123
event: update
data: {"message": "This is a test message"}

此外,SSE 也支持多条连续消息的发送。只要每条消息之间使用两个换行符隔开即可。

4. 客户端实践

接入 SSE 并不困难,尤其在客户端这边。主流浏览器提供了EventSourceAPI,使得与 SSE 服务端建立和维护连接变得异常简单。

4.1 如何建立连接

首先,需要创建一个EventSource对象,它将代表与服务器的持久连接。初始化时,可以为它提供一些选项,以满足特定需求。

const options = {withCredentials: true  // 允许跨域请求携带凭证
};// 创建一个 EventSource 对象以开始监听
const eventSource = new EventSource('your_server_url', options);

在上面的代码中,withCredentials参数用于指示是否应该在请求中发送凭证(例如 cookies)。这在跨域场景中可能会非常有用。

4.2 如何处理收到的事件

一旦与服务器建立了连接,就可以开始监听从服务器发送过来的事件。

  • 通用事件处理:
    默认情况下,EventSource对象会对三种基本的事件类型进行响应:openmessageerror。可以设置对应的处理函数来对它们进行响应。

    // 监听连接打开事件
    eventSource.onopen = function(event) {console.log('Connection to SSE server established!');
    };// 监听标准消息事件
    eventSource.onmessage = function(event) {console.log('Received data from server: ', event.data);
    };// 监听错误事件
    eventSource.onerror = function(event) {console.error('An error occurred while receiving data:', event);
    };
  • 自定义事件处理:
    除了上述的基本事件外,服务器还可能发送自定义的事件类型。为了处理这些事件,需要使用addEventListener()方法。

    // 监听一个名为 "update" 的自定义事件
    eventSource.addEventListener('update', function(event) {console.log('Received update event:', event.data);
    });

4.3 关闭连接

如果不再需要从服务器接收事件,可以使用close方法关闭连接。

eventSource.close();

关闭连接后,将不再接收任何事件,除非再次初始化EventSource对象。


总结:使用EventSourceAPI,客户端可以方便地与 SSE 服务器交互,从而实时接收数据更新。这为创建响应迅速的 web 应用提供了极大的便利,同时避免了传统的轮询方式带来的资源浪费。

5. 理论实践

5.1 服务端

const http = require('http');
const fs = require('fs');// 初始化 HTTP 服务器
http.createServer((req, res) => {// 为了简洁,将响应方法抽离成函数function serveFile(filePath, contentType) {fs.readFile(filePath, (err, data) => {if (err) {res.writeHead(500);res.end('Error loading the file');} else {res.writeHead(200, {'Content-Type': contentType});res.end(data);}});}function handleSSEConnection() {res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache','Connection': 'keep-alive'});let id = 0;const intervalId = setInterval(() => {const message = {event: 'customEvent',id: id++,retry: 30000,data: { id, time: new Date().toISOString() }};for (let key in message) {if (key !== 'data') {res.write(`${key}: ${message[key]}\n`);} else {res.write(`data: ${JSON.stringify(message.data)}\n\n`);}}}, 1000);req.on('close', () => {clearInterval(intervalId);res.end();});}switch (req.url) {case '/':serveFile('index.html', 'text/html');break;case '/events':handleSSEConnection();break;default:res.writeHead(404);res.end();break;}}).listen(3000);console.log('Server listening on port 3000');

5.2 客户端

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>SSE Demo</title>
</head>
<body><h1>SSE Demo</h1><button onclick="connectSSE()">建立 SSE 连接</button><button onclick="closeSSE()">断开 SSE 连接</button> <br /><br /><div id="message"></div><script>const messageElement = document.getElementById('message');let eventSource;// 连接 SSEfunction connectSSE() {eventSource = new EventSource('/events');eventSource.addEventListener('customEvent', handleReceivedMessage);eventSource.onopen = handleConnectionOpen;eventSource.onerror = handleConnectionError;}// 断开 SSE 连接function closeSSE() {eventSource.close();appendMessage(`SSE 连接关闭,状态${eventSource.readyState}`);}// 处理从服务端收到的消息function handleReceivedMessage(event) {const data = JSON.parse(event.data);appendMessage(`${data.id} --- ${data.time}`);}// 连接建立成功的处理函数function handleConnectionOpen() {appendMessage(`SSE 连接成功,状态${eventSource.readyState}`);}// 连接发生错误的处理函数function handleConnectionError() {appendMessage(`SSE 连接错误,状态${eventSource.readyState}`);}// 将消息添加到页面上function appendMessage(message) {messageElement.innerHTML += `${message}<br />`;}</script>
</body>
</html>

将上面的两份代码保存为server.jsindex.html,并在命令行中执行node server.js启动服务端,然后在浏览器中打开http://localhost:3000即可看到 SSE 效果。

6. 业务实践

6.1 存在问题

在业务真实使用场景中,基于SSE的方法存在一些问题和限制:

  1. 默认请求仅支持GET方法。当前端需要向后端传递参数时,参数只能拼接在请求的 URL 上,对于复杂的业务场景来说实现较为麻烦。

  2. 对于服务端返回的数据格式有固定要求,必须按照eventidretrydata的结构返回。

  3. 服务端发送的数据可以在浏览器控制台中查看,这可能会暴露敏感数据,导致数据安全问题。

为了解决以上问题,并使其支持POST请求以及自定义的返回数据格式,我们可以使用以下技巧

6.2 优化技巧

利用 Fetch API 的流处理能力,我们可以实现对 SSE 的扩展:

/*** Utf8ArrayToStr: 将Uint8Array的数据转为字符串* @param {Uint8Array} array - Uint8Array数据* @return {string} - 转换后的字符串*/
function Utf8ArrayToStr(array) {const decoder = new TextDecoder();return decoder.decode(array);
}/*** fetchStream: 建立一个SSE连接,并支持多种HTTP请求方式* @param {string} url - 请求的URL地址* @param {object} params - 请求的参数,包括HTTP方法、头部、主体内容等* @return {Promise} - 返回一个Promise对象*/
const fetchStream = (url, params) => {const { onmessage, onclose, ...otherParams } = params;return fetch(url, otherParams).then(response => {let reader = response.body?.getReader();return new ReadableStream({start(controller) {function push() {reader?.read().then(({ done, value }) => {if (done) {controller.close();onclose?.();return;}const decodedData = Utf8ArrayToStr(value);console.log(decodedData);onmessage?.(decodedData);controller.enqueue(value);push();});}push();}});}).then(stream => {return new Response(stream, {headers: { "Content-Type": "text/html" }}).text();});
};// 示例:调用fetchStream函数
fetchStream("/events", {method: "POST", // 使用POST方法headers: {"content-type": "application/json"},credentials: "include",body: JSON.stringify({// 这里列出了一些示例数据,实际业务场景请替换为你的数据boxId: "exampleBoxId",sessionId: "exampleSessionId",queryContent: "exampleQueryContent"}),onmessage: res => {console.log(res); // 当接收到消息时的回调},onclose: () => {console.log("Connection closed."); // 当连接关闭时的回调}
});

6.3 封装插件

我们定义一个名为eventStreamHandler.ts的文件

// 定义请求主体的接口,需要根据具体的应用场景定义具体的属性
interface RequestBody {// 示例属性,具体属性需要根据实际需求定义key?: string;
}// 错误响应的结构
interface ErrorResponse {error: string;detail: string;
}// 返回值类型定义
type TextStream = ReadableStreamDefaultReader<Uint8Array>;// 获取数据并返回TextStream
async function fetchData(url: string,body: RequestBody,accessToken: string,onError: (message: string) => void
): Promise<TextStream | undefined> {try {// 尝试发起请求const response = await fetch(url, {method: "POST",cache: "no-cache",keepalive: true,headers: {"Content-Type": "application/json",Accept: "text/event-stream",Authorization: `Bearer ${accessToken}`,},body: JSON.stringify(body),});// 检查是否有冲突,例如重复请求if (response.status === 409) {const error: ErrorResponse = await response.json();onError(error.detail);return undefined;}return response.body?.getReader();} catch (error) {onError(`Failed to fetch: ${error.message}`);return undefined;}
}// 读取流数据
async function readStream(reader: TextStream): Promise<string | null> {const result = await reader.read();return result.done ? null : new TextDecoder().decode(result.value);
}// 处理文本流数据
async function processStream(reader: TextStream,onStart: () => void,onText: (text: string) => void,onError: (error: string) => void,shouldClose: () => boolean
): Promise<void> {try {// 开始处理数据onStart();while (true) {if (shouldClose()) {await reader.cancel();return;}const text = await readStream(reader);if (text === null) break;onText(text);}} catch (error) {onError(`Processing stream failed: ${error.message}`);}
}/*** 主要的导出函数,用于处理流式文本数据。* * @param url 请求的URL。* @param body 请求主体内容。* @param accessToken 访问令牌。* @param onStart 开始处理数据时的回调。* @param onText 接收到数据时的回调。* @param onError 错误处理回调。* @param shouldClose 判断是否需要关闭流的函数。*/
export async function streamText(url: string,body: RequestBody,accessToken: string,onStart: () => void,onText: (text: string) => void,onError: (error: string) => void,shouldClose: () => boolean
): Promise<void> {const reader = await fetchData(url, body, accessToken, onError);if (!reader) {console.error("Reader is undefined!");return;}await processStream(reader, onStart, onText, onError, shouldClose);
}

7. 兼容性

发展至今,SSE 已具有广泛的的浏览器兼容性,几乎除 IE 之外的浏览器均已支持。

8. 总结

SSE (Server-Sent Events) 是基于 HTTP 协议的轻量级实时通信技术。其核心特点是由服务器主动推送数据到客户端,而不需要客户端频繁请求。这样的特点使得 SSE 在某些应用场景中成为了理想选择,例如股票行情实时更新、网站活动日志推送、或聊天室中的实时在线人数统计。

然而,尽管 SSE 有很多优势,如断线重连机制、相对简单的实现和轻量性等,但它也存在明显的局限性。首先,SSE 只支持单向通信,即服务器到客户端的数据推送,而无法实现真正的双向交互。其次,由于浏览器对并发连接数有限制,当需要大量的实时通信连接时,SSE 可能会受到限制。

相对而言,WebSockets 提供了一个更加强大的双向通信机制,能够满足高并发、高吞吐量和低延迟的需求。因此,在选择适合的实时通信方案时,开发者需要根据应用的具体需求和场景来做出选择。简而言之,对于需要简单、低频率更新的场景,SSE 是一个非常不错的选择;而对于需要复杂、高频、双向交互的应用,WebSockets 可能更为合适。

最后,无论选择哪种技术,都应对其优缺点有深入了解,以确保在特定场景下可以提供最佳的用户体验。

作者:京东科技 卞荣成

来源:京东云开发者社区 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/179206.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue路由导航(replace、push、forward、back、go)

Vue路由导航&#xff08;replace、push、forward、back、go&#xff09; 先了解栈结构&#xff0c;再学习以下内容 栈的数据结构&#xff1a;先进后出&#xff0c;后进先出。原理&#xff1a;push将元素压入栈内&#xff0c;pop将元素弹出&#xff0c;栈有分别有栈底指针和栈顶…

Oracle 19c 可插拔数据库PDB的创建方式

多租户容器数据库架构图总览 多租户容器数据库组成部分&#xff1a; 1.有且仅有一个CDB Root(CDB$ROOT)&#xff0c;它包含了Root和所有PDB数据库的元数据和数据字典信息。 2.有且仅有一个Seed PDB(PDB$SEED),它的作用是创建其他PDB的模板&#xff0c;它是只读库&#xff0c;…

饥荒联机版 Don‘t Starve Together(WinMac)最新中文学习版

《饥荒联机版》是由Klei自主开发的开放世界冒险游戏。在这个游戏中&#xff0c;玩家将扮演各种各样的人物&#xff0c;这些人物不幸来到了一个神秘的异世界。在旅行中&#xff0c;玩家会邂逅性格各异、能力独特的同伴们&#xff0c;并和他们一起生存下去并征服异世界。游戏中的…

从零开始的目标检测和关键点检测(一):用labelme标注数据集

从零开始的目标检测和关键点检测&#xff08;一&#xff09;&#xff1a;用labelme标注数据集 1、可视化标注结果2、划分数据集3、Lableme2COCO&#xff0c;将json文件转换为MS COCO格式 前言&#xff1a;前段时间用到了mmlab的mmdetction和mmpose&#xff0c;因此以一个小的数…

ruoyi系统改造

前端启动报错&#xff1a;Error: error:0308010C:digital envelope routines::unsupported 修改ruoyi-ui/package.json&#xff0c;添加export NODE_OPTIONS–openssl-legacy-provider && "scripts": {"dev": "export NODE_OPTIONS--openssl…

【Python_GraphicsView 学习笔记(一)】Graphics View框架的基本介绍

【Python_GraphicsView 学习笔记&#xff08;一&#xff09;】Graphics View框架的基本介绍 前言正文1、Graphics View框架简介2、Graphics View框架与QPainter类的区别3、Graphics View框架的三个组成部分4、场景QGraphicsScene类5、视图QGraphicsView类6、图形项QGraphicsIte…

利用GEE对季节性地物进行分类的代码实现

采样点的选取 如果你采用监督学习的话&#xff0c;那就手动打标签 或者可以了解一下非监督学习 合成多季节多波段影像 首先&#xff0c;制作一个包含多波段的影像&#xff0c;每个波段作为随机森林分类器的一个feature输入&#xff0c;提升feature的丰富度以保证分类精度。…

MySQL用户管理和授权

目录 一.用户管理 1.1.新建用户 1.2.查看用户 1.3.重命名用户rename 1.4.删除用户 1.5.修改当前登录用户密码 1.6.修改其他用户密码 1.7.忘记root 密码并找回 二.数据库用户授权 2.1.all privilege包含的权限 2.2.授予权限 ①允许指定用户查询指定数据库表 ②允许…

FlexmonsterPivotTable-2.9.63 LICENSE

FlexmonsterPivotTable-v2.9.63用于网络报告的数据透视表组件&#xff0c;用于可视化业务数据的最强大的 JavaScript 工具 与任何技术堆栈集成 该组件可与任何技术堆栈无缝协作&#xff1a; 与Angular、React、jQuery、Vue等 完美集成 没有服务器端依赖项 只需几行代码 即可开始…

Python接口自动化测试实战,一篇足矣

接口自动化测试是指通过编写程序来模拟用户的行为&#xff0c;对接口进行自动化测试。Python是一种流行的编程语言&#xff0c;它在接口自动化测试中得到了广泛应用。下面详细介绍Python接口自动化测试实战。 1、接口自动化测试框架 在Python接口自动化测试中&#xff0c;我们…

社区论坛在线交流网站系统源码+SEO优化 带前后端完整搭建教程

大家好&#xff0c;今天罗峰来给大家分享一款社区论坛在线交流网站系统源码。社区论坛在线交流在当下时时代还是很火的。现在人们对于在线交流和互动的需求不断增加。社区论坛作为一种传统的在线交流方式&#xff0c;仍然有着广泛的市场需求和用户群体。然而&#xff0c;现有的…

MySQL - Zero date value prohibited

问题: timestamp字段报Caused by: com.mysql.cj.exceptions.DataReadException: Zero date value prohibited 原因: timestamp字段存入了0值, 超出了最小值1900-01-01 00:00:00, 转Java对象的时候报错 解决: 1.修复或删除原数据 2. mysqlurl 中添加zeroDateTimeBehaviorconve…

【Linux】 shutdown 命令使用

shutdown 命令可以用来进行关机程序&#xff0c;并且在关机以前传送讯息给所有使用者正在执行的程序&#xff0c;shutdown 也可以用来重开机。使用权限&#xff1a;系统管理者。 语法 shutdown [选项] 时间 [警告信息] 命令选项及作用 执行令 man shutdown 执行命令结果 参…

替代知网!中国科学院发布公益学术平台,可免费获取8000万篇论文专著!

可检索1.7亿篇科技文献的 公益平台来了&#xff01; 11月1日由中国科学院等单位联合建设的 PubScholar公益学术平台 正式对社会公众开放 该平台首期整合集成了 中国科学院的科技成果资源 科技出版资源和学术交流资源 内容包含期刊论文、学位论文 预发布论文、专利文献、…

时间复杂度的计算技巧-算法模型中的时间复杂度如何计算,有哪些技巧呢

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下时间复杂度的计算技巧-算法模型中的时间复杂度如何计算&#xff0c;有哪些技巧呢&#xff0c;算法的时间复杂度是评估算法性能和效率的一种方式&#xff0c;它表示算法需要执行多少次基本操作才能完成其任务&#x…

LuaHttp库写的一个简单的爬虫

LuaHttp库是一个基于Lua语言的HTTP客户端库&#xff0c;可以用于爬取网站数据。与Python的Scrapy框架类似&#xff0c;LuaHttp库也可以实现网站数据的抓取&#xff0c;并且可以将抓取到的数据保存到数据库中。不过需要注意的是&#xff0c;LuaHttp库并不像Scrapy框架那样具有完…

【C++ 系列文章 -- 程序员考试 201811 下午场 C++ 专题 】

1.1 C 题目六 阅读下列说明和C代码&#xff0c;填写程序中的空&#xff08;1&#xff09; &#xff5e;&#xff08;5&#xff09;&#xff0c;将解答写入答题纸的对应栏内。 【说明】 以下C代码实现一个简单乐器系统&#xff0c;音乐类&#xff08;Music&#xff09;可以使用…

VMware——VMware17设置WindowServer2012R2环境静态IP及关闭防火墙

目录 一、VMware17设置WindowServer2012R2环境静态IP1.1、工具栏虚拟机的设置步骤1.2、工具栏编辑的设置步骤1.3、静态IP的设置步骤 二、VMware17关闭WindowServer2012R2环境防火墙 一、VMware17设置WindowServer2012R2环境静态IP 1.1、工具栏虚拟机的设置步骤 打开VMware虚拟…

【工具】【IDE】Qt Creator社区版

Qt Creator社区版下载地址&#xff1a;https://download.qt.io/archive/qt/ 参考&#xff1a;https://cloud.tencent.com/developer/article/2084698?areaSource102001.8&traceIduMchNghqp8gWPdFHvSOGg MAC安装并配置Qt&#xff08;超级简单版&#xff09; 1.安装brew&…

el-table 列分页

<template><div><el-table:data"tableData":key"tampTime"style"width: 100%"><el-table-columnprop"name"label"姓名"width"180"></el-table-column><el-table-columnprop&quo…