如何在Spring Boot中通过REST调用实现SSH脚本输出的实时流式传输
解决Spring Boot实时流式传输SSH脚本输出的问题
我来帮你梳理下问题的核心和解决方案:你当前的实现之所以会等待8秒才一次性返回所有内容,主要是因为commandWithContinousRead是阻塞方法,且你没有正确处理流式输出的刷新逻辑,另外StreamingResponseBody的使用方式也需要调整。下面是具体的优化方案和测试方法:
推荐方案:使用SSEEmitter实现实时推送
Server-Sent Events(SSE)其实非常适合你的场景——它就是为服务器主动向客户端流式推送实时数据设计的,而且Spring的SseEmitter能帮你自动处理SSE的格式规范(比如每个事件的data: ...\n\n结构),不用手动拼接。
1. 修改Spring接口代码
首先我们改用SseEmitter,并把SSH的阻塞调用放到异步线程池中,避免阻塞请求线程:
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.*; // 先配置一个异步线程池,专门处理SSH的阻塞任务 @Configuration public class AsyncTaskConfig { @Bean(name = "sshTaskExecutor") public ThreadPoolTaskExecutor sshTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(5); executor.setQueueCapacity(10); executor.setThreadNamePrefix("SSH-Worker-"); executor.initialize(); return executor; } } // 你的Controller类 @RestController public class StreamController { private final SSHManager sshManager; private final ThreadPoolTaskExecutor sshTaskExecutor; // 构造注入 public StreamController(SSHManager sshManager, ThreadPoolTaskExecutor sshTaskExecutor) { this.sshManager = sshManager; this.sshTaskExecutor = sshTaskExecutor; } @GetMapping("/stream-this") public SseEmitter streamScript() { // 设置超时时间,根据你的脚本时长调整(这里设30秒) SseEmitter emitter = new SseEmitter(30000L); // 把SSH任务放到异步线程池执行,避免阻塞请求线程 sshTaskExecutor.submit(() -> { try { sshManager.commandWithContinousRead("bash /scripts/stream-this.sh", echo -> { try { // 发送SSE事件,自动处理格式 emitter.send(SseEmitter.event().data(echo)); System.out.println("已推送内容:" + echo); } catch (IOException e) { // 客户端断开连接时,结束推送 emitter.completeWithError(e); } }); // 脚本执行完成,关闭Emitter emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; } }
2. 为什么这个方案能生效?
SseEmitter会保持HTTP连接打开,每次调用send()都会立刻把数据推送给客户端,不需要等待所有内容生成。- 异步线程池确保SSH的阻塞调用不会占用Spring的请求线程,避免影响其他接口的响应。
- 自动处理SSE的格式规范,客户端可以直接用标准的
EventSourceAPI接收数据。
备选方案:修复StreamingResponseBody的实现
如果你坚持使用StreamingResponseBody,需要手动刷新输出流,确保数据实时发送:
@RequestMapping(value = "/stream-this", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity<StreamingResponseBody> streamScript() { StreamingResponseBody stream = out -> { try (OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); BufferedWriter bufferedWriter = new BufferedWriter(writer)) { sshManager.commandWithContinousRead("bash /scripts/stream-this.sh", echo -> { try { bufferedWriter.write(echo); bufferedWriter.newLine(); bufferedWriter.flush(); // 关键:每次写入后强制刷新,立刻发送数据 System.out.println(echo); } catch (IOException e) { e.printStackTrace(); } }); } catch (IOException e) { e.printStackTrace(); } }; HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CACHE_CONTROL, "no-cache"); headers.add(HttpHeaders.CONNECTION, "keep-alive"); return new ResponseEntity<>(stream, headers, HttpStatus.OK); }
这个方案是普通的文本流,没有SSE的事件格式,客户端需要自己处理换行分隔的内容。
如何验证流式传输是否正常?
1. Postman测试
Postman完全支持SSE:
- 发送GET请求到
http://localhost:8080/stream-this - 切换到Events标签页(默认会自动识别SSE响应)
- 你会看到每4秒刷新一条新的事件内容,而不是等待8秒一次性显示全部。
2. curl测试(最直接)
在终端执行:
curl -N http://localhost:8080/stream-this
-N参数禁用curl的缓冲,你会实时看到每4秒输出一行内容。
3. 前端页面测试
写一个简单的HTML页面,用标准的EventSource接收SSE:
<!DOCTYPE html> <html> <body> <div id="output"></div> <script> const source = new EventSource('http://localhost:8080/stream-this'); source.onmessage = function(event) { const output = document.getElementById('output'); output.innerHTML += event.data + '<br>'; }; source.onerror = function(error) { console.error('SSE连接出错:', error); source.close(); }; </script> </body> </html>
打开这个页面,就能看到内容实时刷新显示。
内容的提问来源于stack exchange,提问作者treeden
相关产品推荐
相关产品推荐

