模拟chartGPT的聊天方式(SSE)

背景

我们在使用chartGPT的时候,在输入框输入文字发送时,页面是逐步回复的,打开控制台,发现请求接口返回数据与平时HTTP请求返回数据是不同的,请求接口是一段一段返回的,与平时调用http接口不一样,倒类似于WebSockets技术。

于是在网上调查了该技术,才知道这个技术是Server Send Event(SSE)。

概述

Server Send Event 服务器推送事件,简称SSE,浏览器发一个接口请求到服务器,服务器则是通过事件来推送数据。

基于SSE与WebSockets,从服务器接收到数据有相似之处,我就做如下比较

Server Send Event WebSockets
基于传统的 HTTP 协议 基于独立的 TCP 连接,使用自定义协议
服务器向客户端单向通信 客户端和服务器之间的双向实时通信
简单易用 相对复杂
某些旧版本浏览器中可能存在兼容性问题 广泛支持
使用场景: 实时新闻,股票行情 使用场景:聊天室、实时协作应用

为什么选择SSE

  1. SSE 更加轻量化
  2. 不需要双向通信,占用服务器的资源
  3. 聊天发送消息,只是短暂触发,不需要实时监控后端发出的数据

服务端实现

协议

对于SSE请求时,需要在相应头设置以下参数

javascript 复制代码
res.writeHead(200, {
  'Content-Type': 'text/event-stream', // 指定内容类型为 text/event-stream
  'Cache-Control': 'no-cache', // 禁用缓存
  'Connection': 'keep-alive' // 保持长连接
});
  • SSE 规定的消息体的MIME 类型 规定为 'text/event-stream'
  • 设置 'no-cache' 或者 'no-store' 用以确保服务端无法保存 客户端的数据
  • SSE 是要保持持久链接的,不是链接后关闭,需要设置成 keep-alive

示例

javascript 复制代码
// 导入 http 模块
const http = require('http');

// 定义主机和端口号
const hostname = '127.0.0.1';
const port = 3000;
// 存储客户端连接信息
const clients = new Set();

// 创建 HTTP 服务器
const server = http.createServer((req, res) => {
  // 设置 HTTP 响应头
  res.statusCode = 200;
  if (req.url === '/sse') {
    // 设置响应头,指明使用 SSE
    res.writeHead(200, {
      'Content-Type': 'text/event-stream', // 指定内容类型为 text/event-stream
      'Cache-Control': 'no-cache', // 禁用缓存
      'Connection': 'keep-alive' // 保持长连接
    });
    // 每5秒发送一条消息
    let intervalId = setInterval(() => {
      // 发送数据,消息类型为"message"
      const data = `data: ${JSON.stringify({ message: 'Hello, client!' })}\n\n`;
      res.write(data);
      res.write(data);
      res.write(data);
      setTimeout(() => res.flushHeaders(), 0)
      
        // res.socket.write();
    }, 1000);
    res.on('message', (event) => {
      console.log(event)
    })
    setTimeout(() => {
      res.end()
    }, 6000)
    // 当客户端关闭连接时,停止发送数据
    req.on('close', () => {
      clearInterval(intervalId);
    });

    console.log(req.on)
  }
});

// 监听指定的主机和端口号
server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

客户端

EventSource

介绍

浏览器提供的HTTP协议的持久化连接的接口,以 text/event-stream 格式发送事件,链接会一直保持开启,断线会自动重连,直到调用EventSource.close()关闭。浏览器则是通过EventSource对象的onmessage、onopen和onerror事件来处理这些消息

浏览器兼容

建立连接

EventSource()构造函数只接受两个参数 url 和 configuration

参数

url 远程连接的路由,

configuration ,是一个可选的对象,里面只有一个参数withCredentials的,如果为true,表示客户端向服务端传送凭据(Cookie,会话标识等)

连接状态

EventSource() 会返回一个对象,里面有readyState 只读属性,这个则表示连接状态

连接状态 含义
0 未建立连接
1 连接中
2 连接关闭

