問題場景
?在開發(fā)web項(xiàng)目時(shí),有一個(gè)需求是:后端服務(wù)器要主動(dòng)地、不斷地推送消息給客戶端網(wǎng)頁。要實(shí)現(xiàn)該需求,需要先考慮幾個(gè)常用的技術(shù)方案:
- 在客戶端網(wǎng)頁用fetch、XmlHttpRequest發(fā)送請(qǐng)求是行不通的,因?yàn)檫@類請(qǐng)求在后端返回一次數(shù)據(jù)之后就會(huì)中斷連接,導(dǎo)致后端無法主動(dòng)地傳數(shù)據(jù)給客戶端。
- 客戶端網(wǎng)頁使用輪詢或者長輪詢的資源效率低,會(huì)花很多時(shí)間在不斷地建立網(wǎng)絡(luò)連接、不斷地?cái)嚅_網(wǎng)絡(luò)連接。(不熟悉輪詢和長輪詢的朋友可以自己百度一下,有很多博文)。
- 客戶端和后端一同使用WebSocket的話,雖然只需要建立一次網(wǎng)絡(luò)連接就能讓客戶端和后端都主動(dòng)地、不斷地向?qū)Ψ桨l(fā)送消息,但該項(xiàng)目只需要后端向客戶端主動(dòng)地、不斷地發(fā)送消息,且WebSocket使用起來相對(duì)復(fù)雜,因此沒必要用它。
- 客戶端和后端一同使用Server side event (SSE)的話,能在建立一次網(wǎng)絡(luò)連接后,讓后端向客戶端主動(dòng)地、不斷地發(fā)送消息,并且使用起來也非常簡單,是實(shí)現(xiàn)項(xiàng)目功能的首選方案。
Server side event (SSE)簡介
?SSE定義了客戶端網(wǎng)頁和后端服務(wù)器的網(wǎng)絡(luò)連接方式,具體包括:
- 先由客戶端發(fā)送一次http請(qǐng)求給后端(注意,第一次http請(qǐng)求的發(fā)起者永遠(yuǎn)是客戶端而不是后端)。
- 后端接收請(qǐng)求并建立網(wǎng)絡(luò)連接,該網(wǎng)絡(luò)連接將長久存在,可以一直被使用,直到下述第4點(diǎn)發(fā)生。
- 在網(wǎng)絡(luò)連接后,后端不會(huì)立馬發(fā)送數(shù)據(jù)給客戶端,而是在后端運(yùn)行到了自定義的條件時(shí)(如數(shù)據(jù)庫里更新了數(shù)據(jù)),再將數(shù)據(jù)通過網(wǎng)絡(luò)連接發(fā)送給客戶端。接著后端會(huì)繼續(xù)等待這個(gè)自定義的條件的發(fā)生。因此,后端能夠主動(dòng)地、不斷地向客戶端發(fā)送數(shù)據(jù)。
- 有4種讓網(wǎng)絡(luò)連接斷開的情況:
①網(wǎng)絡(luò)問題(如客戶端斷網(wǎng)),瀏覽器監(jiān)測(cè)到網(wǎng)絡(luò)問題后,每隔一段時(shí)間就會(huì)向后端發(fā)起重連接請(qǐng)求;后端是察覺不到客戶端已經(jīng)斷網(wǎng)了的,只有在后端向客戶端發(fā)送數(shù)據(jù),但收到了發(fā)送錯(cuò)誤的信息后,才會(huì)在后端也斷開連接。
②網(wǎng)絡(luò)連接的時(shí)長超出了后端設(shè)置的超時(shí)時(shí)間,此時(shí)后端會(huì)斷開網(wǎng)絡(luò)連接,并且通知客戶端也要斷開,但這之后客戶端仍然會(huì)不斷地發(fā)起重連接請(qǐng)求,以便繼續(xù)傳輸數(shù)據(jù)。
③后端主動(dòng)地調(diào)用SSE的斷開函數(shù),此時(shí)客戶端和后端的網(wǎng)絡(luò)連接就會(huì)斷開,客戶端仍然會(huì)不斷重連。
④客戶端主動(dòng)地調(diào)用SSE的斷開函數(shù),此時(shí)客戶端和后端的網(wǎng)絡(luò)連接就會(huì)斷開,但是客戶端不會(huì)再重連。
一旦客戶端重連成功,情況又變到了上面的第2點(diǎn)的位置。
在后端使用SSE
?個(gè)人使用的后端框架是Springboot,該框架自帶實(shí)現(xiàn)了SSE的類,我們只需要如下4步即可讓后端能實(shí)現(xiàn)SSE功能。
- 在項(xiàng)目的pom.xml文件中導(dǎo)入Springboot的web依賴,從而導(dǎo)入實(shí)現(xiàn)了SSE功能的類。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 在項(xiàng)目的Controller類里定義返回
SseEmitter
對(duì)象的函數(shù),來讓Springboot自動(dòng)建立客戶端和后端的SSE連接。
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
SseEmitter emitter; // 聲明SseEmitter類型的變量
@GetMapping("/test/sse")
@ResponseBody // @ResponseBody讓函數(shù)返回的對(duì)象不被解析成前端文件的路徑
public SseEmitter handle(){
emitter=new SseEmitter();
return emitter;
}
只要客戶端訪問了/test/sse
這個(gè)路徑,Springboot會(huì)根據(jù)返回的SseEmitter
對(duì)象,來自動(dòng)建立起客戶端和后端的SSE連接,不需要我們?cè)賮碜鼋⒕W(wǎng)絡(luò)連接的事。
- 在項(xiàng)目的Controller類里定義讓SseEmitter發(fā)送數(shù)據(jù)的函數(shù)。
public void send() throws IOException{
emitter.send(
SseEmitter
.event()
.data("<Your Data>")
.id("<Your Id>")
.reconnectTime(<Your Time>)
);
其中SseEmitter.event().data("<Your Data>").id("<Your Id>").reconnectTime()
得到的類型是SseEventBuilder
類,用來定義要發(fā)送的數(shù)據(jù)。然后調(diào)用SseEmitter
的send(SseEventBuilder)
方法,SseEmitter
便會(huì)自動(dòng)地把定義好的數(shù)據(jù)發(fā)送給客戶端。說一句,SseEmitter
發(fā)送數(shù)據(jù)不一定要靠send(SseEventBuilder)
方法,只是個(gè)人覺得這樣比較方便且功能完備,至于SseEmitter
還有哪些發(fā)送數(shù)據(jù)的方法,直接看官方的SseEmitter文檔吧。
?接下來說一下SSE發(fā)送的數(shù)據(jù)包含哪些東西。就拿上述SseEmitter.event().data("<Your Data>").id("<Your Id>").reconnectTime()
定義的數(shù)據(jù)來說:
-
SseEmitter.event()
用來得到一個(gè)記錄數(shù)據(jù)的容器(該容器使用建造者模式添加數(shù)據(jù)),該方法不帶任何參數(shù)。 -
.data("<Your Data>")
用"<Your Data>"
來添加傳輸給客戶端的數(shù)據(jù),參數(shù)是Object類型,但最好以字符串為參數(shù),這樣就不需要擔(dān)心SseEmitter
會(huì)怎樣處理Object了(如無需擔(dān)心SseEmitter
怎么自動(dòng)處理Map)。如果是想返回JSON數(shù)據(jù),可先將JSON數(shù)據(jù)轉(zhuǎn)換為字符串;如果是想返回二進(jìn)制數(shù)據(jù),可以用base64的編碼方式,每6個(gè)比特用一個(gè)字符來代替,再由客戶端將字符解碼為比特。 -
.id("<Your Id>")
用"<Your Id>"
來作為這條數(shù)據(jù)的id。每當(dāng)客戶端要重連時(shí)(由于斷網(wǎng)、或者網(wǎng)絡(luò)連接時(shí)長達(dá)到了后端設(shè)置的超時(shí)時(shí)間等),客戶端就會(huì)將最后收到的那條數(shù)據(jù)的id,連同重連接請(qǐng)求一并發(fā)給后端,從而讓后端知道客戶端已經(jīng)接收了哪些數(shù)據(jù)。后端可以在HttpServletRequest
的請(qǐng)求頭中拿到這個(gè)id,至于拿到id之后怎么處理,就需要后端自定義函數(shù)了。舉兩個(gè)例子,一個(gè)例子是后端拿到id后不做任何處理的,另一個(gè)例子是后端拿到id后,對(duì)比自己已產(chǎn)生的id,來推斷出客戶端漏接收了哪些數(shù)據(jù),然后將客戶端漏收的數(shù)據(jù)補(bǔ)發(fā)出去。
// 例子1:后端拿到id后不做任何處理
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
SseEmitter emitter; // 聲明SseEmitter類型的變量
@GetMapping("/test/sse")
@ResponseBody
// 客戶端每次重連,都是在觸發(fā)該函數(shù),因此在該函數(shù)用HttpServletRequest即可獲取請(qǐng)求里的id
public SseEmitter handle(HttpServletRequest request){
String lastId=request.getHeader("Last-Event-ID");
// 雖然獲得了該id,但不做任何事情,客戶端有沒有漏收數(shù)據(jù)就不管了
emitter=new SseEmitter();
return emitter;
}
// 例子2:后端拿到id后,看客戶端漏收了哪些數(shù)據(jù),漏收的要補(bǔ)發(fā)
SseEmitter emitter; // 聲明SseEmitter類型的變量
Integer backEndId; // 記錄后端最新數(shù)據(jù)的id
@GetMapping("/test/sse")
@ResponseBody
// 客戶端每次重連,都是在觸發(fā)該函數(shù),因此在該函數(shù)用HttpServletRequest即可獲取請(qǐng)求里的id
public SseEmitter handle(HttpServletRequest request){
String lastId=request.getHeader("Last-Event-ID");
// 獲得了客戶端最后收到的數(shù)據(jù)的id后,對(duì)比后端最新數(shù)據(jù)的id,來補(bǔ)發(fā)客戶端漏收的數(shù)據(jù)
// 自定義一個(gè)函數(shù)來決定要補(bǔ)發(fā)的數(shù)據(jù),如定義下面的compare()函數(shù)
String data=compare(lastId,backEndId)
emitter=new SseEmitter();
// 先添加要發(fā)送的數(shù)據(jù)給SseEmitter ,等Springboot自動(dòng)建立SSE連接后便會(huì)自動(dòng)發(fā)送該數(shù)據(jù)
emitter.send(
SseEmitter
.event()
.data(data)
.id("<Your Id>")
.reconnectTime(5000)
);
return emitter;
}
-
.reconnectTime(<Your Time>)
用<Your Time>
定義在網(wǎng)絡(luò)連接斷開后,客戶端向后端發(fā)起重連的時(shí)間間隔(以毫秒為單位),網(wǎng)絡(luò)連接斷開并重連的情況見開頭那幾點(diǎn)。這個(gè)東西的作用就是讓客戶端在網(wǎng)絡(luò)連接斷開的情況下重連后端,恢復(fù)數(shù)據(jù)傳輸。既然說到了客戶端的時(shí)間設(shè)置
.reconnectTime(<Your Time>)
,那么另外一個(gè)和時(shí)間相關(guān)的設(shè)置就是后端SseEmitter對(duì)象的超時(shí)時(shí)間。在用new SseEmitter()
實(shí)例化SseEmitter 對(duì)象時(shí),還可以輸入一個(gè)long型參數(shù)(如new SseEmitter(timeout);
),代表后端SseEmitter 對(duì)象的超時(shí)時(shí)間(以毫秒為單位),不傳入?yún)?shù)的話則默認(rèn)是30000毫秒。如果SseEmitter 對(duì)象的存在時(shí)間超過了設(shè)定的超時(shí)時(shí)間,那么后端和客戶端分別發(fā)生以下事情:- 后端的SseEmitter 對(duì)象會(huì)主動(dòng)通知客戶端已超時(shí),并且這個(gè)SseEmitter 對(duì)象已無法再發(fā)送新數(shù)據(jù)(此時(shí)用
send()
方法發(fā)送會(huì)拋異常)。 - 客戶端方面,如果后端給客戶端發(fā)送過數(shù)據(jù),那么客戶端接收到超時(shí)通知后會(huì)一直自動(dòng)重連(也就是重新訪問
"/test/sse"
路徑),重連的時(shí)間間隔為.reconnectTime(<Your Time>)
中設(shè)定的時(shí)間,在得到一個(gè)新的SseEmitter
對(duì)象后繼續(xù)發(fā)送數(shù)據(jù)。但如果后端從未用SseEmitter
對(duì)象發(fā)送過任何數(shù)據(jù),那么客戶端便不會(huì)自動(dòng)重連,而是直接報(bào)503的http錯(cuò)誤代碼。 - 說一下超時(shí)時(shí)間的作用,后端設(shè)置超時(shí)時(shí)間,可以在客戶端一直斷網(wǎng)、直接關(guān)閉頁面但未提醒后端的情況下,讓后端在一定時(shí)間等待后自動(dòng)關(guān)閉網(wǎng)絡(luò)連接,節(jié)省資源。如果不存在超時(shí)時(shí)間的話,一旦遇到以上意外情況,后端就會(huì)一直維持該網(wǎng)絡(luò)連接,直到某一次發(fā)送數(shù)據(jù)發(fā)現(xiàn)連接不上才會(huì)去斷開后端的連接,這會(huì)浪費(fèi)資源。
- 后端的SseEmitter 對(duì)象會(huì)主動(dòng)通知客戶端已超時(shí),并且這個(gè)SseEmitter 對(duì)象已無法再發(fā)送新數(shù)據(jù)(此時(shí)用
客戶端使用SSE
?客戶端和后端建立SSE連接,需要雙方都做出努力,不是某一方做了另一方就不用做。基于Springboot的后端如何建立SSE連接已經(jīng)在上面講了,這部分就講下客戶端的Javascript該如何建立SSE連接。
?客戶端需要做3件事:
- 實(shí)例化支持SSE功能的對(duì)象,在絕大部分瀏覽器都內(nèi)置了EventSource對(duì)象,可以用于實(shí)現(xiàn)SSE功能。(好用的Chrome, Firefox, Edge當(dāng)然內(nèi)置了該對(duì)象;但I(xiàn)E瀏覽器沒內(nèi)置該對(duì)象,不過這對(duì)IE瀏覽器來說還挺合理)
// new一個(gè)EventSource對(duì)象,第一個(gè)參數(shù)是后端的訪問地址;第二個(gè)參數(shù)是可選的,如果要填就只能填{withCredentials:true}或{withCredentials:true},表示發(fā)送或不發(fā)送Cookie
var eventSource = new EventSource("http://127.0.0.1:8000/test/sse");
這一行就可以讓客戶端向后端發(fā)送建立SSE連接的請(qǐng)求,只要后端成功建立了SSE連接,后端就可以主動(dòng)向客戶端主動(dòng)、不斷地發(fā)送數(shù)據(jù)了。
- 監(jiān)聽
EventSource
對(duì)象接收數(shù)據(jù)、報(bào)錯(cuò)的事件。
// 監(jiān)聽接受數(shù)據(jù)的事件
eventSource.addEventListener("message", function(event) {console.log(event.data)});
EventSource
默認(rèn)支持三類事件:“open”, “message”, “error”;分別表示客戶端和后端建立了連接、客戶端接收到了來自后端的數(shù)據(jù)、客戶端報(bào)錯(cuò)這三個(gè)場景。使用addEventListener()
用于注冊(cè)事件,第一個(gè)參數(shù)是事件名,第二個(gè)參數(shù)是一個(gè)回調(diào)函數(shù),這個(gè)回調(diào)函數(shù)自帶一個(gè)MessageEvent
對(duì)象,MessageEvent.data
表示后端傳來的數(shù)據(jù),MessageEvent.lastEventId
能表示后端傳來的數(shù)據(jù)id。客戶端拿到MessageEvent.data
后,就可以用來更新用戶的頁面了,不一定非要像console.log(event.data)
在控制臺(tái)打印。
- 在任務(wù)結(jié)束后關(guān)閉SSE連接。下面這行會(huì)讓客戶端自己關(guān)閉SSE連接,并且也會(huì)通知后端關(guān)閉SSE連接。
eventSource.close()
實(shí)例
為了展示客戶端和后端的SSE通信,實(shí)現(xiàn)這樣一個(gè)簡單的例子:
-
客戶端用
EventSource
發(fā)起創(chuàng)建SSE連接的請(qǐng)求給后端,后端進(jìn)行接收并建立起SSE連接。 -
后端每隔5秒向客戶端發(fā)送一次數(shù)據(jù),并設(shè)置
id
用于展示如何補(bǔ)發(fā)數(shù)據(jù),設(shè)置reconnectTime
來展示控制客戶端重連接的時(shí)間間隔(間隔太低浪費(fèi)資源,太高又可能浪費(fèi)時(shí)間來等待)。 -
后端SseEmitter 的對(duì)象設(shè)置超時(shí)時(shí)間為30秒,用來展示客戶端在SSE連接超時(shí)后如何反應(yīng)。(“超時(shí)時(shí)間的大小怎么設(shè)置”要看后端傳數(shù)據(jù)一般需要的時(shí)長,太短了就會(huì)讓客戶端多次重連接后端消耗資源,太長了又可能在客戶端斷網(wǎng)、關(guān)閉頁面但未通知后端的情況下,依然讓后端維持太久的連接。)
代碼1:pom.xml中的依賴
后端只需要引入springboot的web場景。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
代碼2:后端的controller
先說一句題外話,前后端分離項(xiàng)目先解決跨域問題,自己用瀏覽器做測(cè)試也要解決跨域問題。對(duì)于Springboot框架來說,解法跨域的方案之一就是繼承WebMvcConfigurationSupport
類并注冊(cè)到容器中(加上@Configuration
或@Component
等注冊(cè)組件),并重寫和跨域相關(guān)的方法。這樣的話Springboot會(huì)自動(dòng)讀取這個(gè)組件對(duì)跨域的設(shè)置。代碼如下
@Configuration
public class MvcConfig extends WebMvcConfigurationSupport {
// 重寫這個(gè)方法
@Override
protected void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080") // 允許本地8080端口進(jìn)行請(qǐng)求
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.allowCredentials(true)
.allowedHeaders("*")
.maxAge(3600);
}
}
讓后端能建立SSE連接,并每隔5秒向客戶端發(fā)送數(shù)據(jù)。
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
public class TestSSEController {
SseEmitter emitter; // 實(shí)現(xiàn)SSE功能的類
Integer backEndId=0; // 記錄后端最后發(fā)送的數(shù)據(jù)的id,用于判斷客戶端是否遺漏數(shù)據(jù)
String backEndData=null; // 記錄后端最后發(fā)送的數(shù)據(jù),如果客戶端有遺漏數(shù)據(jù),則先發(fā)送該條數(shù)據(jù)
@GetMapping("/test/sse")
@ResponseBody
public SseEmitter handle(HttpServletRequest request) throws IOException{
emitter=new SseEmitter(30000); // 設(shè)置后端SSE連接的超時(shí)時(shí)長為30秒
String lastId=request.getHeader("Last-Event-ID"); // 獲取客戶端收到的最后一個(gè)數(shù)據(jù)的id,用來判斷是否有漏收
// 兩種情況意味著漏收數(shù)據(jù):
// ①客戶端沒收到任何數(shù)據(jù),但后端記錄的最后發(fā)送的數(shù)據(jù)不為空
// ②客戶端收到過數(shù)據(jù),但客戶端最后收到的數(shù)據(jù)id和后端最后發(fā)送的數(shù)據(jù)id不一致
if((lastId==null && backEndData!=null) || (lastId!=null && !lastId.equals(backEndId.toString()))){
// 客戶端漏收了數(shù)據(jù),后端先將最新的數(shù)據(jù)返回給客戶端
emitter.send(
SseEmitter
.event()
.data(backEndData)
.id(backEndId.toString())
.reconnectTime(3000)
);
}
return emitter;
}
// Springboot用@Scheduled創(chuàng)建定時(shí)任務(wù),每隔5秒自動(dòng)執(zhí)行向客戶端發(fā)送數(shù)據(jù)的函數(shù)
// 除了要在這里加@Scheduled注解,還要在啟動(dòng)類(通常是xxxApplication類)上加@EnableScheduling注解
@Scheduled(cron="0/5 * * * * ? ")
public void trigger() throws IOException{
try{
backEndId++; // 每次發(fā)送時(shí)id都要+1
emitter.send(
SseEmitter
.event()
.data("data"+backEndId) // 發(fā)送新數(shù)據(jù)
.id(backEndId.toString()) // 發(fā)送新id
.reconnectTime(3000)
);
backEndData="data"+backEndId; // 更新最新的數(shù)據(jù)
System.out.println("id: "+backEndId.toString()+" data: "+backEndData);
}catch(Exception e){
backEndId--;
System.out.println("id: "+backEndId.toString()+" data: "+backEndData);
System.out.println(e.getMessage());
}
}
}
代碼3:客戶端的設(shè)置
首先要做的不是打開瀏覽器在控制臺(tái)輸入javascript,而是開啟一個(gè)客戶端服務(wù)器,以便和后端通信。最簡單的開啟方式就是用python自帶的功能,可以創(chuàng)建一個(gè)位于localhost:8080
的客戶端服務(wù)器(要為該路徑設(shè)置跨域,怎么設(shè)置的見上面)。
python -m http.server 8080
# 這個(gè)8080的端口號(hào)可以改成別的
開啟客戶端服務(wù)器后,打開瀏覽器訪問localhost:8080
路徑,再打開控制臺(tái),用EventSource
對(duì)象發(fā)起創(chuàng)建SSE連接的請(qǐng)求,并且在每次收到新數(shù)據(jù)時(shí)打印數(shù)據(jù)。
var eventSource = new EventSource("http://localhost:8000/test/sse");
eventSource.addEventListener("message", function(e) {console.log(e.data)});
// 這兩行一執(zhí)行,客戶端就會(huì)向后端發(fā)起創(chuàng)建SSE的請(qǐng)求,后端接收后會(huì)成功建立SSE。
// 之后后端每隔5秒發(fā)送數(shù)據(jù),客戶端就會(huì)把這些數(shù)據(jù)打印在控制臺(tái)中
效果1:控制臺(tái)打印的數(shù)據(jù)的樣子
后端每隔5秒發(fā)送一次數(shù)據(jù)給客戶端,客戶端拿到之后打印到控制臺(tái),所以也是差不多每隔5秒打印一次數(shù)據(jù)。
效果2:實(shí)際發(fā)起了多次SSE連接
我們將后端SSE超時(shí)時(shí)間設(shè)置為了30秒,且每5秒發(fā)送一次數(shù)據(jù),那么后端大約會(huì)發(fā)6個(gè)數(shù)據(jù),那為什么客戶端控制臺(tái)上打印了遠(yuǎn)遠(yuǎn)不止這么多的數(shù)據(jù)呢?這時(shí)候就需要看網(wǎng)絡(luò)請(qǐng)求而不是控制臺(tái)了,在開發(fā)者工具欄里點(diǎn)擊“網(wǎng)絡(luò)”,結(jié)果如下兩圖所示??梢钥吹矫恳粋€(gè)SSE請(qǐng)求只會(huì)帶來6個(gè)數(shù)據(jù),我們之所以能收到超出6個(gè)數(shù)據(jù),是因?yàn)榭蛻舳俗詣?dòng)地重連了,讓后端不斷地產(chǎn)生新的SseEmitter
(忘記這回事的朋友看上文)。
效果3:客戶端斷網(wǎng)重連時(shí)補(bǔ)發(fā)數(shù)據(jù)
為了模擬客戶端斷網(wǎng)重連,后端補(bǔ)發(fā)遺漏數(shù)據(jù)的情況。我在傳送到第15個(gè)數(shù)據(jù)時(shí)啟動(dòng)了瀏覽器的離線功能來模擬斷網(wǎng),見下圖紅色圈圈那里。
此時(shí)客戶端檢測(cè)到了網(wǎng)絡(luò)問題,會(huì)每隔3秒(3秒是后端的.reconnectTime(30000)
設(shè)置的)重連一次,此時(shí)控制臺(tái)會(huì)顯示以下重連信息:
后端只有在發(fā)送數(shù)據(jù)后,收到數(shù)據(jù)沒傳過去的錯(cuò)誤信息時(shí)才會(huì)意識(shí)到客戶端那邊已斷開連接,此時(shí)后端也會(huì)斷開自己的連接,SseEmitter
也不能再傳數(shù)據(jù)(此時(shí)再調(diào)用SseEmitter.send()
方法會(huì)拋異常的),而在此之前后端可能已經(jīng)發(fā)了好幾個(gè)新數(shù)據(jù),如下圖所示??蛻舳嗽谑盏降?5個(gè)數(shù)據(jù)的時(shí)候就已經(jīng)斷網(wǎng)了,但后端不能立即知道這件事,就又發(fā)到了第17個(gè)數(shù)據(jù),這時(shí)才收到數(shù)據(jù)傳輸?shù)腻e(cuò)誤信息,從而斷開連接,并讓SseEmitter再也不能發(fā)送任何消息。
接下來就是展示補(bǔ)發(fā)數(shù)據(jù)的過程了,如果恢復(fù)網(wǎng)絡(luò),也就是將下圖紅圈圈的下拉框選成“高速3G”,客戶端重連后端就會(huì)成功,上文后端Controller實(shí)現(xiàn)了補(bǔ)發(fā)最后一個(gè)數(shù)據(jù)的功能,此時(shí)客戶端會(huì)收到第17個(gè)數(shù)據(jù),正如下圖紅圈圈所示。如果你想要補(bǔ)發(fā)中途漏掉的所有數(shù)據(jù),那就自己修改后端的Controller。
說幾句廢話,上圖網(wǎng)絡(luò)請(qǐng)求中未成功的、標(biāo)紅的,都是客戶端重連失敗的產(chǎn)物;至于為什么上圖只有3個(gè)數(shù)據(jù)而不是預(yù)想中的6個(gè),是因?yàn)槲姨崆罢{(diào)用了客戶端中EventSource
對(duì)象的close()
方法,來關(guān)閉SSE連接。
線程安全問題
上面說的都是測(cè)試性的例子,沒有討論后端SseEmitter
在多線程并發(fā)下的問題,這里補(bǔ)充一下:SseEmitter
對(duì)象的send()
方法是線程不安全的,如果兩個(gè)線程對(duì)同一個(gè)SseEmitter
對(duì)象交替使用send()
方法,就可能會(huì)讓發(fā)送的數(shù)據(jù)有問題。所以,如果想實(shí)現(xiàn)線程安全,要么一個(gè)線程修改一個(gè)SseEmitter
對(duì)象而不跟其他線程共用,要么自己寫一個(gè)類繼承SseEmitter
,重寫send()
方法時(shí)加上synchronized
同步代碼塊,如下面所示文章來源:http://www.zghlxwxcb.cn/news/detail-456357.html
// 自己封裝一個(gè)線程安全的SseEmitter
public class SSEThreadSafeWrapper extends SseEmitter{
public SSEThreadSafeWrapper() {
super();
}
public SSEThreadSafeWrapper(long l) {
super(l);
}
@Override
public void send(SseEventBuilder arg0) throws IOException {
// 線程安全
synchronized(this){
super.send(arg0);
}
}
@Override
public void send(Object object) throws IOException {
// 線程安全
synchronized(this){
super.send(object);
}
}
@Override
public void send(Object object, MediaType mediaType) throws IOException {
// 線程安全
synchronized(this){
super.send(object, mediaType);
}
}
}
然后Springboot就改一處地方:文章來源地址http://www.zghlxwxcb.cn/news/detail-456357.html
emitter=new SseEmitter(30000);
// 上面的改為下面的
emitter=new SSEThreadSafeWrapper (30000);
到了這里,關(guān)于Server side event (SSE)實(shí)現(xiàn)消息推送功能的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!