背景
近期有這么一個需求:
手機端需要展示一個比較大的pdf
基於手機端網絡/流量/體驗等考慮,希望不通過pdf下載然后展示
而是把pdf轉成一張張的圖片,然后再在手機上展示。
分析
pdf轉圖片,肯定是一個比較慢的過程,最好能轉完一張就返回一張到前端。
So,此文要講的是 請求異步多次返回的技術實現SSE
當然,WebSocket也能做到,它可以雙向通信,比SSE(單向發送)強大且復雜,SSE好在比較簡單
服務器端事件發送 SSE
全稱:Server Send Event
其實嚴格地說,HTTP 協議無法做到服務器主動推送數據到客戶端的。只不過可以變通一下,就是服務器向客戶端聲明,接下來要發送的是流數據(stream)。
此時,客戶端不會關閉連接,會一直等着服務器發過來的新的數據流。
SSE 就是利用這種機制,使用流信息向瀏覽器推送信息。它基於 HTTP 協議,目前除了 IE,其他瀏覽器都支持。
IE的話,也可以通過evensource.js來兼容起來。
代碼實現
客戶端
需要用到EventSource,並實現onmessage方法
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-stream
@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
做了如下修改:
1、原文示例代碼中,每個請求只返回了一次數據,服務器每次發完數據斷開了連接。
但SSE默認會自動重連,所以客戶端不斷地重連(重新發請求)。瀏覽器F12 network,可以看到刷了很多請求
這和ajax長輪詢沒什么區別了。
2、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-async
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
