Bun技术评估 - 30 SSE支持

概述

本文是笔者的系列博文 《Bun技术评估》 中的第二十九篇。

在本文的内容中,笔者主要想要来探讨一下Bun中和SSE(服务器端消息)集成开发相关的问题。

关于SSE,笔者其实已经有几篇博文有所涉及,自认为已经解析的非常清晰了。所以本文主要讨论的内容并不是关于这个概念和技术,而是展示一下其在Bun开发中的实现。

如果关于这个技术读者想要进一步了解,可以参考下面这几篇相关博文:

《Nodejs开发进阶S-WebSocket和SSE》

《基于SSE的服务集群状态和数据同步》

《Nodejs SSE示例代码和解析》

Bun实现重点

SSE本质上而言,就是一个HTTP协议的扩展应用,所以理论上,任何完整实现HTTP协议的技术和框架,都可以改进成为支持SSE。笔者简单的总结了一下,只需要注意以下几个问题:

  • 请求响应时,服务器的响应头有特殊的内容,包括
  • content-type: text/event-stream
  • connection: keep-alive
  • 响应的实际内容,应该是一个文本流,但SSE有一定的规范比如使用"\n\n"消息结束字符
  • 理论上开发者可以自定义任意的消息格式和处理方式
  • 所以,在服务端和客户端都需要有内容流的处理机制
  • 服务端需要有多个客户端的管理机制,这和普通HTTP请求响应处理有很大区别,和WS类似
  • 和WS相比巨大的优势就是HTTP协议的内置支持,无需协议升级,网络环境兼容性更好;问题是半双工的工作模式;适合特定的应用场景

其实,理解了以上技术内核,在Bun中,实现SSE是非常简单的。下面就是笔者编写的示例代码,可以单文件执行和测试。

实现代码和解析

相关的参考示例代码如下:

sse.ts 复制代码
const
EventEmitter = require('events'),
EM_PUSH = new EventEmitter(),
SSE_DATA = "data: ",
SSE_END  = "\n\n",
EV_HEADER = {
    "Access-Control-Allow-Origin": "*",
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
};

const 
PORT = 7085,
HOST = "0.0.0.0",
PATH_SSE = "/sse/:clientid";

const CLIENTS = [];
const onAbort = (ctrLer)=>{
    const index = CLIENTS.indexOf(ctrLer);
    if (index > -1) CLIENTS.splice(index, 1);
};

const pushHandle = (req)=>{
    const  clientid = req.params.clientid;
        // 2. 创建事件流
    const stream = new ReadableStream({
        start(controller) {
            controller.clientid = clientid;
            // 3. 将新客户端的控制器存入全局数组
            CLIENTS.push(controller);

            const encoder = new TextEncoder();

            // 可选:立即发送一条欢迎消息或初始数据
            controller.enqueue(encoder.encode( " Welcome : client : " + clientid + SSE_END));

            // 4. 当客户端关闭连接时,将其从数组中移除
            req.signal.addEventListener("abort", ()=>onAbort(controller));
        },
    });

    return new Response(stream, { headers: EV_HEADER });
}

const routes = {
    [PATH_SSE] : pushHandle
};

const start=(port = 7085, hostname="0.0.0.0")=>{
    const server = Bun.serve({ port, hostname, 
        routes, 
        fetch: (req) => new Response("Not Found: "+ req.url, { status:  404 }), 
        development: true 
    });

    console.log(`Server running at http://${server.hostname}:${server.port}`);
    console.log("Routes: \n", Object.keys(routes).join(" \n"));

    // start push
    // const encoder = 
    EM_PUSH.on("idxData", (data)=>{
        CLIENTS.forEach(c => {
            c.enqueue(new TextEncoder().encode(SSE_DATA + data + SSE_END));
        });
    });

    setInterval(()=> EM_PUSH.emit("idxData", "somekey"+ Math.random().toString().slice(-4) +":"+ "someValue"), 2000);
}; start(PORT, HOST);

