背景
有一個項目,前端vue,后端springboot?,F(xiàn)在需要做一個功能:用戶在使用系統(tǒng)的時候,管理員發(fā)布公告,則使用系統(tǒng)的用戶可以看到該公告。
基于此,一個簡單的方案:前端使用JS方法setInterval,重復(fù)調(diào)用后端公告獲取接口。此方法有幾點缺陷:
- 循環(huán)調(diào)用的時間間隔不好確定:太長了,獲取公告的時效有延遲;太短了,給服務(wù)器造成壓力,很多請求都是無用的(公告發(fā)布的時間不定,很可能幾天都沒有新公告);
- token的續(xù)期問題:項目中,前端請求,需要帶上token,token有過期時間,如果用戶一直使用(前后端有交互),會無感續(xù)期。如果有這種定時循環(huán)和后端交互的場景,就會造成token用不過期(循環(huán)的調(diào)用會觸發(fā)續(xù)期),當(dāng)然,可以在續(xù)期中,排除某個場景的請求,但是這樣的設(shè)計不好,因為這種場景太多了,就會造成維護上的困難。
因此就想到了,如果后端主動向前端推送消息,這個問題就可以完美解決。
方案
有兩種方案可以實現(xiàn)后端向前端推送消息:
- 使用websocket;
- 使用sse;
這里介紹SSE的方式(如果系統(tǒng)中對這種消息的準(zhǔn)確性和可靠性有嚴(yán)格的要求,則使用websocket,websocket的使用相對復(fù)雜的多);
如果想了解SSE的詳細基礎(chǔ)知識,可以參考阮一峰老師的這篇文章:Server-Sent Events 教程
SSE后端代碼
SpringMVC中,已經(jīng)集成了該功能,所以無需額外引入jar包,直接上代碼:
@RestController
@RequestMapping("/notice")
public class NoticeController {
@Autowired
private NoticeService noticeService;
@GetMapping(path = "createSseEmitter")
public SseEmitter createSseEmitter(String id) {
return noticeService.createSseEmitter(id);
}
@PostMapping(path = "sendMsg")
public boolean sendMsg(String id, String content) {
noticeService.sendMsg(id, content);
return true;
}
}
@Slf4j
@Service
public class NoticeServiceImpl implements NoticeService {
@Autowired
@Qualifier("sseEmitterCacheService")
private CacheService<SseEmitter> sseEmitterCacheService;
@Override
public SseEmitter createSseEmitter(String clientId) {
if (StringUtil.isBlank(clientId)) {
clientId = UUID.randomUUID().toString().replace("-", "");
}
SseEmitter sseEmitter = sseEmitterCacheService.getCache(clientId);
log.info("獲取SSE,id={}", clientId);
final String id = clientId;
sseEmitter.onCompletion(() -> {
log.info("SSE已完成,關(guān)閉連接 id={}", id);
sseEmitterCacheService.deleteCache(id);
});
return sseEmitter;
}
@Override
public void sendMsg(String clientId, String content) {
if (sseEmitterCacheService.hasCache(clientId)) {
SseEmitter sseEmitter = sseEmitterCacheService.getCache(clientId);
try {
sseEmitter.send(content);
} catch (IOException e) {
log.error("發(fā)送消息失敗:{}", e.getMessage(), e);
throw new BusinessRuntimeExcepption(CustomExcetionConstant.IO_ERR, "發(fā)送消息失敗", e);
}
} else {
log.error("SSE對象不存在");
throw new BusinessRuntimeExcepption("SSE對象不存在");
}
}
}
這里,只列出了核心的代碼,簡而言之,需要做到兩點即可:
- 前端首先是發(fā)起一個請求,創(chuàng)建SseEmitter,即createSseEmitter方法,該方法必須返回一個SseEmitter對象;
- 返回的SseEmitter,后端必須要緩存起來(我用的是ehcache,也可以直接定義一個map來緩存);
為什么要這么做?看下文,后端代碼一起來分析就明白了。
前端代碼
由于,我請求該接口,需要帶上token,所以直接使用EventSource不行,另外這個IE也不支持。所以選擇了一個工具:event-source-polyfill。
- 先安裝event-source-polyfill
npm install event-source-polyfill
- 然后使用:
import { EventSourcePolyfill } from "event-source-polyfill";
created() {
let _this = this;
this.source = new EventSourcePolyfill(
"/" +
process.env.VUE_APP_MANAGER_PRE_API_URL +
"/notice/createSseEmitter?id=" +
uuid(),
{
headers: {
[process.env.VUE_APP_OAUTH_AUTHORIZATION]: store.getters.getToken,
},
//重連時間間隔,單位:毫秒,默認(rèn)45000毫秒,這里設(shè)置為10分鐘
heartbeatTimeout: 10 * 60 * 1000,
}
);
this.source.onopen = () => {
console.log("NOTICE建立連接");
};
this.source.onmessage = (e) => {
_this.scrollMessage = e.data;
console.log("NOTICE接收到消息");
};
this.source.onerror = (e) => {
if (e.readyState == EventSource.CLOSED) {
console.log("NOTICE連接關(guān)閉");
} else if (this.source.readyState == EventSource.CONNECTING) {
console.log("NOTICE正在重連");
//重新設(shè)置header
this.source.headers = {
[process.env.VUE_APP_OAUTH_AUTHORIZATION]: store.getters.getToken,
};
} else {
console.log(e);
}
};
},
有幾點說明:
- new EventSourcePolyfill中,可以帶入header
- heartbeatTimeout是一個心跳時間,默認(rèn)情況下間隔heartbeatTimeout后,會觸發(fā)重新連接后端接口;
- this.source.headers,該行的作用是在重連的時候重新設(shè)置header,如果不這樣,那么重連的時候,用的參數(shù)信息,還是和最開始的一樣(包括本例中url中的id)。而由于我的項目中,如果token其他操作觸發(fā)了刷新token,則有效token可能會變,所以,這里取緩存中放置的token,而不應(yīng)該使用最初的token。
好了,這樣就基本實現(xiàn)了我們所需要的功能了。
特別注意
前端配置了代理,所以一直收不到后端發(fā)送的消息,嘗試加入以下參數(shù):
devServer: {
compress:false,
…………
}
問題
之前在寫后端的時候提到了兩個問題:為什么要返回SseEmitter對象?為什么要緩存SseEmitter對象?
其實看過SSE的原理,都應(yīng)該明白:這就是一個長連接,前端調(diào)用創(chuàng)建SseEmitter對象的接口,雖然接口返回了,但是并未結(jié)束(這就是為什么要返回SseEmitter對象,如果返回的是一個其他對象,就和普通的接口沒兩樣了, 該接口就直接結(jié)束了),請看下截圖:
發(fā)起請求之后,一直是待處理,并未結(jié)束,10分鐘之后,該請求被取消(前端設(shè)置的重連),然后重新發(fā)起連接,重新發(fā)起的連接也是在等待中。只有接收到消息后,這個請求的狀態(tài)碼才是200,但是這個時候才連接已經(jīng)建立好了。其中的細節(jié),這里不做講述。
所以,如果再使用SseEmitter對象發(fā)送消息,則前端就可以收到對象的消息了(即實現(xiàn)后端向前端發(fā)送消息)。這里使用的SseEmitter對象,就是createSseEmitter接口返回的對象(也就是使用哪個SseEmitter對象,就可以向哪個前端發(fā)送消息)。這也就是為什么要緩存SseEmitter對象的原因了。文章來源:http://www.zghlxwxcb.cn/news/detail-788403.html
效果
通過調(diào)用發(fā)送消息接口,前端即可立即展示發(fā)送的消息:文章來源地址http://www.zghlxwxcb.cn/news/detail-788403.html
到了這里,關(guān)于SSE:后端向前端發(fā)送消息(springboot SseEmitter)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!