HTTP中的event-stream,eventsource,SSE,chatgpt,stream request,golang

news2024/11/15 23:32:51

我们都知道chatgpt是生成式的,因此它返回给客户端的消息也是一段一段的,所以普通的HTTP协议无法满足,当然websocket是能满足的,但是这个是双向的通信,其实 SSE(Server-Sent Events) 正好满足这个需求。

SSE相比websocket的优点:

  • SSE是使用http协议,而websocket是一种单独的协议。
  • SSE是单向传输,只能服务端向客户端推送,websocket是双向。
  • SSE支持断点续传,websocket需要自己实现。
  • SSE支持自动重连、轻量级。
  • SSE支持发送自定义类型消息。
  • SSE的响应头Content-Typ:text/event-stream

要实现SSE,服务端需要设置以下Headers

"Content-Type""text/event-stream"
"Cache-Control""no-cache"
"Connection""keep-alive"
"Access-Control-Allow-Origin": "*" // 跨域问题
一、前端代码

我看网络上有两种实现方式:fetch 和 EventSource

fetch方式

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Event Stream Demo</title>
    <style type="text/css">
        body {
            font-family: Arial, sans-serif;
            text-align: center;
        }

        #event-stream-data {
            margin: 50px auto;
            max-width: 600px;
            border: 1px solid #ccc;
            padding: 10px;
        }
    </style>
</head>

<body>
    <div id="event-stream-data"></div>
</body>

<script>
    const eventStreamDataElement = document.getElementById('event-stream-data');
    function handleEventStreamMessage(event) {
        console.log(event)
        const eventText = event.data;
        displayEvent(eventText);
    }

    function displayEvent(eventText) {
        const eventElement = document.createElement('p');
        eventElement.textContent = eventText;
        eventStreamDataElement.appendChild(eventElement);
    }

    function connectToEventStream() {
        fetch('http://127.0.0.1:8080/stream', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: {
                data: 'example'
            }
        })
            .then(response => {
                const reader = response.body.getReader();
                const decoder = new TextDecoder();

                return reader.read().then(function processResult(result) {
                    // console.log(result)
                    if (result.done) {
                        return;
                    }

                    const chunk = decoder.decode(result.value, {
                        stream: true
                    });
                    handleEventStreamMessage({
                        data: chunk
                    });

                    return reader.read().then(processResult);
                });
            })
            .catch(error => {
                console.error('Error occurred while fetching event stream:', error);
            });
    }
    connectToEventStream();
</script>

</html>

EventSource方式

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Event Stream Demo</title>
    <style type="text/css">
        body {
            font-family: Arial, sans-serif;
            text-align: center;
        }

        #event-stream-data {
            margin: 50px auto;
            max-width: 600px;
            border: 1px solid #ccc;
            padding: 10px;
        }
    </style>
</head>

<body>
    <div id="event-stream-data"></div>
</body>

<script type="text/javascript">
    const eventStreamDataElement = document.getElementById('event-stream-data');
    function handleEventStreamMessage(event) {
        console.log(event)
        const eventText = event.data;
        displayEvent(eventText);
    }

    function displayEvent(eventText) {
        const eventElement = document.createElement('p');
        eventElement.textContent = eventText;
        eventStreamDataElement.appendChild(eventElement);
    }

    // 向后端服务器发起sse请求
    const es = new EventSource("http://127.0.0.1:8080/stream");
    // Event 和 Message 分开处理,需要显示的监听事件,否则不会处理事件
    es.onmessage = function (e) {
        handleEventStreamMessage(e);
    }
    // 监听事件流
    es.addEventListener("start", (e) => {
        handleEventStreamMessage(e);
    });
    es.addEventListener("end", (e) => {
        handleEventStreamMessage(e);

        // 一定要关闭连接,否则会一直轮训
        es.close()
    });
    es.onerror = function (e) {
        // readyState说明
        // 0:浏览器与服务端尚未建立连接或连接已被关闭
        // 1:浏览器与服务端已成功连接,浏览器正在处理接收到的事件及数据
        // 2:浏览器与服务端建立连接失败,客户端不再继续建立与服务端之间的连接
        console.log("readyState = " + e.currentTarget.readyState);
    }
