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