服務器端事件發送SSE

背景

近期有這麼一個需求:html

手機端須要展現一個比較大的pdf
基於手機端網絡/流量/體驗等考慮,但願不經過pdf下載而後展現
而是把pdf轉成一張張的圖片,而後再在手機上展現。

分析

pdf轉圖片,確定是一個比較慢的過程,最好能轉完一張就返回一張到前端。
So,此文要講的是 請求異步屢次返回的技術實現SSE
固然,WebSocket也能作到,它能夠雙向通訊,比SSE(單向發送)強大且複雜,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實現

SpringMvc已經對這種異步響應作了很好的封裝,咱們能夠直接返回Callable、DeferredResult或SseEmitter 來更優雅地實現咱們的需求。服務器

返回Callable的時候,Spring作了這些事情網絡

  • Controller返回一個Callable對象
  • Spring MVC開始異步處理而且提交Callable到TaskExecutor在一個單獨的線程中進行處理
  • DispatcherServlet與全部的Filter的Servlet容器線程退出,但Response仍然開放
  • Callable產生結果而且Spring MVC分發請求給Servlet容器繼續處理
  • DispatcherServlet再次被調用而且繼續異步的處理由Callable產生的結果

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

相關文章
相關標籤/搜索