Java服务器OutboundSseEvent/JAX-RS SSE无法发送消息事件排查
我帮你排查了下,核心问题主要出在服务器发送的事件名称与客户端监听的事件不匹配,再加上服务器端的一些实现细节问题,导致后续事件无法被客户端正确接收:
事件名称不匹配
你的服务器代码里,每个事件的名称被设置成了数字(name("" + lastEventId)),也就是事件类型是0、1、2...这类字符串。但客户端代码里只监听了open、message、stringEvent这几种事件类型,完全没有对应服务器发送的数字名称事件。只有当服务器发送不带名称的事件时,才会触发客户端的onmessage和message监听;带名称的事件必须客户端显式监听对应的名称才能收到。服务器端阻塞请求线程+未正确处理连接生命周期
你的服务器方法是同步阻塞的,在请求处理线程里执行10次循环+睡眠,这不仅会占用容器的请求线程资源,还可能导致响应输出流没有及时flush(部分JAX-RS实现需要手动确保事件被推送)。另外,循环结束后没有关闭SseEventSink,会导致连接一直处于挂起状态,但客户端也不会收到任何事件。媒体类型不匹配
服务器设置了mediaType(MediaType.APPLICATION_JSON_TYPE),但实际发送的是普通字符串"hello world",这会导致响应的Content-Type头是application/json,但内容并不是合法JSON,可能会被客户端的polyfill或者浏览器忽略。
针对上面的问题,给你几个具体的修复步骤:
1. 统一事件名称,确保客户端能监听对应事件
两种方案选其一:
- 方案一:服务器发送无名称的默认事件
去掉服务器代码里的.name("" + lastEventId),这样事件会被归类为默认的message类型,客户端的onmessage和message监听就能触发:OutboundSseEvent stringEvent = sse.newEventBuilder() // .name("" + lastEventId) // 移除这一行 .mediaType(MediaType.TEXT_PLAIN_TYPE) .data("hello world") .id(String.valueOf(lastEventId)) .reconnectDelay(4000) .build(); - 方案二:服务器使用固定事件名称,客户端对应监听
比如服务器统一用stringEvent作为事件名称,和客户端已有的监听对应:
这样客户端的OutboundSseEvent stringEvent = sse.newEventBuilder() .name("stringEvent") // 改成固定名称 .mediaType(MediaType.TEXT_PLAIN_TYPE) .data("hello world") .id(String.valueOf(lastEventId)) .reconnectDelay(4000) .build();evtSource.addEventListener("stringEvent", ...)就能正确收到事件了。
2. 服务器端改用异步线程发送事件,避免阻塞请求线程
把循环发送事件的逻辑放到异步线程里,释放容器的请求线程,同时确保事件能及时推送:
@GET @Path("/test") @Produces("text/event-stream") public void runTest(@Context Sse sse, @Context SseEventSink sseEventSink) { // 用异步线程执行发送逻辑 CompletableFuture.runAsync(() -> { int lastEventId = 0; try { while (lastEventId < 10) { OutboundSseEvent stringEvent = sse.newEventBuilder() .name("stringEvent") .mediaType(MediaType.TEXT_PLAIN_TYPE) .data("hello world") .id(String.valueOf(lastEventId)) .reconnectDelay(4000) .build(); // 检查sink是否还处于打开状态 if (sseEventSink.isClosed()) { break; } sseEventSink.send(stringEvent); lastEventId++; TimeUnit.SECONDS.sleep(1); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 循环结束后关闭sink,释放连接 sseEventSink.close(); } }); }
3. 修正媒体类型,确保内容与类型匹配
如果发送的是普通字符串,就用MediaType.TEXT_PLAIN_TYPE;如果要发送JSON数据,就把data改成合法的JSON字符串(比如"{\"content\":\"hello world\"}"),同时保留APPLICATION_JSON_TYPE。
4. 添加必要的响应头,防止浏览器缓存
在服务器方法里添加Cache-Control: no-cache和Connection: keep-alive头,避免浏览器缓存SSE响应:
@GET @Path("/test") @Produces("text/event-stream") public void runTest(@Context Sse sse, @Context SseEventSink sseEventSink, @Context HttpServletResponse response) { response.setHeader("Cache-Control", "no-cache"); response.setHeader("Connection", "keep-alive"); // 后面的异步发送逻辑... }
内容的提问来源于stack exchange,提问作者GNG