连接关闭

利用EventSource.close()关闭连接,如果调用该方法,则将readyState改变为2,如果不调用的话,则会与服务端一直连接

监听事件

EventSource继承于EventTarget,可以接收事件,并且可以创建侦听器。主要有三个事件error、message和open

  • open 事件:当连接服务端后触发
  • message 事件: 当客户端接收到服务端数后触发,该事件的data包括服务端的数据
  • error 事件: 当发生错误后触发或者关闭服务,该事件对象event包含了错误信息

示例

javascript 复制代码
const eventSource = new EventSource('/api/sse');
eventSource.onopen = (event) => {
  console.log("Connection to server opened.", event);
}

pc.addEventListener('message', (event) => {
  console.log('Received message:' + event.data)
})
eventSource.addEventListener('error', event => {
  if (event.readyState === EventSource.CLOSED) {  
    console.log('Connection with the server was closed.');  
  } else {  
    console.error('EventSource failed:', event);  
  } 
})

setTimeout(() => {
  pc.close()
}, 8000)

Axios

刚开始时,第一个想到的就是axios,相信大家也是一样的。然后在网上找到了调SSE的代码,如下

ini 复制代码
axios({
  method: 'get',
  url: '/event-stream',
  responseType: 'stream'
}).then(response => {
  const eventSource = new EventSource('/event-stream');

  eventSource.onmessage = function(event) {
    const data = JSON.parse(event.data);
    // 处理数据
  };

  eventSource.onerror = function(event) {
    // 处理错误
  };
});

相信大家已经看出问题来了,在axios调取后,又用了EventSource又调取一次接口。

后来尝试将EventSource去掉,返回返回的接口不像EventSource接口,是一段一段返回的,而是将接口一次性返回,跟需求上的要求完全不一致

根据上面两个问题,就放弃了axios

Fetch

为什么使用Fetch

EventSource 只能发送GET请求,无法设置请求头,无法向服务端发送DATA参数,无法发POST请求,导致了EventSource无法适配大多数情况,例如URL只能限制2000个字符,像GhatGPT 肯定是无法满足其请求

所以可以利用Fetch模拟SSE实现。

浏览器兼容

示例

javascript 复制代码
fetch('/api/post/sse', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  // body: JSON.stringify({ })
})
.then(async response => {
const reader = response.body.getReader();
let buffer = '';

  reader.read().then(function process({ done, value }) {
    if (done) {
      console.log('Stream closed');
      return;
    }

    buffer += new TextDecoder('utf-8').decode(value);
    const lines = buffer.split('\n');
    buffer = lines.pop();

    lines.forEach(line => {
      console.log(line);
    });

    // Continue reading
    return reader.read().then(process);
  });
})
.catch(error => {
  console.error('Error:', error);
});

效果

总结

  1. SSE 相比于 WebSockets 更加轻量化
  2. SSE是单向通信,服务端向客户端发送流的信息
  3. 如果对于客户端与服务端双方通信要求比较高,还是建议使用WebSockets
  4. EventSource虽然浏览器API,使用起来比较方便,但其的通用性不是很强
  5. SSE前端调用,建议使用Fetch浏览器提供的API

参考文档

一文读懂即时更新方案:SSE

SSE 和 WebSocket 的区别,差异对比

SSE 和 Websocket 的比较

相关推荐
anyup_前端梦工厂7 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand7 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL7 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
小马哥编程9 小时前
Function.prototype和Object.prototype 的区别
javascript
王小王和他的小伙伴10 小时前
解决 vue3 中 echarts图表在el-dialog中显示问题
javascript·vue.js·echarts
学前端的小朱10 小时前
处理字体图标、js、html及其他资源
开发语言·javascript·webpack·html·打包工具
outstanding木槿10 小时前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
好名字082111 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
摇光9311 小时前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务
胡西风_foxww11 小时前
【ES6复习笔记】Class类(15)
javascript·笔记·es6·继承··class·静态成员