SpringBoot實(shí)現(xiàn)網(wǎng)頁(yè)消息推送的5種方法小結(jié)
項(xiàng)目開(kāi)發(fā)中,實(shí)時(shí)消息推送已成為提升用戶體驗(yàn)的關(guān)鍵技術(shù)。無(wú)論是聊天應(yīng)用、通知系統(tǒng)、實(shí)時(shí)數(shù)據(jù)展示,還是協(xié)同辦公場(chǎng)景,都需要服務(wù)器能夠主動(dòng)向客戶端推送消息。本文將詳細(xì)介紹SpringBoot中實(shí)現(xiàn)網(wǎng)頁(yè)消息推送的幾種主流方案,幫助開(kāi)發(fā)者根據(jù)實(shí)際需求選擇最合適的技術(shù)。
一、為什么需要消息推送
傳統(tǒng)的HTTP請(qǐng)求是客戶端主動(dòng)請(qǐng)求,服務(wù)端被動(dòng)響應(yīng)的模式。但在很多場(chǎng)景下,我們需要服務(wù)器能夠主動(dòng)將消息推送給瀏覽器,例如:
- Web版即時(shí)通訊
- 股票、基金等金融數(shù)據(jù)實(shí)時(shí)更新
- 系統(tǒng)通知和提醒
- 協(xié)同編輯文檔時(shí)的實(shí)時(shí)更新
- ......
二、消息推送實(shí)現(xiàn)方案
1. 短輪詢 (Short Polling)
原理:客戶端以固定的時(shí)間間隔頻繁發(fā)送請(qǐng)求,詢問(wèn)服務(wù)器是否有新消息。
實(shí)現(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);
}
}
前端實(shí)現(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)點(diǎn):
- 實(shí)現(xiàn)簡(jiǎn)單,不需要特殊的服務(wù)器配置
- 兼容性好,支持幾乎所有瀏覽器和服務(wù)器
缺點(diǎn):
- 資源消耗大,大量無(wú)效請(qǐng)求
- 實(shí)時(shí)性較差,受輪詢間隔影響
- 服務(wù)器負(fù)載高,尤其是在用戶量大的情況下
2. 長(zhǎng)輪詢 (Long Polling)
原理:客戶端發(fā)送請(qǐng)求后,如果服務(wù)器沒(méi)有新消息,則保持連接打開(kāi)直到有新消息或超時(shí),然后客戶端立即發(fā)起新的請(qǐng)求。
實(shí)現(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 {
// 沒(méi)有消息,等待
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) {
// 查看是否有等待的請(qǐng)求
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 {
// 存儲(chǔ)消息,等待下一次輪詢
pendingMessages.computeIfAbsent(userId, k -> new ArrayList<>()).add(message);
}
}
}
前端實(shí)現(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ā)起下一次長(zhǎng)輪詢
longPolling();
})
.catch(() => {
// 出錯(cuò)后延遲一下再重試
setTimeout(longPolling, 5000);
});
}
優(yōu)點(diǎn):
- 減少無(wú)效請(qǐng)求,相比短輪詢更高效
- 近實(shí)時(shí)體驗(yàn),有消息時(shí)立即推送
- 兼容性好,幾乎所有瀏覽器都支持
缺點(diǎn):
- 服務(wù)器資源消耗,大量連接會(huì)占用服務(wù)器資源
- 可能受超時(shí)限制
- 難以處理服務(wù)器主動(dòng)推送的場(chǎng)景
3. Server-Sent Events (SSE)
原理:服務(wù)器與客戶端建立單向連接,服務(wù)器可以持續(xù)向客戶端推送數(shù)據(jù),而不需要客戶端重復(fù)請(qǐng)求。
SpringBoot實(shí)現(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ā)送一個(gè)初始事件保持連接
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ā)送");
}
}
前端實(shí)現(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();
// 可以在這里實(shí)現(xiàn)重連邏輯
setTimeout(connectSSE, 5000);
};
}
優(yōu)點(diǎn):
- 真正的服務(wù)器推送,節(jié)省資源
- 自動(dòng)重連機(jī)制
- 支持事件類型區(qū)分
- 相比WebSocket更輕量
缺點(diǎn):
- 單向通信,客戶端無(wú)法通過(guò)SSE向服務(wù)器發(fā)送數(shù)據(jù)
- 連接數(shù)限制,瀏覽器對(duì)同一域名的SSE連接數(shù)有限制
- IE瀏覽器不支持
4. WebSocket
原理:WebSocket是一種雙向通信協(xié)議,在單個(gè)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);
}
}
});
}
}
前端實(shí)現(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)閉');
// 可以在這里實(shí)現(xiàn)重連邏輯
setTimeout(connectWebSocket, 5000);
};
socket.onerror = function(error) {
console.error('WebSocket錯(cuò)誤:', error);
socket.close();
};
}
優(yōu)點(diǎn):
- 全雙工通信,服務(wù)器和客戶端可以隨時(shí)相互發(fā)送數(shù)據(jù)
- 實(shí)時(shí)性最好,延遲最低
- 效率高,建立連接后無(wú)需HTTP頭,數(shù)據(jù)傳輸量小
- 支持二進(jìn)制數(shù)據(jù)
缺點(diǎn):
- 實(shí)現(xiàn)相對(duì)復(fù)雜
- 對(duì)服務(wù)器要求高,需要處理大量并發(fā)連接
- 可能受到防火墻限制
5. STOMP (基于WebSocket)
原理:STOMP (Simple Text Oriented Messaging Protocol) 是一個(gè)基于WebSocket的簡(jiǎn)單消息傳遞協(xié)議,提供了更高級(jí)的消息傳遞模式。
SpringBoot配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 啟用簡(jiǎn)單的基于內(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ā)送");
}
}
前端實(shí)現(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錯(cuò)誤: ' + frame.headers['message']);
console.error('Additional details: ' + frame.body);
};
stompClient.activate();
優(yōu)點(diǎn):
- 高級(jí)消息模式:主題訂閱、點(diǎn)對(duì)點(diǎn)消息傳遞
- 內(nèi)置消息代理,簡(jiǎn)化消息路由
- 支持消息確認(rèn)和事務(wù)
- 框架支持完善,SpringBoot集成度高
- 支持認(rèn)證和授權(quán)
缺點(diǎn):
- 學(xué)習(xí)曲線較陡
- 資源消耗較高
- 配置相對(duì)復(fù)雜
三、方案對(duì)比與選擇建議
| 方案 | 實(shí)時(shí)性 | 雙向通信 | 資源消耗 | 實(shí)現(xiàn)復(fù)雜度 | 瀏覽器兼容性 |
|---|---|---|---|---|---|
| 短輪詢 | 低 | 否 | 高 | 低 | 極好 |
| 長(zhǎng)輪詢 | 中 | 否 | 中 | 中 | 好 |
| SSE | 高 | 否(單向) | 低 | 低 | IE不支持 |
| WebSocket | 極高 | 是 | 低 | 高 | 良好(需考慮兼容) |
| STOMP | 極高 | 是 | 中 | 高 | 良好(需考慮兼容) |
選擇建議:
- 簡(jiǎn)單通知場(chǎng)景:對(duì)實(shí)時(shí)性要求不高,可以選擇短輪詢或長(zhǎng)輪詢
- 服務(wù)器單向推送數(shù)據(jù):如實(shí)時(shí)數(shù)據(jù)展示、通知提醒等,推薦使用SSE
- 實(shí)時(shí)性要求高且需雙向通信:如聊天應(yīng)用、在線游戲等,應(yīng)選擇WebSocket
- 復(fù)雜消息傳遞需求:如需要主題訂閱、點(diǎn)對(duì)點(diǎn)消息、消息確認(rèn)等功能,推薦使用STOMP
- 需要考慮老舊瀏覽器:應(yīng)避免使用SSE和WebSocket,或提供降級(jí)方案
四、總結(jié)
在SpringBoot中實(shí)現(xiàn)網(wǎng)頁(yè)消息推送,有多種技術(shù)方案可選,每種方案都有其適用場(chǎng)景:
- 短輪詢:最簡(jiǎn)單但效率最低,適合非實(shí)時(shí)性要求的場(chǎng)景
- 長(zhǎng)輪詢:改進(jìn)版的輪詢,降低了服務(wù)器負(fù)載,提高了實(shí)時(shí)性
- SSE:輕量級(jí)的服務(wù)器推送技術(shù),適合單向通信場(chǎng)景
- WebSocket:功能最強(qiáng)大的雙向通信方案,適合高實(shí)時(shí)性要求場(chǎng)景
- STOMP:基于WebSocket的消息協(xié)議,提供了更高級(jí)的消息傳遞功能
選擇合適的推送技術(shù)需要根據(jù)業(yè)務(wù)需求、性能要求和瀏覽器兼容性等因素綜合考慮。在實(shí)際應(yīng)用中,也可以結(jié)合多種技術(shù),提供優(yōu)雅降級(jí)方案,確保在各種環(huán)境下都能提供良好的用戶體驗(yàn)。
以上就是SpringBoot實(shí)現(xiàn)網(wǎng)頁(yè)消息推送的5種方法小結(jié)的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot網(wǎng)頁(yè)消息推送的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實(shí)現(xiàn)支付寶之第三方支付寶即時(shí)到賬支付功能
這篇文章主要介紹了Java實(shí)現(xiàn)支付寶之第三方支付寶即時(shí)到賬支付功能的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07
java中關(guān)于getProperties方法的使用
這篇文章主要介紹了java中關(guān)于getProperties方法的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
SpringBoot集成Spring Security的方法
Spring security,是一個(gè)強(qiáng)大的和高度可定制的身份驗(yàn)證和訪問(wèn)控制框架。這篇文章主要介紹了SpringBoot集成Spring Security的操作方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07
SpringBoot快速集成jxls-poi(自定義模板,支持本地文件導(dǎo)出,在線文件導(dǎo)出)
這篇文章主要介紹了SpringBoot快速集成jxls-poi(自定義模板,支持本地文件導(dǎo)出,在線文件導(dǎo)出),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
詳解java一維數(shù)組及練習(xí)題實(shí)例
在本篇文章里小編給大家整理了關(guān)于java一維數(shù)組及練習(xí)題的相關(guān)知識(shí)點(diǎn)和實(shí)例代碼,有需要的朋友們跟著學(xué)習(xí)下。2019-07-07
Spring中的事務(wù)管理及實(shí)現(xiàn)方式解析
這篇文章主要介紹了Spring中的事務(wù)管理及實(shí)現(xiàn)方式解析,Spring事務(wù)管理基于底層數(shù)據(jù)庫(kù)本身的事務(wù)處理機(jī)制,數(shù)據(jù)庫(kù)事務(wù)的基礎(chǔ),是掌握Spring事務(wù)管理的基礎(chǔ),這篇總結(jié)下Spring事務(wù),需要的朋友可以參考下2024-01-01
Java編程實(shí)現(xiàn)基于TCP協(xié)議的Socket聊天室示例
這篇文章主要介紹了Java編程實(shí)現(xiàn)基于TCP協(xié)議的Socket聊天室,結(jié)合實(shí)例形式詳細(xì)分析了java基于TCP協(xié)議的Socket聊天室客戶端與服務(wù)器端相關(guān)實(shí)現(xiàn)與使用技巧,需要的朋友可以參考下2018-01-01

