Jakarta EE如何检测SSE客户端断开并及时清理服务端资源
SSE失效连接清理方案(适配OpenLiberty + RESTEasy环境)
核心问题
在Jakarta EE服务端对接Web端、Java客户端搭建SSE通信通道时,通常会在内存中维护已连接客户端的SseEventSink实例列表实现主动推送,但默认配置下缺少可靠的客户端断连检测机制,会导致失效连接长期占用服务端资源。
当前典型实现代码如下:
@ApplicationScoped @Path("/sse") public class SseController { private Sse sse; private List<SseEventSink> sinks = new CopyOnWriteArrayList<>(); @GET @Produces(SERVER_SENT_EVENTS) public void onSse(@Context Sse sse, @Context SseEventSink sink) { this.sse = sse; this.sinks.add(sink); } }
默认实现存在的已知问题:
- 客户端主动断开、网络闪断、进程崩溃后,服务端最长15分钟无法感知断连
- 仅当向失效连接连续发送3次消息触发发送异常时才会清理资源,实时性差、无效资源占用高
可落地方案
1. 注册连接关闭回调,实现断连即时清理
RESTEasy作为OpenLiberty的SSE实现组件,其SseEventSink实现类原生支持关闭事件回调,不需要等待发送失败即可感知连接关闭:无论是客户端主动断开、网络中断还是连接超时,回调都会第一时间触发,直接在回调中移除对应失效sink即可。
修改连接建立逻辑:
@GET @Produces(SERVER_SENT_EVENTS) public void onSse(@Context Sse sse, @Context SseEventSink sink) { sinks.add(sink); // 连接建立后发送首条注释帧确认连接状态,同时注册关闭回调 sink.send(sse.newEventBuilder().comment("connected").build()) .whenComplete((result, ex) -> { // 仅对RESTEasy实现的sink注册关闭监听,OpenLiberty全量支持版本均适配 if (sink instanceof ResteasySseEventSink resteasySink) { resteasySink.onClose(() -> sinks.remove(sink)); } }); }
注意:原代码中将请求上下文注入的
Sse实例存储为类变量存在线程安全问题,@ApplicationScoped生命周期的Bean会被多请求并发访问,类变量会被反复覆盖,建议每次推送时通过注入获取Sse实例,或在连接建立时为每个sink单独绑定事件构造器。
2. 推送时增加状态校验,做兜底清理
关闭回调不能覆盖100%的异常场景(比如服务端重启、回调注册失败),需要在每次批量推送消息时增加二次校验,彻底避免资源泄漏:
public void broadcast(String messageContent) { OutboundSseEvent pushEvent = sse.newEvent("message", messageContent); Iterator<SseEventSink> sinkIterator = sinks.iterator(); while (sinkIterator.hasNext()) { SseEventSink currentSink = sinkIterator.next(); // 直接跳过已标记关闭的连接 if (currentSink.isClosed()) { sinkIterator.remove(); continue; } // 捕获发送过程中的异常,移除发送失败的失效连接 try { currentSink.send(pushEvent); } catch (Exception e) { sinkIterator.remove(); } } }
3. 调整服务端超时配置,清理僵死连接
OpenLiberty默认的HTTP连接超时配置对SSE长连接适配较差,需要在server.xml中调整SSE专属的空闲超时参数,强制无响应的僵死连接触发关闭回调:
<httpEndpoint httpPort="9080" httpsPort="9443" id="defaultHttpEndpoint"> <!-- TCP层读超时设置为5分钟 --> <tcpOptions soTimeout="300000" /> <!-- SSE连接空闲超时设置为5分钟,无数据传输自动关闭连接 --> <sse idleTimeout="300000" /> </httpEndpoint>
如果业务对实时性要求高,可以把超时时间调整到1-2分钟,配合定期发送心跳注释帧(比如每30秒发一条:心跳帧),可以把断连感知延迟控制在1分钟以内。
关键注意事项
- 遍历
CopyOnWriteArrayList删除元素时必须使用迭代器的remove方法,禁止在for-each循环中直接删除,避免抛出并发修改异常 - 不要依赖客户端侧发送的断开请求:网络闪断、客户端强杀进程的场景下,服务端无法收到应用层的断开通知,必须依赖TCP层断连感知+超时机制兜底
- 心跳帧不要携带业务数据,用SSE规范规定的注释帧(以
:开头)即可,不会触发客户端的业务消息回调
内容的提问来源于stack exchange,提问作者DanielM
相关产品推荐
相关产品推荐

