为啥 DeferredResult 在尝试使用 SSE 时以 setResult() 结束

Posted

技术标签:

【中文标题】为啥 DeferredResult 在尝试使用 SSE 时以 setResult() 结束【英文标题】:why DeferredResult ends on setResult() on trying to use SSE为什么 DeferredResult 在尝试使用 SSE 时以 setResult() 结束 【发布时间】:2014-11-10 21:25:33 【问题描述】:

我正在尝试实现一个由 Spring 提供支持的服务器发送事件 (SSE) 网页。我的测试代码执行以下操作:

浏览器使用 EventSource(url) 连接到服务器。 Spring 使用以下控制器代码接受请求:

@RequestMapping(value="myurl", method = RequestMethod.GET, produces = "text/event-stream")
@ResponseBody
public DeferredResult<String> subscribe() throws Exception 
    final DeferredResult<String> deferredResult = new DeferredResult<>();
    resultList.add(deferredResult);

    deferredResult.onCompletion(() -> 
        logTimer.info("deferedResult "+deferredResult+" completion");
        resultList.remove(deferredResult);
    );
    return deferredResult;

所以主要是它把 DeferredResult 放在一个列表中并注册一个完成回调,以便我可以在完成的情况下从列表中删除这个东西。

现在我有一个计时器方法,它会通过它们的 DeferredResults 定期向所有注册的“浏览器”输出当前时间戳

@Scheduled(fixedRate=10000)
public void processQueues() 
    Date d = new Date();
    log.info("outputting to "+ LoginController.resultList.size()+ " connections");
    LoginController.resultList.forEach(deferredResult -> deferredResult.setResult("data: "+d.getTime()+"\n\n"));

数据被发送到浏览器,下面的客户端代码工作:

 var source = new EventSource('/myurl');
   source.addEventListener('message', function (e) 
            console.log(e.data);
            $("#content").append(e.data).append("<br>");
        );

现在的问题:

在计时器线程中的每个 setResult() 调用上都会调用 DeferredResult 的完成回调。因此,由于某种原因,在 setResult() 调用后连接被关闭。浏览器中的 SSE 根据规范重新连接,然后再次连接。所以在客户端我有一个轮询行为,但我想要一个保持打开的请求,我可以一次又一次地在同一个 DeferredResult 上推送数据。

我在这里想念什么吗? DeferredResult 不能发送多个结果吗?我在计时器线程中延迟了 10 秒,以查看请求是否仅在 setResult() 之后终止。因此,在浏览器中,请求保持打开状态,直到计时器推送数据但随后关闭。

感谢您对此的任何提示。还有一点需要注意:我在 tomcat 中的所有过滤器/servlet 中添加了异步支持。

【问题讨论】:

【参考方案1】:

确实 DeferredResult 只能设置一次(注意 setResult 返回一个布尔值)。它使用所有 Spring MVC 处理选项来完成处理,也就是说,除了异步生成的返回值之外,您所知道的关于 Spring MVC 请求期间发生的事情的所有信息都大致相同。

SSE 需要更集中的东西,即使用 HttpMessageConverter 将每个值写入响应。我已经为 https://jira.spring.io/browse/SPR-12212 创建了一张票。

请注意,Spring 的 SockJS 支持确实有一个 SSE 传输,它处理一些额外的问题,例如带有 cookie 的跨域请求(对于 IE 很重要)。它还用于 WebSocket API 和 WebSocket 样式的消息传递(即使 WebSocket 在客户端或服务器端都不可用),这完全抽象了 HTTP 长轮询的细节。

作为一种解决方法,您还可以使用 HttpMessageConverter 直接写入 Servlet 响应。

【讨论】:

感谢罗森的澄清。为了真正确定那不是我,我编写了一个普通的异步 servlet 并一遍又一遍地将数据刷新到 responseStream 并且有效。当然,我避免了实际上完成请求的 AsyncContext.complete() 调用。我认为这就是我试图理解的代码丛林中某个地方的 DeferredResult 中的 setResult() 之后 spring mvc 所做的。我想避免使用 Websocket,因为无论如何我只需要单向通信,但 SSE 在服务器端并没有那么简单;-) 其实 DeferredResult 并不是简单的调用 AsyncContext.complete。最初尝试过,但在 servlet 容器外访问 HttpServletRequest 并不安全,因此该方法不适合完全透明的 Spring MVC 处理。相反,我们调用 asyncContext.dispatch() ,这就像一个转发到容器中,然后我们照常完成处理。 啊好的。事实上,我还没有看到 Spring MVC 代码中的 complete() 调用,只是猜测它可能是这种方式。反正。在那个 MVC 领域做得很好。我早在 1 光年前就开始使用 Struts,所以我真的知道 Spring MVC 的好处 ;-)

以上是关于为啥 DeferredResult 在尝试使用 SSE 时以 setResult() 结束的主要内容,如果未能解决你的问题,请参考以下文章

DeferredResult使用方式和场景

DeferredResult 如何实现长轮询?

Spring DeferredResult 异步请求

DeferredResult 实现长轮询

如何测试 DeferredResult timeoutResult

使用DeferredResult实现异步处理REST服务示例