SpringBoot實現(xiàn)網(wǎng)頁消息推送的5種方法小結(jié)
項目開發(fā)中,實時消息推送已成為提升用戶體驗的關(guān)鍵技術(shù)。無論是聊天應(yīng)用、通知系統(tǒng)、實時數(shù)據(jù)展示,還是協(xié)同辦公場景,都需要服務(wù)器能夠主動向客戶端推送消息。本文將詳細介紹SpringBoot中實現(xiàn)網(wǎng)頁消息推送的幾種主流方案,幫助開發(fā)者根據(jù)實際需求選擇最合適的技術(shù)。
一、為什么需要消息推送
傳統(tǒng)的HTTP請求是客戶端主動請求,服務(wù)端被動響應(yīng)的模式。但在很多場景下,我們需要服務(wù)器能夠主動將消息推送給瀏覽器,例如:
- Web版即時通訊
- 股票、基金等金融數(shù)據(jù)實時更新
- 系統(tǒng)通知和提醒
- 協(xié)同編輯文檔時的實時更新
- ......
二、消息推送實現(xiàn)方案
1. 短輪詢 (Short Polling)
原理:客戶端以固定的時間間隔頻繁發(fā)送請求,詢問服務(wù)器是否有新消息。
實現(xiàn)方式:
@RestController @RequestMapping("/api/messages") public class MessageController { private final Map<String, List<String>> userMessages = new ConcurrentHashMap<>(); @GetMapping("/{userId}") public List<String> getMessages(@PathVariable String userId) { List<String> messages = userMessages.getOrDefault(userId, new ArrayList<>()); List<String> result = new ArrayList<>(messages); messages.clear(); // 清空已讀消息 return result; } @PostMapping("/{userId}") public void sendMessage(@PathVariable String userId, @RequestBody String message) { userMessages.computeIfAbsent(userId, k -> new ArrayList<>()).add(message); } }
前端實現(xiàn):
function startPolling() { setInterval(() => { fetch('/api/messages/user123') .then(response => response.json()) .then(messages => { if (messages.length > 0) { messages.forEach(msg => console.log(msg)); } }); }, 3000); // 每3秒查詢一次 }
優(yōu)點:
- 實現(xiàn)簡單,不需要特殊的服務(wù)器配置
- 兼容性好,支持幾乎所有瀏覽器和服務(wù)器
缺點:
- 資源消耗大,大量無效請求
- 實時性較差,受輪詢間隔影響
- 服務(wù)器負載高,尤其是在用戶量大的情況下
2. 長輪詢 (Long Polling)
原理:客戶端發(fā)送請求后,如果服務(wù)器沒有新消息,則保持連接打開直到有新消息或超時,然后客戶端立即發(fā)起新的請求。
實現(xiàn)方式:
@RestController @RequestMapping("/api/long-polling") public class LongPollingController { private final Map<String, DeferredResult<List<String>>> waitingRequests = new ConcurrentHashMap<>(); private final Map<String, List<String>> pendingMessages = new ConcurrentHashMap<>(); @GetMapping("/{userId}") public DeferredResult<List<String>> waitForMessages(@PathVariable String userId) { DeferredResult<List<String>> result = new DeferredResult<>(60000L, new ArrayList<>()); // 檢查是否有待處理的消息 List<String> messages = pendingMessages.get(userId); if (messages != null && !messages.isEmpty()) { List<String> messagesToSend = new ArrayList<>(messages); messages.clear(); result.setResult(messagesToSend); } else { // 沒有消息,等待 waitingRequests.put(userId, result); result.onCompletion(() -> waitingRequests.remove(userId)); result.onTimeout(() -> waitingRequests.remove(userId)); } return result; } @PostMapping("/{userId}") public void sendMessage(@PathVariable String userId, @RequestBody String message) { // 查看是否有等待的請求 DeferredResult<List<String>> deferredResult = waitingRequests.get(userId); if (deferredResult != null) { List<String> messages = new ArrayList<>(); messages.add(message); deferredResult.setResult(messages); waitingRequests.remove(userId); } else { // 存儲消息,等待下一次輪詢 pendingMessages.computeIfAbsent(userId, k -> new ArrayList<>()).add(message); } } }
前端實現(xiàn):
function longPolling() { fetch('/api/long-polling/user123') .then(response => response.json()) .then(messages => { if (messages.length > 0) { messages.forEach(msg => console.log(msg)); } // 立即發(fā)起下一次長輪詢 longPolling(); }) .catch(() => { // 出錯后延遲一下再重試 setTimeout(longPolling, 5000); }); }
優(yōu)點:
- 減少無效請求,相比短輪詢更高效
- 近實時體驗,有消息時立即推送
- 兼容性好,幾乎所有瀏覽器都支持
缺點:
- 服務(wù)器資源消耗,大量連接會占用服務(wù)器資源
- 可能受超時限制
- 難以處理服務(wù)器主動推送的場景
3. Server-Sent Events (SSE)
原理:服務(wù)器與客戶端建立單向連接,服務(wù)器可以持續(xù)向客戶端推送數(shù)據(jù),而不需要客戶端重復(fù)請求。
SpringBoot實現(xiàn):
@RestController @RequestMapping("/api/sse") public class SSEController { private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>(); @GetMapping("/subscribe/{userId}") public SseEmitter subscribe(@PathVariable String userId) { SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); emitter.onCompletion(() -> emitters.remove(userId)); emitter.onTimeout(() -> emitters.remove(userId)); emitter.onError(e -> emitters.remove(userId)); // 發(fā)送一個初始事件保持連接 try { emitter.send(SseEmitter.event().name("INIT").data("連接已建立")); } catch (IOException e) { emitter.completeWithError(e); } emitters.put(userId, emitter); return emitter; } @PostMapping("/publish/{userId}") public ResponseEntity<String> publish(@PathVariable String userId, @RequestBody String message) { SseEmitter emitter = emitters.get(userId); if (emitter != null) { try { emitter.send(SseEmitter.event() .name("MESSAGE") .data(message)); return ResponseEntity.ok("消息已發(fā)送"); } catch (IOException e) { emitters.remove(userId); return ResponseEntity.internalServerError().body("發(fā)送失敗"); } } else { return ResponseEntity.notFound().build(); } } @PostMapping("/broadcast") public ResponseEntity<String> broadcast(@RequestBody String message) { List<String> deadEmitters = new ArrayList<>(); emitters.forEach((userId, emitter) -> { try { emitter.send(SseEmitter.event() .name("BROADCAST") .data(message)); } catch (IOException e) { deadEmitters.add(userId); } }); deadEmitters.forEach(emitters::remove); return ResponseEntity.ok("廣播消息已發(fā)送"); } }
前端實現(xiàn):
function connectSSE() { const eventSource = new EventSource('/api/sse/subscribe/user123'); eventSource.addEventListener('INIT', function(event) { console.log(event.data); }); eventSource.addEventListener('MESSAGE', function(event) { console.log('收到消息: ' + event.data); }); eventSource.addEventListener('BROADCAST', function(event) { console.log('收到廣播: ' + event.data); }); eventSource.onerror = function() { eventSource.close(); // 可以在這里實現(xiàn)重連邏輯 setTimeout(connectSSE, 5000); }; }
優(yōu)點:
- 真正的服務(wù)器推送,節(jié)省資源
- 自動重連機制
- 支持事件類型區(qū)分
- 相比WebSocket更輕量
缺點:
- 單向通信,客戶端無法通過SSE向服務(wù)器發(fā)送數(shù)據(jù)
- 連接數(shù)限制,瀏覽器對同一域名的SSE連接數(shù)有限制
- IE瀏覽器不支持
4. WebSocket
原理:WebSocket是一種雙向通信協(xié)議,在單個TCP連接上提供全雙工通信通道。
SpringBoot配置:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new MessageWebSocketHandler(), "/ws/messages") .setAllowedOrigins("*"); } } public class MessageWebSocketHandler extends TextWebSocketHandler { private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String userId = extractUserId(session); sessions.put(userId, session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 處理從客戶端接收的消息 String payload = message.getPayload(); // 處理邏輯... } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { String userId = extractUserId(session); sessions.remove(userId); } private String extractUserId(WebSocketSession session) { // 從session中提取用戶ID return session.getUri().getQuery().replace("userId=", ""); } // 發(fā)送消息給指定用戶 public static void sendToUser(String userId, String message) { WebSocketSession session = sessions.get(userId); if (session != null && session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { sessions.remove(userId); } } } // 廣播消息 public static void broadcast(String message) { sessions.forEach((userId, session) -> { if (session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { sessions.remove(userId); } } }); } }
前端實現(xiàn):
function connectWebSocket() { const socket = new WebSocket('ws://localhost:8080/ws/messages?userId=user123'); socket.onopen = function() { console.log('WebSocket連接已建立'); // 可以發(fā)送一條消息 socket.send(JSON.stringify({type: 'JOIN', content: '用戶已連接'})); }; socket.onmessage = function(event) { const message = JSON.parse(event.data); console.log('收到消息:', message); }; socket.onclose = function() { console.log('WebSocket連接已關(guān)閉'); // 可以在這里實現(xiàn)重連邏輯 setTimeout(connectWebSocket, 5000); }; socket.onerror = function(error) { console.error('WebSocket錯誤:', error); socket.close(); }; }
優(yōu)點:
- 全雙工通信,服務(wù)器和客戶端可以隨時相互發(fā)送數(shù)據(jù)
- 實時性最好,延遲最低
- 效率高,建立連接后無需HTTP頭,數(shù)據(jù)傳輸量小
- 支持二進制數(shù)據(jù)
缺點:
- 實現(xiàn)相對復(fù)雜
- 對服務(wù)器要求高,需要處理大量并發(fā)連接
- 可能受到防火墻限制
5. STOMP (基于WebSocket)
原理:STOMP (Simple Text Oriented Messaging Protocol) 是一個基于WebSocket的簡單消息傳遞協(xié)議,提供了更高級的消息傳遞模式。
SpringBoot配置:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 啟用簡單的基于內(nèi)存的消息代理 registry.enableSimpleBroker("/topic", "/queue"); // 設(shè)置應(yīng)用的前綴 registry.setApplicationDestinationPrefixes("/app"); // 設(shè)置用戶目的地前綴 registry.setUserDestinationPrefix("/user"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") .setAllowedOrigins("*") .withSockJS(); // 添加SockJS支持 } } @Controller public class MessageController { private final SimpMessagingTemplate messagingTemplate; public MessageController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } // 處理客戶端發(fā)送到/app/sendMessage的消息 @MessageMapping("/sendMessage") public void processMessage(String message) { // 處理消息... } // 處理客戶端發(fā)送到/app/chat/{roomId}的消息,并廣播到相應(yīng)的聊天室 @MessageMapping("/chat/{roomId}") @SendTo("/topic/chat/{roomId}") public ChatMessage chat(@DestinationVariable String roomId, ChatMessage message) { // 處理聊天消息... return message; } // 發(fā)送私人消息 @MessageMapping("/private-message") public void privateMessage(PrivateMessage message) { messagingTemplate.convertAndSendToUser( message.getRecipient(), // 接收者的用戶名 "/queue/messages", // 目的地 message // 消息內(nèi)容 ); } // REST API發(fā)送廣播消息 @PostMapping("/api/broadcast") public ResponseEntity<String> broadcast(@RequestBody String message) { messagingTemplate.convertAndSend("/topic/broadcast", message); return ResponseEntity.ok("消息已廣播"); } // REST API發(fā)送私人消息 @PostMapping("/api/private-message/{userId}") public ResponseEntity<String> sendPrivateMessage( @PathVariable String userId, @RequestBody String message) { messagingTemplate.convertAndSendToUser(userId, "/queue/messages", message); return ResponseEntity.ok("私人消息已發(fā)送"); } }
前端實現(xiàn):
const stompClient = new StompJs.Client({ brokerURL: 'ws://localhost:8080/ws', connectHeaders: { login: 'user', passcode: 'password' }, debug: function (str) { console.log(str); }, reconnectDelay: 5000, heartbeatIncoming: 4000, heartbeatOutgoing: 4000 }); stompClient.onConnect = function (frame) { console.log('Connected: ' + frame); // 訂閱廣播消息 stompClient.subscribe('/topic/broadcast', function (message) { console.log('收到廣播: ' + message.body); }); // 訂閱特定聊天室 stompClient.subscribe('/topic/chat/room1', function (message) { const chatMessage = JSON.parse(message.body); console.log('聊天消息: ' + chatMessage.content); }); // 訂閱私人消息 stompClient.subscribe('/user/queue/messages', function (message) { console.log('收到私人消息: ' + message.body); }); // 發(fā)送消息到聊天室 stompClient.publish({ destination: '/app/chat/room1', body: JSON.stringify({ sender: 'user123', content: '大家好!', timestamp: new Date() }) }); // 發(fā)送私人消息 stompClient.publish({ destination: '/app/private-message', body: JSON.stringify({ sender: 'user123', recipient: 'user456', content: '你好,這是一條私信', timestamp: new Date() }) }); }; stompClient.onStompError = function (frame) { console.error('STOMP錯誤: ' + frame.headers['message']); console.error('Additional details: ' + frame.body); }; stompClient.activate();
優(yōu)點:
- 高級消息模式:主題訂閱、點對點消息傳遞
- 內(nèi)置消息代理,簡化消息路由
- 支持消息確認和事務(wù)
- 框架支持完善,SpringBoot集成度高
- 支持認證和授權(quán)
缺點:
- 學(xué)習(xí)曲線較陡
- 資源消耗較高
- 配置相對復(fù)雜
三、方案對比與選擇建議
方案 | 實時性 | 雙向通信 | 資源消耗 | 實現(xiàn)復(fù)雜度 | 瀏覽器兼容性 |
---|---|---|---|---|---|
短輪詢 | 低 | 否 | 高 | 低 | 極好 |
長輪詢 | 中 | 否 | 中 | 中 | 好 |
SSE | 高 | 否(單向) | 低 | 低 | IE不支持 |
WebSocket | 極高 | 是 | 低 | 高 | 良好(需考慮兼容) |
STOMP | 極高 | 是 | 中 | 高 | 良好(需考慮兼容) |
選擇建議:
- 簡單通知場景:對實時性要求不高,可以選擇短輪詢或長輪詢
- 服務(wù)器單向推送數(shù)據(jù):如實時數(shù)據(jù)展示、通知提醒等,推薦使用SSE
- 實時性要求高且需雙向通信:如聊天應(yīng)用、在線游戲等,應(yīng)選擇WebSocket
- 復(fù)雜消息傳遞需求:如需要主題訂閱、點對點消息、消息確認等功能,推薦使用STOMP
- 需要考慮老舊瀏覽器:應(yīng)避免使用SSE和WebSocket,或提供降級方案
四、總結(jié)
在SpringBoot中實現(xiàn)網(wǎng)頁消息推送,有多種技術(shù)方案可選,每種方案都有其適用場景:
- 短輪詢:最簡單但效率最低,適合非實時性要求的場景
- 長輪詢:改進版的輪詢,降低了服務(wù)器負載,提高了實時性
- SSE:輕量級的服務(wù)器推送技術(shù),適合單向通信場景
- WebSocket:功能最強大的雙向通信方案,適合高實時性要求場景
- STOMP:基于WebSocket的消息協(xié)議,提供了更高級的消息傳遞功能
選擇合適的推送技術(shù)需要根據(jù)業(yè)務(wù)需求、性能要求和瀏覽器兼容性等因素綜合考慮。在實際應(yīng)用中,也可以結(jié)合多種技術(shù),提供優(yōu)雅降級方案,確保在各種環(huán)境下都能提供良好的用戶體驗。
以上就是SpringBoot實現(xiàn)網(wǎng)頁消息推送的5種方法小結(jié)的詳細內(nèi)容,更多關(guān)于SpringBoot網(wǎng)頁消息推送的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實現(xiàn)支付寶之第三方支付寶即時到賬支付功能
這篇文章主要介紹了Java實現(xiàn)支付寶之第三方支付寶即時到賬支付功能的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-07-07java中關(guān)于getProperties方法的使用
這篇文章主要介紹了java中關(guān)于getProperties方法的使用,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12SpringBoot集成Spring Security的方法
Spring security,是一個強大的和高度可定制的身份驗證和訪問控制框架。這篇文章主要介紹了SpringBoot集成Spring Security的操作方法,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07SpringBoot快速集成jxls-poi(自定義模板,支持本地文件導(dǎo)出,在線文件導(dǎo)出)
這篇文章主要介紹了SpringBoot快速集成jxls-poi(自定義模板,支持本地文件導(dǎo)出,在線文件導(dǎo)出),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09Java編程實現(xiàn)基于TCP協(xié)議的Socket聊天室示例
這篇文章主要介紹了Java編程實現(xiàn)基于TCP協(xié)議的Socket聊天室,結(jié)合實例形式詳細分析了java基于TCP協(xié)議的Socket聊天室客戶端與服務(wù)器端相關(guān)實現(xiàn)與使用技巧,需要的朋友可以參考下2018-01-01