近期有這麼一個需求: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