近期有这么一个需求:html
手机端须要展现一个比较大的pdf 基于手机端网络/流量/体验等考虑,但愿不经过pdf下载而后展现 而是把pdf转成一张张的图片,而后再在手机上展现。
pdf转图片,确定是一个比较慢的过程,最好能转完一张就返回一张到前端。
So,此文要讲的是 请求异步屡次返回的技术实现SSE
固然,WebSocket也能作到,它能够双向通讯,比SSE(单向发送)强大且复杂,SSE好在比较简单前端
全称:Server Send Event
其实严格地说,HTTP 协议没法作到服务器主动推送数据到客户端的。只不过能够变通一下,就是服务器向客户端声明,接下来要发送的是流数据(stream)。
此时,客户端不会关闭链接,会一直等着服务器发过来的新的数据流。
SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE,其余浏览器都支持。
IE的话,也能够经过evensource.js来兼容起来。git
须要用到EventSource,并实现onmessage方法github
if (!!window.EventSource) { var source = new EventSource('push'); s = ''; source.addEventListener('message', function(e) { s += e.data + "<br/>"; $("#msgFrompPush").html(s); }); source.addEventListener('open', function(e) { console.log("链接打开."); }, false); source.addEventListener('error', function(e) { if (e.readyState == EventSource.CLOSED) { console.log("链接关闭"); } else { console.log(e); source.close(); } }, false); } else { console.log("你的浏览器不支持SSE"); }
须要设置类型为event-streamajax
@RequestMapping(value = "/pushV2", produces = "text/event-stream") public void pushV2(HttpServletResponse response) { response.setContentType("text/event-stream"); response.setCharacterEncoding("utf-8"); int count = 0; while (true) { Random r = new Random(); try { Thread.sleep(1000); PrintWriter pw = response.getWriter(); // 若是浏览器直接关闭,须要check一下 if (pw.checkError()) { System.out.println("客户端主动断开链接"); return; } pw.write("data:Testing 1,2,3" + r.nextInt() + "\n\n"); pw.flush(); count++; if(count>5){ return; } } catch (Exception e) { e.printStackTrace(); } } }
以上客户端和服务端的代码示例基于http://blog.longjiazuo.com/archives/1489
作了以下修改:spring
一、原文示例代码中,每一个请求只返回了一次数据,服务器每次发完数据断开了链接。 但SSE默认会自动重连,因此客户端不断地重连(从新发请求)。浏览器F12 network,能够看到刷了不少请求 这和ajax长轮询没什么区别了。 二、Controller端处理完return返回以后,前端页面会收到一个error事件。浏览器接收到error事件后,SSE又会自动重连,因此我加了一个source.close(); 固然这里close不合理,后面再聊合理的作法
这里须要知道的是:return以后长链接就断开了,就不是咱们想要的持续推送了。
修改后的代码见Github:
https://github.com/yejg/springMvc4.x-project/tree/master/springMvc4.x-serverSendEvent浏览器
SpringMvc已经对这种异步响应作了很好的封装,咱们能够直接返回Callable、DeferredResult或SseEmitter 来更优雅地实现咱们的需求。服务器
返回Callable的时候,Spring作了这些事情网络
DeferredResult的处理逻辑和Callable返回差很少,只不过DeferredResult的线程不禁SpringMvc管理。
参考资料: https://docs.spring.io/spring/docs/4.3.16.RELEASE/spring-framework-reference/html/mvc.html#mvc-ann-asyncmvc
Callable和DeferredResult通常用于异步返回单个结果;
SseEmitter则能够异步屡次返回。
在使用SseEmitter写代码前,再解决如下前面提到的一个小问题 -- 合理地close掉EventSource。
前面的代码里面,为了不Controller中return后,浏览器重连,咱们直接在error里面把source给close掉了。 source.addEventListener('error', function(e) { if (e.readyState == EventSource.CLOSED) { console.log("链接关闭"); } else { console.log(e); source.close(); // <--- 就是这里 } }, false); SseEmitter有complete()方法,不过执行以后,浏览器也是会收到error事件,并从新请求连接; 那么,最好的作法是: Controller处理返回完以后,通知请求端浏览器,告诉它数据都传完了,由浏览器端主动去close掉EventSource。
通过上面一系列的分析,能够开始愉快地写代码了:
返回一个自定义的event,type为finish,告知浏览器能够关闭链接了。
@RequestMapping("/sseEmitter") @ResponseBody public SseEmitter sseEmitterCall() { // SseEmitter用于异步返回多个结果,直到调用sseEmitter.complete()结束返回 SseEmitter sseEmitter = new SseEmitter(); Thread t = new Thread(new TestRun(sseEmitter)); t.start(); return sseEmitter; } class TestRun implements Runnable { private SseEmitter sseEmitter; private int times = 0; public TestRun(SseEmitter sseEmitter) { this.sseEmitter = sseEmitter; } @Override public void run() { while (true) { try { System.out.println("当前times=" + times); sseEmitter.send(System.currentTimeMillis()); times++; Thread.sleep(1000); if (times > 4) { System.out.println("发送finish事件"); sseEmitter.send(SseEmitter.event().name("finish").id("6666").data("哈哈")); System.out.println("调用complete"); sseEmitter.complete(); System.out.println("complete!times=" + times); break; } } catch (IOException | InterruptedException e) { e.printStackTrace(); } } } }
增长处理finish事件的响应代码
if (!!window.EventSource) { var source = new EventSource('sseEmitter'); s=''; source.addEventListener('message', function(e) { s+=e.data+"<br/>"; $("#msgFrompPush").html(s); }); source.addEventListener('open', function(e) { console.log("链接打开."); }, false); // 响应finish事件,主动关闭EventSource source.addEventListener('finish', function(e) { console.log("数据接收完毕,关闭EventSource"); source.close(); console.log(e); }, false); source.addEventListener('error', function(e) { if (e.readyState == EventSource.CLOSED) { console.log("链接关闭"); } else { console.log(e); } }, false); } else { console.log("你的浏览器不支持SSE"); }
完整代码见:
https://github.com/yejg/springMvc4.x-project/tree/master/springMvc4.x-servlet3/src/main
推荐阅读:
Server-Sent Events 教程 http://www.ruanyifeng.com/blog/2017/05/server-sent_events.html