前后端流式传输技术详解 笔记

前后端流式传输技术详解

在现代Web应用开发中,实时数据传输已成为提升用户体验的关键技术。本文将详细介绍四种主流的前后端流式传输方式:SseEmitter、ResponseBodyEmitter、StreamingResponseBody和WebSocket,并分析各自的特点、适用场景以及实现方式。

一、Server-Sent Events (SSE) 与 SseEmitter

1.1 SSE 技术简介

Server-Sent Events (SSE) 是一种基于HTTP的服务器推送技术,允许服务器向客户端发送事件流。与WebSocket不同,SSE是单向通信的,仅支持服务器向客户端推送数据。

SSE的主要特点:

  • 基于HTTP协议,无需额外的协议支持
  • 自动重连机制
  • 支持自定义事件
  • 单向通信(服务器→客户端)
  • 文本格式传输

1.2 SseEmitter 实现

Spring框架提供了SseEmitter类来实现SSE功能。

后端代码
@GetMapping("/T1")
SseEmitter T1() {
    SseEmitter emitter = new SseEmitter();
    new Thread(() -> {
        try {
            for (int i = 0; i < 20; i++) {
                emitter.send(SseEmitter.event().id(String.valueOf(i))
                        .name("msg").data("sse" + i));
                Thread.sleep(100);
            }
            emitter.complete();
        } catch (Exception e) { emitter.completeWithError(e);}
    }).start();
    return emitter;
}

技术要点:

  • SseEmitter类创建了一个SSE连接
  • emitter.send()方法用于发送事件,可设置事件ID、名称和数据
  • emitter.complete()表示传输完成
  • 实际应用中通常在新线程中处理数据发送,避免阻塞主线程
前端代码
// 创建EventSource对象连接到服务器端点
const source = new EventSource('/api/test');

// 连接成功的回调
source.onmessage = function(event) {
  console.log('接收到消息:', event.data);
  // 处理接收到的数据
}

// 监听特定事件(如果后端发送了命名事件)
source.addEventListener("msg", function(event) {
  console.log('接收到msg事件:', event.data);
}, false);

// 处理错误
source.addEventListener("error", function(err) {
  console.error('发生错误:', err);
  // 可以根据错误状态码处理不同情况
  err && err.status === 401 && console.log('未授权');
})

// 关闭连接
// source.close();

技术要点:

  • EventSource是浏览器原生支持的SSE客户端API
  • 支持自动重连(除非连接返回HTTP 204)
  • 可以监听命名事件或默认消息
  • 需要手动关闭连接

1.3 适用场景

SSE特别适合以下场景:

  • 实时通知和提醒
  • 实时数据更新(如股票价格、天气更新)
  • 社交媒体信息流
  • 日志或事件监控
  • AI聊天应用中的流式响应

二、ResponseBodyEmitter

2.1 技术简介

ResponseBodyEmitter是Spring框架提供的一个用于异步HTTP响应的组件,允许在一个HTTP请求中多次写入响应数据。与SSE不同,它不遵循特定的事件格式,而是发送原始数据流。

2.2 实现方式

