You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Hono与Bun流式传输并消费两个二进制文件?

解决方案

核心问题

直接把两个文件的二进制流拼在一起发送,消费端根本没法区分文件边界——就像把两本书的页粘成一本,没人分得清哪页属于哪本书。所以必须在服务端的流里加明确的“分隔标记”,这里推荐用「先传文件元数据(文件名+大小),再传文件内容」的方式,比随便找个特殊符号当分隔符靠谱多了(毕竟二进制文件里啥奇怪内容都可能有,万一刚好撞上分隔符就会拆分错误)。

1. 修改服务端:给每个文件添加元数据前缀

服务端要在每个文件流的前面,先把该文件的文件名、大小转成二进制发送,而且得先告诉消费端“接下来多少字节是元数据”,不然消费端不知道该读多少字节来解析元数据。

import { Hono } from "hono";
import { stream } from "hono/streaming";

const app = new Hono();

// 将文件元数据转为带长度前缀的二进制,方便消费端识别边界
async function sendMetadata(stream: any, meta: { filename: string; size: number }) {
  const metaJson = JSON.stringify(meta);
  const metaBytes = new TextEncoder().encode(metaJson);
  // 用4字节无符号整数存储元数据长度(大端序,保证跨平台兼容)
  const lengthBuf = new ArrayBuffer(4);
  new DataView(lengthBuf).setUint32(0, metaBytes.length, false);
  // 先发送长度前缀,再发送元数据内容
  await stream.write(new Uint8Array(lengthBuf));
  await stream.write(metaBytes);
}

app.get("/", (c) => {
  return stream(c, async (stream) => {
    stream.onAbort(() => {
      console.log("传输被中止!");
    });

    // 传输第一个文件:视频
    const videoFile = Bun.file("./data/worker/video.mp4");
    console.log("开始传输视频文件...");
    await sendMetadata(stream, { filename: "video.mp4", size: videoFile.size });
    await stream.pipe(videoFile.stream());

    // 传输第二个文件:音频
    const audioFile = Bun.file("./data/worker/audio.mp4");
    console.log("开始传输音频文件...");
    await sendMetadata(stream, { filename: "audio.mp4", size: audioFile.size });
    await stream.pipe(audioFile.stream());

    console.log("两个文件传输完成!");
  });
});

console.log("服务端运行在端口3001");

export default {
  port: 3001,
  fetch: app.fetch,
};

2. 修改消费端:按元数据拆分流并保存文件

消费端要先读取4字节的元数据长度,再读取对应长度的字节解析出文件名和大小,然后精准读取对应大小的文件内容,写完一个文件再处理下一个,直到流结束。

import { Hono } from "hono";

const app = new Hono();

// 从流中精准读取指定长度的字节
async function readFixedLength(reader: ReadableStreamDefaultReader<Uint8Array>, length: number): Promise<Uint8Array> {
  const result = new Uint8Array(length);
  let offset = 0;
  while (offset < length) {
    const { done, value } = await reader.read();
    if (done) throw new Error("流提前中断");
    result.set(value, offset);
    offset += value.length;
  }
  return result;
}

// 读取并解析文件元数据
async function getFileMetadata(reader: ReadableStreamDefaultReader<Uint8Array>) {
  // 先读取4字节的元数据长度
  const lengthBuf = await readFixedLength(reader, 4);
  const metaLength = new DataView(lengthBuf.buffer).getUint32(0, false);
  // 读取元数据内容并解析
  const metaBytes = await readFixedLength(reader, metaLength);
  const metaJson = new TextDecoder().decode(metaBytes);
  return JSON.parse(metaJson) as { filename: string; size: number };
}

app.get("/", async (c) => {
  console.log("开始从服务端拉取文件流...");

  const response = await fetch("http://localhost:3001/");
  if (!response.ok) {
    return c.json({ error: "拉取文件流失败" }, 500);
  }

  const reader = response.body!.getReader();
  const outputDir = "./data/output/";
  // 确保输出目录存在,Bun.write到目录会自动创建目录
  await Bun.write(outputDir, "");

  try {
    while (true) {
      // 获取当前文件的元数据
      const meta = await getFileMetadata(reader);
      console.log(`开始接收文件:${meta.filename},大小${meta.size}字节`);
      
      // 读取对应大小的文件内容
      const fileData = await readFixedLength(reader, meta.size);
      
      // 写入本地文件
      await Bun.write(`${outputDir}${meta.filename}`, fileData);
      console.log(`${meta.filename}保存完成`);
    }
  } catch (err) {
    if ((err as Error).message === "流提前中断") {
      console.log("所有文件接收完成");
    } else {
      console.error("接收文件出错:", err);
      return c.json({ error: "接收文件失败" }, 500);
    }
  }

  return c.json({ message: "所有文件流式传输并保存成功" });
});

export default app;

方案优势

  • 可靠性高:用文件大小拆分流,不会出现特殊分隔符和文件内容冲突的问题;
  • 扩展性强:元数据携带文件名,后续新增文件无需修改消费端逻辑;
  • 跨平台兼容:用标准的大端序存储长度,避免不同系统的字节序差异问题。

内容的提问来源于stack exchange,提问作者empz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 01:53:17