const clientStart = async ()=>{
    const abortController = new AbortController();
    const url = `http://localhost:${PORT}${PATH_SSE.replace(":clientid",Math.random().toString(36).slice(-8) )}`;
    const headers = {
        'Accept': 'text/event-stream',
        'Cache-Control': 'no-cache'
    };

    try {
        console.log("request:", url);
        // 2. 发起fetch请求,注意设置Accept头部和signal
        const res = await fetch(url, { 
            method: "GET",
            headers, 
            signal: abortController.signal 
        });// 关联中止信号
        
        if (!res.ok) {
            console.log("Error", res.status, await res.text() || "Unknow Errorr");
            return;
        }

        // 3. 检查响应内容类型,确保是事件流
        const contentType = res.headers.get('content-type');
        if (!contentType || !contentType.includes('text/event-stream'))  throw new Error(`Expected event-stream content-type, got: ${contentType}`);

        
        // 4. 获取可读流和读取器
        const reader = res.body.getReader();
        const decoder = new TextDecoder();
        let buffer = ''; // 缓冲区,用于处理可能被截断的消息

        // 5. 持续读取流数据
        while (true) {
            const { done, value } = await reader.read();
      
            if (done) break;
            // onComplete?.(); // 流正常结束

            // 6. 将二进制的数据块解码为文本并添加到缓冲区
            buffer += decoder.decode(value, { stream: true });
      
            // 7. 按照SSE协议规范,以两个换行符为分隔符拆分消息
            const messages = buffer.split('\n\n');
            // 最后一个元素可能是半条消息,放回缓冲区等待下次数据
            buffer = messages.pop() || '';

            let mtype,mkey,mvalue;
            for (const message of messages) {
                [mtype, mkey, mvalue] = message.split(":");                
                console.log({mtype, mkey, mvalue});
            }; 
        }
    } catch (error) {
        console.log("Error", error.message);
    }
}; setTimeout(clientStart, 2000);

执行和效果如下:

css 复制代码
bun --watch sse
 
Server running at http://0.0.0.0:7085
Routes: 
 /sse/:clientid
request: http://localhost:7085/sse/zq5zvd79
{
  mtype: " Welcome ",
  mkey: " client ",
  mvalue: " zq5zvd79",
}
{
  mtype: "data",
  mkey: " somekey7556",
  mvalue: "someValue",
}
{
  mtype: "data",
  mkey: " somekey8487",
  mvalue: "someValue",
}
{
  mtype: "data",
  mkey: " somekey7285",
  mvalue: "someValue",
}

简单说明一下,SSE在Bun的实现原理和一些细节:

  • 程序无任何外部依赖,可以独立直接执行
  • 程序分为两个部分,服务端和客户端(用于自测试),实现了完整的网络应用环境和过程
  • 服务端使用正常方式启动,但为SSE配置相关路由
  • 客户端使用Fetch发起请求,路径为SSE路径,请求方式是标准GET,增加相应头
  • 由于使用标准fetch方法,理论上也可以在浏览器中执行
  • 第一次请求,服务端响应一个数据流,并包括相应的头信息(保存连接和响应类型)
  • 客户端检查响应状态和内容类型(头信息)
  • 客户端通过配置一个读取器(reader),准备接收数据流
  • 连接建立后,服务端将当前这个连接的控制器,放入客户端列表中
  • 服务端使用一个消息总线(EventEmitter),接收要发送的消息,并遍历客户端列表发送消息(controller.queue)
  • 消息总线用于解耦SSE服务器和业务应用,业务应用只需要按照业务需求,发送数据到总线即可,不用关心服务器方面的问题
  • 发送的消息是文本消息,具有固定格式,包括类型、键值对和分隔符
  • 客户端持续使用流reader接收消息,并持续处理收到的数据信息
  • 如果发生中断,服务端会清除客户端连接(控制器)

后续的增加和改进可以包括:

  • 可以在连接创建时,对客户端进行认证满足业务安全需求(此处没有实现)
  • 使用路径,来区分正常请求和SSE请求,甚至支持更多的配置信息,如不同的SSE通道等等
  • 可以附带clientid,然后实现更精准的按需数据推送(已基本实现)

小结

本文探讨了在Bun中,实现HTTP的SSE特性的相关内容。文中展示了完整的在Bun支持SSE应用的示例代码,包括了服务端的配置和客户端的处理。读者应该可以感受到其实现的简洁和方便。

相关推荐
程序猿_极客1 小时前
【2025最新】 Java 入门到实战:数组 + 抽象类 + 接口 + 异常(含案例 + 语法全解析+巩固练习题)
java·开发语言·后端·java基础·java入门到实战
yzx9910131 小时前
一个嵌入式存储芯片质量评估系统的网页界面设计
开发语言·javascript·ecmascript
v***43171 小时前
spring.profiles.active和spring.profiles.include的使用及区别说明
java·后端·spring
fruge1 小时前
前端可视化家庭账单:用 ECharts 实现支出统计与趋势分析
前端·javascript·echarts
IT_陈寒2 小时前
Vue3性能优化实战:5个被低估的Composition API技巧让你的应用快30%
前端·人工智能·后端
Moe4882 小时前
@SpringBootApplication 注解(Spring Boot 自动配置)详解
java·后端
后端小张2 小时前
【JAVA 进阶】SpringBoot 事务深度解析:从理论到实践的完整指南
java·开发语言·spring boot·后端·spring·spring cloud·事务
间彧2 小时前
Docker Compose 数据卷挂载详解与项目实战
后端
荔枝吖2 小时前
html2canvas+pdfjs 打印html
前端·javascript·html