7種實(shí)現(xiàn)web實(shí)時消息推送的方案
發(fā)布日期:2022/8/9 14:30:22 瀏覽量:
大家好,我是小富~
我有一個朋友~
做了一個小破站,現(xiàn)在要實(shí)現(xiàn)一個站內(nèi)信web消息推送的功能,對,就是下圖這個小紅點(diǎn),一個很常用的功能。
不過他還沒想好用什么方式做,這里我?guī)退砹艘幌聨追N方案,并簡單做了實(shí)現(xiàn)。
推送的場景比較多,比如有人關(guān)注我的公眾號,這時我就會收到一條推送消息,以此來吸引我點(diǎn)擊打開應(yīng)用。
消息推送(push)通常是指網(wǎng)站的運(yùn)營工作等人員,通過某種工具對用戶當(dāng)前網(wǎng)頁或移動設(shè)備APP進(jìn)行的主動消息推送。
消息推送一般又分為web端消息推送和移動端消息推送。
上邊的這種屬于移動端消息推送,web端消息推送常見的諸如站內(nèi)信、未讀郵件數(shù)量、監(jiān)控報警數(shù)量等,應(yīng)用的也非常廣泛。
在具體實(shí)現(xiàn)之前,咱們再來分析一下前邊的需求,其實(shí)功能很簡單,只要觸發(fā)某個事件(主動分享了資源或者后臺主動推送消息),web頁面的通知小紅點(diǎn)就會實(shí)時的+1就可以了。
通常在服務(wù)端會有若干張消息推送表,用來記錄用戶觸發(fā)不同事件所推送不同類型的消息,前端主動查詢(拉)或者被動接收(推)用戶所有未讀的消息數(shù)。
消息推送無非是推(push)和拉(pull)兩種形式,下邊我們逐個了解下。
短輪詢
輪詢(polling)應(yīng)該是實(shí)現(xiàn)消息推送方案中最簡單的一種,這里我們暫且將輪詢分為短輪詢和長輪詢。
短輪詢很好理解,指定的時間間隔,由瀏覽器向服務(wù)器發(fā)出HTTP請求,服務(wù)器實(shí)時返回未讀消息數(shù)據(jù)給客戶端,瀏覽器再做渲染顯示。
一個簡單的JS定時器就可以搞定,每秒鐘請求一次未讀消息數(shù)接口,返回的數(shù)據(jù)展示即可。
setInterval(() => {
// 方法請求
messageCount().then((res) => {
if (res.code === 200) {
this.messageCount = res.data
}, 1000);
效果還是可以的,短輪詢實(shí)現(xiàn)固然簡單,缺點(diǎn)也是顯而易見,由于推送數(shù)據(jù)并不會頻繁變更,無論后端此時是否有新的消息產(chǎn)生,客戶端都會進(jìn)行請求,勢必會對服務(wù)端造成很大壓力,浪費(fèi)帶寬和服務(wù)器資源。
長輪詢
長輪詢是對上邊短輪詢的一種改進(jìn)版本,在盡可能減少對服務(wù)器資源浪費(fèi)的同時,保證消息的相對實(shí)時性。長輪詢在中間件中應(yīng)用的很廣泛,比如Nacos和apollo配置中心,消息隊列kafka、RocketMQ中都有用到長輪詢。
一文中我詳細(xì)介紹過Nacos長輪詢的實(shí)現(xiàn)原理,感興趣的小伙伴可以瞅瞅。
這次我使用apollo配置中心實(shí)現(xiàn)長輪詢的方式,應(yīng)用了一個類DeferredResult,它是在servelet3.0后經(jīng)過Spring封裝提供的一種異步請求機(jī)制,直意就是延遲結(jié)果。
DeferredResult可以允許容器線程快速釋放占用的資源,不阻塞請求線程,以此接收更多的請求提升系統(tǒng)的吞吐量,然后啟動異步工作線程處理真正的業(yè)務(wù)邏輯,處理完成調(diào)用DeferredResult.setResult(200)提交響應(yīng)結(jié)果。
下邊我們用長輪詢來實(shí)現(xiàn)消息推送。
因?yàn)橐粋€ID可能會被多個長輪詢請求監(jiān)聽,所以我采用了guava包提供的Multimap結(jié)構(gòu)存放長輪詢,一個key可以對應(yīng)多個value。一旦監(jiān)聽到key發(fā)生變化,對應(yīng)的所有長輪詢都會響應(yīng)。前端得到非請求超時的狀態(tài)碼,知曉數(shù)據(jù)變更,主動查詢未讀消息數(shù)接口,更新頁面數(shù)據(jù)。
@Controller
@RequestMapping("/polling")
public class PollingController {
// 存放監(jiān)聽某個Id的長輪詢集合
// 線程同步結(jié)構(gòu)
public static Multimap> watchRequests = Multimaps.synchronizedMultimap(HashMultimap.create());
/**
* 公眾號:程序員小富
* 設(shè)置監(jiān)聽
*/
@GetMapping(path = "watch/{id}")
@ResponseBody
public DeferredResult watch(@PathVariable String id) {
// 延遲對象設(shè)置超時時間
DeferredResult deferredResult = new DeferredResult<>(TIME_OUT);
// 異步請求完成時移除 key,防止內(nèi)存溢出
deferredResult.onCompletion(() -> {
watchRequests.remove(id, deferredResult);
});
// 注冊長輪詢請求
watchRequests.put(id, deferredResult);
return deferredResult;
}
/**
* 公眾號:程序員小富
* 變更數(shù)據(jù)
*/
@GetMapping(path = "publish/{id}")
@ResponseBody
public String publish(@PathVariable String id) {
// 數(shù)據(jù)變更 取出監(jiān)聽ID的所有長輪詢請求,并一一響應(yīng)處理
if (watchRequests.containsKey(id)) {
Collection> deferredResults = watchRequests.get(id);
for (DeferredResult deferredResult : deferredResults) {
deferredResult.setResult("我更新了" + new Date());
}
}
return "success";
}
當(dāng)請求超過設(shè)置的超時時間,會拋出AsyncRequestTimeoutException異常,這里直接用@ControllerAdvice全局捕獲統(tǒng)一返回即可,前端獲取約定好的狀態(tài)碼后再次發(fā)起長輪詢請求,如此往復(fù)調(diào)用。
@ControllerAdvice
public class AsyncRequestTimeoutHandler {
@ResponseStatus(HttpStatus.NOT_MODIFIED)
@ResponseBody
@ExceptionHandler(AsyncRequestTimeoutException.class)
public String asyncRequestTimeoutHandler(AsyncRequestTimeoutException e) {
System.out.println("異步請求超時");
return "304";
}
}
我們來測試一下,首先頁面發(fā)起長輪詢請求/polling/watch/10086監(jiān)聽消息變更,請求被掛起,不變更數(shù)據(jù)直至超時,再次發(fā)起了長輪詢請求;緊接著手動變更數(shù)據(jù)/polling/publish/10086,長輪詢得到響應(yīng),前端處理業(yè)務(wù)邏輯完成后再次發(fā)起請求,如此循環(huán)往復(fù)。
長輪詢相比于短輪詢在性能上提升了很多,但依然會產(chǎn)生較多的請求,這是它的一點(diǎn)不完美的地方。
iframe流
iframe流就是在頁面中插入一個隱藏的標(biāo)簽,通過在src中請求消息數(shù)量API接口,由此在服務(wù)端和客戶端之間創(chuàng)建一條長連接,服務(wù)端持續(xù)向iframe傳輸數(shù)據(jù)。
傳輸?shù)臄?shù)據(jù)通常是HTML、或是內(nèi)嵌的javascript腳本,來達(dá)到實(shí)時更新頁面的效果。
這種方式實(shí)現(xiàn)簡單,前端只要一個標(biāo)簽搞定了
馬上咨詢: 如果您有業(yè)務(wù)方面的問題或者需求,歡迎您咨詢!我們帶來的不僅僅是技術(shù),還有行業(yè)經(jīng)驗(yàn)積累。
QQ: 39764417/308460098 Phone: 13 9800 1 9844 / 135 6887 9550 聯(lián)系人:石先生/雷先生