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应用的示例代码,包括了服务端的配置和客户端的处理。读者应该可以感受到其实现的简洁和方便。

相关推荐
摘星编程8 小时前
OpenHarmony环境下React Native:Sensors摇一摇换图
javascript·react native·react.js
码界奇点8 小时前
基于Spring Boot与Vue的校园后台管理系统设计与实现
vue.js·spring boot·后端·毕业设计·源代码管理
爱编程的小庄8 小时前
Rust 发行版本及工具介绍
开发语言·后端·rust
2501_944526428 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 关于页面实现
android·java·开发语言·javascript·python·flutter·游戏
咸鱼2.08 小时前
【java入门到放弃】VUE部分知识点
java·javascript·vue.js
web小白成长日记9 小时前
Vue3中如何优雅实现支持多绑定变量和修饰符的双向绑定组件?姜姜好
前端·javascript·vue.js
十六年开源服务商9 小时前
WordPress在线聊天系统推荐
大数据·javascript·html
Apifox.9 小时前
测试用例越堆越多?用 Apifox 测试套件让自动化回归更易维护
运维·前端·后端·测试工具·单元测试·自动化·测试用例
喵喵喵小鱼9 小时前
arcgis JavaScript api实现同时展示多个撒点气泡
开发语言·javascript·arcgis
sunnyday04269 小时前
Nginx与Spring Cloud Gateway QPS统计全攻略
java·spring boot·后端·nginx