限时福利领取


流式传输示意图

问题背景:传统传输的痛点

在传统的HTTP文件传输中,服务端通常需要将整个文件加载到内存中,再通过HttpServletResponse一次性输出。当处理大文件(如视频、日志文件)或实时数据流(如股票行情)时,这种模式会导致:

  • 内存爆炸:一个1GB的文件需要消耗同等大小的JVM堆内存
  • 响应延迟:必须等待全部数据就绪才能开始传输
  • 客户端体验差:用户长时间看不到任何数据加载

技术选型:Chunked vs SSE

两种流式传输技术的核心区别:

  1. 分块编码(Chunked Transfer Encoding)
  2. HTTP/1.1标准协议
  3. 适用任意二进制/文本数据
  4. 服务端主动推送,客户端被动接收

  5. SSE(Server-Sent Events)

  6. 基于HTTP的长连接
  7. 仅支持UTF-8文本
  8. 支持事件类型和自动重连

协议对比

Spring Boot实现方案

使用ResponseBodyEmitter实现异步流式输出:

@GetMapping("/stream")
public ResponseBodyEmitter handleStream() {
    ResponseBodyEmitter emitter = new ResponseBodyEmitter();
    executor.execute(() -> {
        try {
            // 模拟分块发送
            for (int i = 0; i < 100; i++) {
                emitter.send("Chunk " + i + "\n");
                Thread.sleep(100); // 控制流速
            }
            emitter.complete();
        } catch (Exception ex) {
            emitter.completeWithError(ex);
        }
    });
    return emitter;
}

关键配置项:

  • spring.mvc.async.request-timeout=30000 设置超时时间
  • 必须使用@EnableAsync启用异步支持

原生Servlet实现

手动控制输出流的分块写入:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
    resp.setContentType("text/plain");
    resp.setHeader("Transfer-Encoding", "chunked");

    try (ServletOutputStream out = resp.getOutputStream()) {
        byte[] buffer = new byte[4096]; // 建议4KB的chunk size
        while (hasMoreData()) {
            int len = readData(buffer);
            out.write(buffer, 0, len);
            out.flush();  // 关键!立即发送当前块
        }
    } catch (IOException e) {
        log.error("Stream interrupted", e);
    }
}

生产环境最佳实践

  1. 连接保持策略
  2. 设置合理的Keep-Alive超时时间
  3. 心跳机制:定期发送空行保持连接

  4. 背压控制

  5. 监控输出队列积压情况
  6. 当客户端消费速度过慢时暂停发送

  7. 性能调优

  8. 使用Wireshark抓包验证分块格式
  9. 典型Chunk结构:[长度]\r\n[数据]\r\n

避坑指南

  • 必须设置响应头:缺少Transfer-Encoding: chunked会导致浏览器无法正确解析
  • Chunk Size选择
  • 太小(如1KB):增加协议开销
  • 太大(如1MB):失去流式意义
  • 推荐4KB-16KB平衡性能
  • 字符编码陷阱:二进制流需明确禁用字符转换
// 错误示例:会导致数据损坏
resp.setCharacterEncoding("UTF-8"); 
// 正确做法:二进制流禁用编码
resp.setContentType("application/octet-stream");

实测效果

在4核8G的测试环境中,传输1GB文件时:

| 方式 | 内存占用 | 首字节时间 | |---------------|----------|------------| | 传统缓冲 | 1GB | 5.2s | | 分块传输 | <10MB | 0.01s |

性能对比图

总结

分块传输编码就像『用传送带搬货』,相比传统『整箱搬运』的优势显而易见。在实际项目中,建议:

  1. 动态内容优先使用Spring封装好的ResponseBodyEmitter
  2. 静态文件考虑Nginx的X-Accel-Redirect更高效
  3. 实时监控HttpOutputMessage的写入状态

遇到连接中断等异常时,记得记录最后成功发送的位置,方便实现断点续传。完整示例代码已上传GitHub(链接见文末)。

Logo

音视频技术社区,一个全球开发者共同探讨、分享、学习音视频技术的平台,加入我们,与全球开发者一起创造更加优秀的音视频产品!

更多推荐