概述
本文是笔者的系列博文 《Bun技术评估》 中的第二十九篇。
在本文的内容中,笔者主要想要来探讨一下Bun中和SSE(服务器端消息)集成开发相关的问题。
关于SSE,笔者其实已经有几篇博文有所涉及,自认为已经解析的非常清晰了。所以本文主要讨论的内容并不是关于这个概念和技术,而是展示一下其在Bun开发中的实现。
如果关于这个技术读者想要进一步了解,可以参考下面这几篇相关博文:
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应用的示例代码,包括了服务端的配置和客户端的处理。读者应该可以感受到其实现的简洁和方便。