</script>

</html>
二、GIN 中自带的 SSE
package main

import (
	"time"

	"github.com/gin-contrib/sse"
	"github.com/gin-gonic/gin"
)

func main() {
	engin := gin.Default()

	engin.Any("/stream", func(c *gin.Context) {
		c.Header("Access-Control-Allow-Origin", "*")
		c.Header("Access-Control-Allow-Headers", "*")

		// c.SSEvent("start", "start...")
		sse.Event{
			Id:    "1",
			Event: "start",
			Data:  "start...",
		}.Render(c.Writer)

		c.Writer.Flush()
		time.Sleep(1 * time.Second)

		for i := 0; i < 10; i++ {
			sse.Event{
				Id:   "1",
				Data: "SSE data",
			}.Render(c.Writer)

			c.Writer.Flush() // 需要手动刷新输出缓冲区

			time.Sleep(1 * time.Second)
		}

		// c.SSEvent("end", "end...")
		sse.Event{
			Id:    "1",
			Event: "end",
			Data:  "end...",
		}.Render(c.Writer)
	})

	engin.Run(":8080")
}

说明

// sse.Event
type Event struct {
	Event string
	Id    string
	Retry uint
	Data  interface{}
}

sse.Event 结构在渲染的时候会自动加上前缀和后面的回车,比如id:xxx\nevent:xxx\nretry:xxx\ndata:xxx\n\n,因此在设置内容的时候不需要关心format。

并且会自动填充两个响应头
Content-Type: text/event-stream
Cache-Control: no-cache

如果服务器端提供了event参数,那么客户端就需要使用addEventListener 显式监听这个事件,才会正常获取消息,否则事件不会触发。如果服务器端没有提供event 参数,只有id、data等,可以使用onmessage回调监听消息。

id 的意思是 lastEventId,用途不明。

完整的数据结构是:id:xxx\nevent:xxx\nretry:xxx\ndata:xxx\n\n

一般只需要data字段即可,后面接一个json串。

前端使用EventSource对象发起请求

在这里插入图片描述

在这里插入图片描述

使用 fetch 的方式发起请求,需要先打开调试并打开接口的响应预览tab,否则是看不到响应结果的。

在这里插入图片描述

在这里插入图片描述

使用EventSource对象发起请求与使用fetch方式的请求两者的区别在于,在处理响应结果的时候,前者是按照SSE协议来处理消息中的\n\n\n以及那几个字段;而后者则不会。下面是打印结果

EventSource示例

在这里插入图片描述

fetch示例

在这里插入图片描述

上面的实现仅仅是为了满足ChatGPT这种对话形式,或者说仅仅实现了一个长连接下的流式传输,即使不适用SSE也能实现。

如果想要实现真正的消息推送还需要对客户端连接进行管理,在这一块,SSE和websocket要做的事情差不多,这里就不展开了。

三、使用golang请求chatgpt

大部分的时候,在客户端和chatgpt之间还需要有一个代理层,即它代替用户向chatgpt发起请求,接收数据流,然后将数据流转发给用户。前面已经实现了SSE,所以,这里需要处理的是golang发起stream request。

package main

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"time"
)

func main() {
	client := &http.Client{Timeout: time.Second * 20}
	req, _ := http.NewRequest("POST", "http://127.0.0.1:8080/stream", strings.NewReader(""))

	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}

	reader := bufio.NewReader(resp.Body)
	defer resp.Body.Close()

	for {
		rawLine, err := reader.ReadBytes('\n')
		if errors.Is(err, io.EOF) {
			return
		} else if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Println(string(bytes.TrimRight(rawLine, "\n")))
	}
}
id:1
event:start
data:start...

id:1
data:SSE data

id:1
data:SSE data

id:1
data:SSE data

id:1
data:SSE data

id:1
data:SSE data

id:1
data:SSE data

id:1
data:SSE data

id:1
data:SSE data

id:1
data:SSE data

id:1
data:SSE data

id:1
event:end
data:end...

