用 SseEmitter 推送要注意什么?为什么我在 StreamingResponseBody 里 flush 了客户端还是收不到?客户端断开后服务端什么时候知道?超时多准?压缩会不会让流式推送变成最后一起到?这篇在 Spring Boot 4.0.8 上用逐行计时的客户端实测,并读了 spring-web 7.0.9 的源码确认 flush 为什么无效。
客户端看到的(行 = 一行文本,空行是事件分隔):
响应头到达 49 200 {'Content-Type': 'text/event-stream', 'Transfer-Encoding': 'chunked'}
49 ms id:1
49 ms event:greet
49 ms data:hello
49 ms data:world
49 ms retry:3000
49 ms
353 ms :heartbeat
353 ms
701 ms id:2
701 ms data:{"k":"v"}
701 ms
1022 ms (流结束)
服务端事件:
40 ms 控制器方法在 http-nio-9601-exec-2 返回 SseEmitter;Last-Event-ID=41
43 ms 已发送事件 1(线程 producer)
353 ms 已发送注释
701 ms 已发送事件 2(对象 → JSON)
1012 ms complete()线格式很简单:id、event、data、retry 和以冒号开头的注释;事件逐个到达。
对照:Controller 里直接写 HttpServletResponse 并 flush:
响应头到达 36 200 {'Content-Type': 'text/plain', 'Transfer-Encoding': 'chunked'}
36 ms chunk-1
339 ms chunk-2
650 ms chunk-3
954 ms (流结束)
(服务端) 36 ms raw 写出 chunk-1 并 flush
(服务端) 339 ms raw 写出 chunk-2 并 flush
(服务端) 649 ms raw 写出 chunk-3 并 flush
直接返回 StreamingResponseBody(不包 ResponseEntity),每块 flush:
响应头到达 928 200 {'Transfer-Encoding': 'chunked'}
928 ms chunk-1
928 ms chunk-2
928 ms chunk-3
932 ms (流结束)
(服务端) 6 ms direct 写出 chunk-1 并 flush
(服务端) 316 ms direct 写出 chunk-2 并 flush
(服务端) 627 ms direct 写出 chunk-3 并 flush
ResponseEntity<StreamingResponseBody>,每块 20 KB 并 flush:
响应头到达 6 200 {'Content-Type': 'text/plain', 'Transfer-Encoding': 'chunked'}
6 ms chunk-1
310 ms xxxxxxxxxxxxxxxxxxxx…(共 20000 个字符)
311 ms chunk-2
618 ms xxxxxxxxxxxxxxxxxxxx…(共 20000 个字符)
618 ms chunk-3
927 ms xxxxxxxxxxxxxxxxxxxx…(共 20000 个字符)
931 ms (流结束)
(服务端) 6 ms big 写出 chunk-1(20 KB)并 flush
(服务端) 310 ms big 写出 chunk-2(20 KB)并 flush
(服务端) 618 ms big 写出 chunk-3(20 KB)并 flush
ResponseEntity<StreamingResponseBody>,每块 flush:
响应头到达 924 200 {'Content-Type': 'text/plain', 'Transfer-Encoding': 'chunked'}
925 ms chunk-1
925 ms chunk-2
925 ms chunk-3
927 ms (流结束)
(服务端) 5 ms 控制器方法在 http-nio-9602-exec-10
(服务端) 7 ms StreamingResponseBody 在 task-3 开始写
(服务端) 7 ms 写出 chunk-1 并 flush
(服务端) 310 ms 写出 chunk-2 并 flush
(服务端) 612 ms 写出 chunk-3 并 flush
StreamingResponseBody,不 flush:
响应头到达 931 200 {'Content-Type': 'text/plain', 'Transfer-Encoding': 'chunked'}
931 ms chunk-1
931 ms chunk-2
931 ms chunk-3
931 ms (流结束)
(服务端) 7 ms 写出 chunk-1(没有 flush)
(服务端) 317 ms 写出 chunk-2(没有 flush)
(服务端) 626 ms 写出 chunk-3(没有 flush)
对照:普通 String 返回(处理 900ms 后一次性返回):
响应头到达 927 200 {'Content-Type': 'text/plain;charset=UTF-8', 'Content-Length': '24'}
927 ms chunk-1
927 ms chunk-2
927 ms chunk-3
927 ms (流结束)流的 flush() 被包装成什么都不做,小块数据攒到结束才发;要逐块实时发,用 SseEmitter。
客户端:
响应头到达 55 200 {'Content-Type': 'text/event-stream', 'Transfer-Encoding': 'chunked'}
55 ms id:1
55 ms data:tick-1
55 ms
252 ms id:2
252 ms data:tick-2
252 ms (客户端读到第 2 个数据后主动关闭连接)
服务端(客户端关闭连接后继续观察):
49 ms 发送 tick-1 成功
253 ms 发送 tick-2 成功
464 ms 发送 tick-3 成功
674 ms 发送 tick-4 失败:AsyncRequestNotUsableException
674 ms onError 回调:AsyncRequestNotUsableException(线程 http-nio-9603-exec-3)
680 ms onCompletion 回调(线程 http-nio-9603-exec-3)服务端靠写失败发现客户端断开,不写就不知道。
客户端:
响应头到达 40 200 {'Content-Type': 'text/event-stream', 'Transfer-Encoding': 'chunked'}
40 ms data:only-one
40 ms
1986 ms (流结束)
服务端:
1975 ms onTimeout 回调(线程 http-nio-9604-exec-2)
1986 ms onCompletion 回调亚秒级超时不精确。
不带 Accept-Encoding:
响应头到达 44 ms,Content-Encoding=None
44 ms 收到 51 字节
345 ms 收到 12 字节
689 ms 收到 21 字节
1011 ms 流结束
Accept-Encoding: gzip:
响应头到达 5 ms,Content-Encoding=gzip
5 ms 收到 10 字节
5 ms 收到 53 字节
313 ms 收到 17 字节
624 ms 收到 23 字节
935 ms 收到 10 字节
935 ms 流结束开压缩后事件仍逐块到达,但要注意测量方式。