71岁的陈佩斯,把北京昌平的别墅给抵押了,就为了这部《戏台》。这事儿不容易。电影筹备了快八年,剧本阶段投资方就跑了三家,6000万资金说没就没。剧组差...
2025-07-27 0
项目开发中,实时消息推送已成为提升用户体验的关键技术。无论是聊天应用、通知系统、实时数据展示,还是协同办公场景,都需要服务器能够主动向客户端推送消息。本文将详细介绍SpringBoot中实现网页消息推送的几种主流方案,帮助开发者根据实际需求选择最合适的技术。
传统的HTTP请求是客户端主动请求,服务端被动响应的模式。但在很多场景下,我们需要服务器能够主动将消息推送给浏览器,例如:
原理:客户端以固定的时间间隔频繁发送请求,询问服务器是否有新消息。
实现方式:
@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); }}
前端实现:
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秒查询一次}
优点:
缺点:
原理:客户端发送请求后,如果服务器没有新消息,则保持连接打开直到有新消息或超时,然后客户端立即发起新的请求。
实现方式:
@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); } }}
前端实现:
function longPolling() { fetch('/api/long-polling/user123') .then(response => response.json()) .then(messages => { if (messages.length > 0) { messages.forEach(msg => console.log(msg)); } // 立即发起下一次长轮询 longPolling(); }) .catch(() => { // 出错后延迟一下再重试 setTimeout(longPolling, 5000); });}
优点:
缺点:
原理:服务器与客户端建立单向连接,服务器可以持续向客户端推送数据,而不需要客户端重复请求。
SpringBoot实现:
@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)); // 发送一个初始事件保持连接 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("消息已发送"); } catch (IOException e) { emitters.remove(userId); return ResponseEntity.internalServerError().body("发送失败"); } } 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("广播消息已发送"); }}
前端实现:
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(); // 可以在这里实现重连逻辑 setTimeout(connectSSE, 5000); };}
优点:
缺点:
原理:WebSocket是一种双向通信协议,在单个TCP连接上提供全双工通信通道。
SpringBoot配置:
@Configuration@EnableWebSocketpublic 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=", ""); } // 发送消息给指定用户 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); } } }); }}
前端实现:
function connectWebSocket() { const socket = new WebSocket('ws://localhost:8080/ws/messages?userId=user123'); socket.onopen = function() { console.log('WebSocket连接已建立'); // 可以发送一条消息 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连接已关闭'); // 可以在这里实现重连逻辑 setTimeout(connectWebSocket, 5000); }; socket.onerror = function(error) { console.error('WebSocket错误:', error); socket.close(); };}
优点:
缺点:
原理:STOMP (Simple Text Oriented Messaging Protocol) 是一个基于WebSocket的简单消息传递协议,提供了更高级的消息传递模式。
SpringBoot配置:
@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 启用简单的基于内存的消息代理 registry.enableSimpleBroker("/topic", "/queue"); // 设置应用的前缀 registry.setApplicationDestinationPrefixes("/app"); // 设置用户目的地前缀 registry.setUserDestinationPrefix("/user"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") .setAllowedOrigins("*") .withSockJS(); // 添加SockJS支持 }}@Controllerpublic class MessageController { private final SimpMessagingTemplate messagingTemplate; public MessageController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } // 处理客户端发送到/app/sendMessage的消息 @MessageMapping("/sendMessage") public void processMessage(String message) { // 处理消息... } // 处理客户端发送到/app/chat/{roomId}的消息,并广播到相应的聊天室 @MessageMapping("/chat/{roomId}") @SendTo("/topic/chat/{roomId}") public ChatMessage chat(@DestinationVariable String roomId, ChatMessage message) { // 处理聊天消息... return message; } // 发送私人消息 @MessageMapping("/private-message") public void privateMessage(PrivateMessage message) { messagingTemplate.convertAndSendToUser( message.getRecipient(), // 接收者的用户名 "/queue/messages", // 目的地 message // 消息内容 ); } // REST API发送广播消息 @PostMapping("/api/broadcast") public ResponseEntity<String> broadcast(@RequestBody String message) { messagingTemplate.convertAndSend("/topic/broadcast", message); return ResponseEntity.ok("消息已广播"); } // REST API发送私人消息 @PostMapping("/api/private-message/{userId}") public ResponseEntity<String> sendPrivateMessage( @PathVariable String userId, @RequestBody String message) { messagingTemplate.convertAndSendToUser(userId, "/queue/messages", message); return ResponseEntity.ok("私人消息已发送"); }}
前端实现:
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); }); // 发送消息到聊天室 stompClient.publish({ destination: '/app/chat/room1', body: JSON.stringify({ sender: 'user123', content: '大家好!', timestamp: new Date() }) }); // 发送私人消息 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();
优点:
缺点:
方案 | 实时性 | 双向通信 | 资源消耗 | 实现复杂度 | 浏览器兼容性 |
短轮询 | 低 | 否 | 高 | 低 | 极好 |
长轮询 | 中 | 否 | 中 | 中 | 好 |
SSE | 高 | 否(单向) | 低 | 低 | IE不支持 |
WebSocket | 极高 | 是 | 低 | 高 | 良好(需考虑兼容) |
STOMP | 极高 | 是 | 中 | 高 | 良好(需考虑兼容) |
选择建议:
在SpringBoot中实现网页消息推送,有多种技术方案可选,每种方案都有其适用场景:
选择合适的推送技术需要根据业务需求、性能要求和浏览器兼容性等因素综合考虑。在实际应用中,也可以结合多种技术,提供优雅降级方案,确保在各种环境下都能提供良好的用户体验。
相关文章
71岁的陈佩斯,把北京昌平的别墅给抵押了,就为了这部《戏台》。这事儿不容易。电影筹备了快八年,剧本阶段投资方就跑了三家,6000万资金说没就没。剧组差...
2025-07-27 0
最近双色球25084期开奖了,红球是06、08、10、14、24、33,蓝球15。一等奖爆出六注,每注奖金860万,北京、内蒙古这些地方的人运气不错。...
2025-07-27 0
2015年,当Apple Watch首次与消费者见面时,或许很少有人能预料到这款可穿戴设备,会在接下来的十年间融入并改变更多人的生活。从最初的时间显示...
2025-07-27 0
源自丨中建六局八建公司为强化人员对工艺工序的管控,提升施工质量水平,公司推出标准化工艺做法展播,本期为大家带来《厂房超大高精地坪智能施工工艺》。在实际...
2025-07-27 0
在人工智能飞速发展的今天,语言模型已经成为我们日常生活中不可或缺的一部分。不过,你可能不知道的是,现在的AI系统在理解文本时都需要一个叫做"分词器"的...
2025-07-27 0
在配电系统的规划与设计中,断路器的合理选型是保障电力系统安全稳定运行的核心环节。其选型需综合考量使用类别、额定工作电压、额定电流、脱扣器整定电流等多维...
2025-07-27 0
7月18日,备受期待的电影《长安的荔枝》迎来全球首映,短短几日,其票房已经突破两亿。作为冰箱行业独家合作伙伴,容声冰箱携其明星产品W60系列亮相首映现...
2025-07-27 0
河北邯郸险峰渡槽,就是世界最大的石拱单跨渡槽。说他是世界最大,不单单指主拱跨度长达106米,而是像这种难度的地型,在没有现代机械,没有原材料保证的情况...
2025-07-27 0
发表评论