golang对接openai:https://github.com/sashabaranov/go-openai

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

树与图的深度优先遍历(dfs的图论中的应用)

模板题 846. 树的重心 给定一颗树&#xff0c;树中包含 nn 个结点&#xff08;编号 1∼n&#xff09;和 n−1条无向边。 请你找到树的重心&#xff0c;并输出将重心删除后&#xff0c;剩余各个连通块中点数的最大值。 重心定义&#xff1a;重心是指树中的一个结点&#xff…

RabbitMQ 高级特性——发送方确认

文章目录 前言发送方确认confirm 确认模式return 退回模式 常见面试题 前言 前面我们学习了 RabbitMQ 中交换机、队列和消息的持久化&#xff0c;这样能够保证存储在 RabbitMQ Broker 中的交换机和队列中的消息实现持久化&#xff0c;就算 RabbitMQ 服务发生了重启或者是宕机&…

【Android】浅析MVC与MVP

【Android】浅析MVC与MVP 什么是架构&#xff1f; 架构&#xff08;Architecture&#xff09;在软件开发中指的是软件系统的整体设计和结构&#xff0c;它描述了系统的高层组织方式&#xff0c;包括系统中各个组件之间的关系、依赖、交互方式&#xff0c;以及这些组件如何协同…

基于OpenCV的YOLOv5图片检测

利用OpenCV的DNN模块加载onnx模型文件进行图片检测。 1、使用的yolov5工程代码&#xff0c;调用export.py导出onnx模型。 2、下载opencv版本&#xff0c;https://opencv.org/releases/ 使用opencv版本4.5.3或以上&#xff0c;本文使用的opencv4.6.0 3、使用vc20…

4.使用 VSCode 过程中的英语积累 - View 菜单(每一次重点积累 5 个单词)

前言 学习可以不局限于传统的书籍和课堂&#xff0c;各种生活的元素也都可以做为我们的学习对象&#xff0c;本文将利用 VSCode 页面上的各种英文元素来做英语的积累&#xff0c;如此做有 3 大利 这些软件在我们工作中是时时刻刻接触的&#xff0c;借此做英语积累再合适不过&a…

STM32 使用 CubeMX 实现按键外部中断

目录 问题背景知识参考需要改什么注意尽量不要在中断函数使用 循环函数做延时中断函数中延时方法调试 问题 我想实现按钮触发紧急停止类似功能&#xff0c;需要使用按键中断功能。 背景知识 GPIO 点亮 LED。stm32cubemx hal学习记录&#xff1a;GPIO输入输出。STM32—HAL库 …

【实战篇】MySQL是怎么保证高可用的?

背景 在一个主备关系中&#xff0c;每个备库接收主库的 binlog 并执行。正常情况下&#xff0c;只要主库执行更新生成的所有 binlog&#xff0c;都可以传到备库并被正确地执行&#xff0c;备库就能达到跟主库一致的状态&#xff0c;这就是最终一致性。 但是&#xff0c;MySQL…

免费在线压缩pdf 压缩pdf在线免费 推荐简单好用

压缩pdf在线免费&#xff1f;在日常生活和工作学习中&#xff0c;处理PDF文件是常见任务。但有时PDF文件体积较大&#xff0c;给传输、存储和分享带来不便。因此&#xff0c;学习PDF文件压缩技巧十分必要。压缩PDF文件是指通过技术手段减小文件占用的存储空间&#xff0c;同时尽…

[Redis][Hash]详细讲解

目录 0.前言1.常见命令1.HSET2.HGET3.HEXISTS4.HDEL5.HKEYS6.HVALS7.HGETALL8.HMGET9.HLEN10.HSETNX11.HINCRBY12.HINCRBYFLOAT 2.内部编码1.ziplist(压缩链表)2.hashtable(哈希表) 3.使用场景4.缓存方式对比1.原⽣字符串类型2.序列化字符串类型3.哈希类型 0.前言 在Redis中&am…

CSS - 通用左边图片,右边内容,并且控制长度溢出处理模板(vue | uniapp | 微信小程序)

