SSE流式输出使用POST 请求

原生的EventSource只支持get请求,如果用post需要使用fetch接收,或者使用fetchEventSource插件。

node服务端代码


const express = require('express');
const cors = require('cors');
const app = express();
const port = 3000;

// 设置CORS策略,允许所有来源的请求
app.use(cors());



// SSE 路由
app.get('/events', (req, res) => {
let counter = 0;

  // 设置响应头,告诉浏览器这是一个 SSE 流
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  
  // 每秒推送一次数据
  const intervalId = setInterval(() => {
    counter++;
    res.write(`data: ${JSON.stringify({ counter })}\n\n`);
    
    // 模拟关闭连接
    if (counter === 10) {
      clearInterval(intervalId);
      res.write('data: {"message": "Stream ended"}\n\n');
      res.end();
    }
  }, 2000);

  // 当客户端断开连接时,清理定时器
  req.on('close', () => {
    clearInterval(intervalId);
  });
});

app.post('/events2', (req, res) => {

  // 设置响应头,告诉浏览器这是一个 SSE 流
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.write(`data: hearbeat \n\n`);
  
  let tottal = 0;
  
  // 每秒推送一次数据
  const intervalId = setInterval(() => {
    tottal++;
    res.write(`data: ${JSON.stringify({ message: tottal })}\n\n`);
    
    // 模拟关闭连接
    if (tottal === 10) {
      clearInterval(intervalId);
      res.write('data: {"message": "Stream ended"}\n\n');
      res.end();
    }
  }, 2000);

  // 当客户端断开连接时,清理定时器
  req.on('close', () => {
    clearInterval(intervalId);
  });
});

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});

前端代码

一 使用Fetch

fetch('http://99.12.39.214:3000/events2', {
      method: 'POST',
      headers: {
        Authorization: sessionStorage.getItem('token') || '',
      },
      body: JSON.stringify({ intention: 'OTHER' }),
    })
      .then(async (response) => {
        console.log(response)
        const reader = response.body?.getReader()
        const decoder = new TextDecoder()
        let buffer = ''
        while (true) {
          const { done, value } = await reader.read()
          if (done) break

          const chunk = decoder.decode(value)
          buffer += chunk // 将上次尾部数据和本次新数据合并
          // 检查buffer中是否包含完整的事件流数据(以`data: `开头,以`\n\n`结尾)
          while (true) {
            // 使用\n\ndata:分割分割而不是 \n\n 是因为流数据中可能包含\n\n,所以需要取最后一个\n\n作为事件的边界
            const eventEnd = buffer.indexOf('\n\ndata:')
            if (eventEnd === -1) break // 如果没有完整的事件流数据,则继续等待下一次数据

            const eventBlock = buffer.slice(0, eventEnd + 2) // 获取data:之前的那部分
            console.log(`eventBlock:${eventBlock}`)
            buffer = buffer.slice(eventEnd + 2) // 移除\n\n
            console.log(`buffer:${buffer}`)

            // 处理一个data:事件块 将换行符换成空格
            let eventData = eventBlock.replaceAll('\n', ' ')
            if (eventData.startsWith('data: ')) {
              eventData = eventData.slice(6)
            }
          }
        }
      })
      .catch((error) => {
        console.error('Error:', error)
      })

二 使用fetchEventSource

import { fetchEventSource } from '@microsoft/fetch-event-source';

const controller = new AbortController();
const { signal } = controller;

fetchEventSource('http://99.12.204.199:3000/events2', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
},
body: JSON.stringify({ key: 'value' }),
signal,
onmessage(event) {
console.log('Message:', event.data);
},
onclose() {
console.log('Connection closed');
},
onerror(error) {
console.error('Error:', error);
}
});

// 手动中断请求
controller.abort();

三 问题记录

1、本地项目post在浏览器network上看是一起返回的

问题: 在浏览器network面板上查看,心跳帧返回了,但后面内容没有流式输出。
原因: umi有压缩设置
解决: 去掉它压缩,重新启动。
配置:cross-env UMI_ENV=dev UMI_DEV_SERVER_COMPRESS=none max dev
rimraf ./src/.umi是删除.umi文件夹,主要解决每次启动都说文件存在

  "scripts": {
    "dev": "rimraf ./src/.umi & cross-env UMI_ENV=dev UMI_DEV_SERVER_COMPRESS=none max dev",
    "start": "npm run dev",
    "build": "max build",
 }

参考文档:SSE 开发实践

2、get请求可以流式输出,但post不行

原因: 有日志接口阻塞了post返回。

查找问题过程

  1. 使用postman或者直接在浏览器查看,都可以看到是逐帧输出的,可以排除接口问题
  2. 是否是umi框架的问题,发现有人提了,本地调试需要进行配置,但我碰到的是非本地也不行
  3. 新建一个umi项目,都是4.4.6版本,发现使用fetchEventSource代码可以流式输出。
  4. 对比两个项目,发现只要加了自定义的ErrorBoundary,post就失败!!!
    查看自定义的ErrorBoundary,里面有一个日志上报逻辑,去掉这个,发现post正常了!!

最后解决方案为: 在日志上报设置中移除掉所有跟ai相关的接口即可。

### 处理Postman中的流式数据包 在Postman中处理流式数据包主要依赖于HTTP协议的支持特性。对于流式数据传输,有两种常见的技术可以选择:Server-Sent Events (SSE)[^4] 和 WebSocket。 #### 使用 Server-Sent Events (SSE) 由于SSE基于HTTP协议,因此可以直接通过标准的GET请求来发起连接并接收事件流。然而需要注意的是,在默认情况下Postman并不特别适合用于测试长时间保持连接的服务端推送场景,因为它主要用于RESTful API接口调试而非持续监听型应用。尽管如此,仍然可以在一定程度上利用Postman来进行简单的尝试: 1. 创建一个新的GET请求; 2. 设置URL为目标服务地址; 3. 如果API文档指定了特定头部信息,则相应配置Headers选项卡下的键值对; 4. 发送请求后,观察Response区域内的实时更新内容。 为了更好地捕捉到完整的消息序列而不是仅限于最初几条记录,建议调整设置以允许更长超时时间或禁用自动关闭未完成事务的功能。 ```javascript // 示例代码展示如何解析接收到的数据帧 pm.sendRequest({ url: 'https://example.com/events', method: 'GET' }, function(err, res){ if (!err && res.code === 200) { console.log(res.stream.on('data', chunk => { let data = JSON.parse(chunk.toString()); // 对每一块数据做进一步处理... })); } }); ``` 请注意上述脚本仅为概念验证性质,并不适用于实际生产环境部署。真实项目开发时应考虑采用更适合长期稳定工作的客户端库实现方式。 #### 使用 WebSockets 虽然WebSockets提供了更为强大的功能集以及更好的性能表现,但遗憾的是当前版本的Postman尚不具备内置支持WebSocket的能力。不过官方团队已经意识到这一点并且正在积极规划未来加入这项重要特性的计划当中。现阶段如果确实有这方面需求的话,可能需要寻找其他专门针对WebSocket协议设计的应用程序或者在线平台作为替代方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值