后端代码
@GetMapping("/T2")
ResponseBodyEmitter T2() {
    ResponseBodyEmitter emitter = new ResponseBodyEmitter();
    new Thread(() -> {
        try {
            for (int i = 0; i < 20; i++) {
                emitter.send("res" + i);
                // 模拟数据生成时间
                Thread.sleep(100);
            }
            emitter.complete();
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    }).start();
    return emitter;
}

技术要点:

  • ResponseBodyEmitter提供了低级别的响应控制
  • 可以发送任何类型的数据,不限于文本
  • 支持分块传输编码(chunked transfer encoding)
  • 需要手动处理完成和错误情况

2.3 适用场景

ResponseBodyEmitter适用于:

  • 大型数据集的分块传输
  • 需要自定义数据格式的流式传输
  • 长时间运行的计算结果的流式输出
  • 不需要事件格式化的简单流式数据

三、StreamingResponseBody

3.1 技术简介

StreamingResponseBody是Spring框架提供的另一种流式响应机制,它直接操作底层输出流,提供了最大的灵活性和控制力。

3.2 实现方式

后端代码
@GetMapping("/T3")
StreamingResponseBody T3() {
    return new StreamingResponseBody() {
        @Override
        public void writeTo(OutputStream out) throws IOException {
            for (int i = 0; i < 20; i++) {
                out.write(("str" + i).getBytes());
                out.flush();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };
    
    // Lambda表达式的替代写法
    // return outputStream -> {
    //     for (int i = 0; i < 20; i++) {
    //         outputStream.write(("str" + i).getBytes());
    //         outputStream.flush();
    //         try {
    //             Thread.sleep(100);
    //         } catch (InterruptedException e) {
    //             e.printStackTrace();
    //         }
    //     }
    // };
}

技术要点:

  • 直接操作原始的OutputStream
  • 提供最细粒度的输出控制
  • 需要手动处理字符编码
  • 需要手动调用flush()确保数据及时发送
  • 执行会在单独的线程中进行,不需要额外创建线程

3.3 适用场景

StreamingResponseBody特别适合:

  • 大文件下载
  • 需要精确控制字节输出的场景
  • 视频或音频流
  • 二进制数据传输
  • 性能要求极高的应用

3.4 ResponseBodyEmitter 和 StreamingResponseBody 的前端接收代码

this.text = ''
const that = this
fetch('/stream/T2')  // 或 '/stream/T3'
.then(res => {
    const reader = res.body.getReader()
     reader.read().then(function processText({ done, value }) {
       if (done) return
       // 处理每个数据块的逻辑
       const chunk = new TextDecoder('utf-8').decode(value)
       that.text += chunk
       console.log(chunk)
       // 读取下一个数据块
       return reader.read().then(processText)
     })
   })
.catch(console.error)

技术要点:

  • 使用Fetch API的Response.body.getReader()获取数据流
  • TextDecoder用于将二进制数据转换为文本
  • 递归调用read()方法处理整个流
  • 适用于任何响应类型,不仅限于文本

四、WebSocket 通信

4.1 技术简介

WebSocket 是一种在单个TCP连接上进行全双工通信的协议,允许服务器和客户端之间持续、双向的数据交换。与前三种技术不同,WebSocket:

  • 基于TCP协议,而非HTTP
  • 支持双向通信
  • 具有更低的延迟
  • 需要特殊的握手过程建立连接
  • 支持二进制和文本传输

4.2 实现方式

后端代码 (使用Java WebSocket API)
@ServerEndpoint("/webSocket")
@Component
public class EchoServer {
    // 存储所有连接的会话
    private static final Set<Session> sessions = Collections.synchronizedSet(new HashSet<Session>());
    
    @OnOpen
    public void onOpen(Session session) {
        System.out.println("连接成功:" + session.getId());
        sessions.add(session);
    }
    
    @OnMessage
    public void onMessage(Session session, String message) throws IOException {
        System.out.println("收到消息:" + message);
        session.getBasicRemote().sendText("服务端接收到的消息是:" + message);
        
        // 群发消息的示例
        // for (Session s : sessions) {
        //     s.getBasicRemote().sendText("广播消息:" + message);
        // }
    }
    
    @OnClose
    public void onClose(Session session) {
        System.out.println("连接关闭:" + session.getId());
        sessions.remove(session);
    }
    
    @OnError
    public void onError(Session session, Throwable throwable) {
        System.out.println("发生错误:" + session.getId());
        throwable.printStackTrace();
        sessions.remove(session);
    }
}

技术要点:

  • @ServerEndpoint注解定义WebSocket端点
  • 四个主要生命周期方法:onOpenonMessageonCloseonError
  • 使用Session对象管理连接
  • 支持一对一通信和广播
  • 线程安全的会话管理
前端代码
initWebSocket() {
    console.log(this.ws === null)
    this.ws = new WebSocket('ws://localhost:8082/webSocket')
    
    this.ws.onopen = () => {
      this.$message.success('连接成功')
      // 可以在连接成功后立即发送消息
      // this.ws.send('Hello Server!')
    }
    
    this.ws.onmessage = e => {
      this.receiveMsg = e.data
      console.log('服务器返回消息', e.data)
    }
    
    this.ws.onclose = () => {
      this.$message.error('连接关闭')
    }
    
    this.ws.onerror = () => {
      this.$message.error('连接失败')
    }
    
    // 发送消息示例
    // sendMessage() {
    //   this.ws.send(this.message)
    // }
    
    // 关闭连接
    // closeConnection() {
    //   this.ws.close()
    // }
},

技术要点:

  • WebSocket API是浏览器原生支持的
  • 提供事件处理机制:onopenonmessageoncloseonerror
  • 使用send()方法发送消息
  • 可以发送文本或二进制数据
  • 需要手动关闭连接

4.3 适用场景

WebSocket特别适合:

  • 实时聊天应用
  • 多人在线游戏
  • 协作编辑工具
  • 实时监控和数据可视化
  • 需要低延迟双向通信的任何应用

五、技术对比与选择指南

技术通信方向协议自动重连复杂度浏览器兼容性适用场景
SSE/SseEmitter服务器→客户端HTTP良好实时通知、流式更新
ResponseBodyEmitter服务器→客户端HTTP优秀通用数据流
StreamingResponseBody服务器→客户端HTTP优秀大文件、精确控制
WebSocket双向WebSocket否(需自行实现)优秀实时交互、聊天应用

选择建议

  1. 只需服务器到客户端的单向通信

    • 对于简单场景,选择 SseEmitter (SSE)
    • 对于需要更多控制的场景,选择 ResponseBodyEmitter
    • 对于二进制数据或大文件,选择 StreamingResponseBody
  2. 需要客户端到服务器的通信

    • WebSocket 是唯一选择
  3. 考虑因素

    • 性能需求
    • 客户端兼容性
    • 开发复杂度
    • 网络环境(如防火墙可能阻止WebSocket)
    • 数据格式需求

六、实际应用示例

6.1 AI聊天机器人的流式响应实现

使用SSE实现类似ChatGPT的流式文本响应:

@GetMapping("/chat/stream")
public SseEmitter chatStream(@RequestParam String prompt) {
    SseEmitter emitter = new SseEmitter(-1L); // 无超时
    aiService.processPromptAsync(prompt, 
        (chunk) -> {
            try {
                emitter.send(SseEmitter.event().data(chunk));
            } catch (IOException e) {
                emitter.completeWithError(e);
            }
        },
        () -> emitter.complete()
    );
    return emitter;
}

6.2 实时数据大屏幕

使用WebSocket实现实时更新的数据可视化:

@Scheduled(fixedRate = 5000)
public void broadcastMetrics() {
    MetricsData data = metricsService.getCurrentMetrics();
    for (Session session : sessions) {
        try {
            session.getBasicRemote().sendText(objectMapper.writeValueAsString(data));
        } catch (IOException e) {
            // 处理错误
        }
    }
}

6.3 大文件下载

使用StreamingResponseBody实现大文件下载:

@GetMapping("/download/{fileId}")
public StreamingResponseBody downloadFile(@PathVariable String fileId) {
    File file = fileService.getFile(fileId);
    return outputStream -> {
        try (FileInputStream inputStream = new FileInputStream(file)) {
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
            }
        }
    };
}

七、性能优化与注意事项

7.1 通用优化建议

  1. 配置适当的超时时间

    • 例如:new SseEmitter(300000L); // 5分钟超时
  2. 使用线程池而非单个线程

    @Autowired
    private TaskExecutor taskExecutor;
    
    @GetMapping("/stream")
    public SseEmitter stream() {
        SseEmitter emitter = new SseEmitter();
        taskExecutor.execute(() -> {
            // 异步处理逻辑
        });
        return emitter;
    }
    
  3. 错误处理与资源清理

    • 总是处理异常并关闭连接
    • 使用try-with-resources确保资源释放
  4. 限制并发连接数

    • 使用信号量或限流器防止服务器过载

7.2 常见问题与解决方案

  1. 代理服务器问题

    • Nginx等代理可能缓冲响应,需配置:
      proxy_buffering off;
      proxy_read_timeout 300s;
      
  2. WebSocket连接断开

    • 实现心跳机制保持连接活跃
    • 客户端断线重连逻辑
  3. 内存泄漏

    • 确保清理未使用的Emitter对象
    • 对长连接实现超时管理
  4. 压力测试

    • 在部署前测试系统在高并发下的表现
    • 确定每种技术的性能边界

八、结论

前后端流式传输技术极大地改善了Web应用的用户体验。选择合适的技术取决于具体的应用需求、性能要求和开发复杂度。

  • SSE/SseEmitter: 简单易用的单向服务器推送
  • ResponseBodyEmitter: 灵活的HTTP流式响应
  • StreamingResponseBody: 精细控制的二进制流传输
  • WebSocket: 功能强大的双向实时通信

在实际项目中,可能需要结合使用多种技术来满足不同的业务场景需求。


项目实战示例:实时通知练习项目

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值