前言 通用模板&#xff0c;可适用于任意前端项目。 如下图所示&#xff0c;手机电脑通用。 示例代码 根据自己的需求修改即可。 <body><div class"container"><!-- 头像图片 --><img class"avatar" src"https://cdn.uviewui.com…

C++初阶学习——探索STL奥秘——标准库中的priority_queue与模拟实现

1.priority_queque的介绍 1.priority_queue中文叫优先级队列。优先队列是一种容器适配器&#xff0c;根据严格的弱排序标准&#xff0c;它的第一个元素总是它所包含的元素中最大的。 2. 此上下文类似于堆&#xff0c;在堆中可以随时插入元素&#xff0c;并且只能检索最大堆元…

学习大数据DAY59 全量抽取和增量抽取实战

目录 需求流程&#xff1a; 需求分析与规范 作业 作业2 需求流程&#xff1a; 全量抽取 增量抽取 - DataX Kettle Sqoop ... 场景: 业务部门同事或者甲方的工作人员给我们的部门经理和你提出了新的需 求 流程: 联系 > 开会讨论 > 确认需求 > 落地 需求文档( 具体…

Vue 项目中引入 Axios 详解

Vue 项目中引入 Axios 详解 在 Vue 项目中&#xff0c;axios 是一个非常流行的 HTTP 客户端&#xff0c;用于向服务器发送请求并处理响应。本文将详细说明如何在 Vue 项目中引入 Axios 插件&#xff0c;以及如何进行基本的配置&#xff0c;包括构建、配置域名、设置全局错误拦…

WEB攻防-JS项目Node.js框架安全识别审计验证绕过

知识点&#xff1a; 1、原生JS&开发框架-安全条件 2、常见安全问题-前端验证&未授权 详细点&#xff1a; 1、什么是JS渗透测试&#xff1f; 在JavaScript中也存在变量和函数&#xff0c;当存在可控变量及函数调用即可参数漏洞 2、流行的Js框架有哪些&#xff1f; …

CC1链的第二种方式-LazyMap版调用链

文章目录 CC1链的第二种方式-LazyMap版调用链LazyMap构造payloadCC1的调用链 CC1链的第二种方式-LazyMap版调用链 CC1链的第一种方式可以参考另一篇文章&#xff1a;CC1链_全网最菜的分析思路 LazyMap 在之前的CC1链中分析&#xff0c;其实是其中一种方式&#xff08;国内版本…

全面解析流量态势感知与网络性能监控:IT运维中的核心技术

在现代IT运维中&#xff0c;网络的稳定性和业务的连续性是企业赖以生存的基石。随着数字化转型的深入&#xff0c;网络流量日益复杂&#xff0c;安全威胁愈加严峻&#xff0c;运维人员不仅需要确保网络的顺畅运行&#xff0c;还必须及时发现潜在风险并快速响应。流量态势感知与…

如何查看Android设备的dpi

adb shell getprop ro.sf.lcd_density adb shell cat /system/build.prop > build_prop.txt shell cat system/build.prop 结果&#xff1a;参考&#xff1a; 如何查看Android设备的dpi_安卓 查看手机dpi-CSDN博客

ABAP-Swagger 一种公开 ABAP REST 服务的方法

ABAP-Swagger An approach to expose ABAP REST services 一种公开 ABAP REST 服务的方法 Usage 1: develop a class in ABAP with public methods 2: implement interface ZIF_SWAG_HANDLER, and register the public methods(example method zif_swag_handler~meta) 3: …

ElementUI 用span-method实现循环el-table组件的合并行功能

需要把指定列的相同数据合并起来&#xff08;项目中用的是updateTime&#xff09; 后端返回的数据格式&#xff1a; html&#xff1a; <el-tab-pane label"执行记录概览" name"fourth" v-loading"loading"><el-timeline v-if"re…

单元测试和unittest框架(超详细总结)

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;薪资嘎嘎涨 单元测试的定义 1. 什么是单元测试&#xff1f; 单元测试是指&#xff0c;对软件中的最小可测试单元在与程序其他部分相隔离的情况下进行检查和验证的工作&am…