SpringBoot实现消息推送:让服务器学会“主动搭讪”

想象一下这个场景:你的APP像个腼腆的男生,只会傻傻等着用户来“敲门”(刷新页面),而不知道主动说“嘿,我有新消息给你!”这多尴尬啊!消息推送就像给服务器装了社交牛逼症,让它能从幕后跳出来大喊:“注意!有热乎的消息!”

一、推送技术选型:给服务器装上“大喇叭”

SSE(Server-Sent Events) - 像单相思,服务器可以一直对客户端叨叨叨,但客户端只能听着 WebSocket - 像热恋中的情侣,双方可以随时互发消息 轮询(Polling) - 像查岗的女朋友,隔几秒就问一次“有新消息吗?” 长轮询(Long Polling) - 像有耐心的女朋友,等不到消息就不挂电话

今天咱们重点玩一下SSE,因为它简单直接,就像给服务器装了个校园广播站!

二、SpringBoot推送实战:三步搞定

第一步:添加依赖(给项目喂点“能量饮料”)


xml

<!-- pom.xml --> <dependencies> <!-- SpringBoot基础套餐 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 给模板引擎加点料 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- 让我们能处理异步请求 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> </dependencies>

第二步:创建SSE控制器(服务器的“播音室”)


kotlin

package com.example.pushdemo.controller; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.concurrent.CopyOnWriteArrayList; @RestController @RequestMapping("/sse") public class SseController { // 保存所有连接的客户端,CopyOnWriteArrayList是线程安全的 private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>(); /** * 客户端连接入口 - 相当于打开收音机 * @return SseEmitter对象 */ @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter connect() { // 设置连接超时时间(毫秒),0表示永不超时 SseEmitter emitter = new SseEmitter(0L); // 连接建立时的处理 emitter.onCompletion(() -> { System.out.println("客户端断开连接了,有点小失落..."); emitters.remove(emitter); }); emitter.onTimeout(() -> { System.out.println("客户端连接超时了,是不是网不好?"); emitters.remove(emitter); }); // 添加到连接列表 emitters.add(emitter); try { // 发送欢迎消息 emitter.send(SseEmitter.event() .name("welcome") // 事件名称 .data("连接成功!我是话痨服务器,我会主动给你推送消息!")); } catch (IOException e) { emitter.completeWithError(e); } System.out.println("新客户端加入,当前连接数:" + emitters.size()); return emitter; } /** * 向所有客户端广播消息 - 服务器开始广播啦! * @param message 要推送的消息 * @return 推送结果 */ @PostMapping("/broadcast") public String broadcast(@RequestParam String message) { System.out.println("准备广播消息:" + message); int successCount = 0; // 遍历所有连接 for (SseEmitter emitter : emitters) { try { // 发送消息 emitter.send(SseEmitter.event() .name("message") // 事件类型 .data(message + " - " + System.currentTimeMillis())); successCount++; System.out.println("消息已推送给客户端:" + emitter); } catch (IOException e) { System.out.println("推送失败,移除失效连接"); emitters.remove(emitter); } } return String.format("广播完成!成功推送给 %d/%d 个客户端", successCount, emitters.size()); } /** * 发送系统通知 */ @PostMapping("/system-notice") public String sendSystemNotice(@RequestParam String notice) { for (SseEmitter emitter : emitters) { try { emitter.send(SseEmitter.event() .name("system") .data("系统通知:" + notice)); } catch (IOException e) { // 静默处理错误连接 } } return "系统通知已发送"; } }

第三步:创建WebSocket配置(备选方案,双向通信)


kotlin

package com.example.pushdemo.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new MyWebSocketHandler(), "/ws") .setAllowedOrigins("*"); // 生产环境记得限制域名哦! } }


java

package com.example.pushdemo.config; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; import java.io.IOException; import java.util.concurrent.CopyOnWriteArraySet; public class MyWebSocketHandler extends TextWebSocketHandler { private static final CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<>(); @Override public void afterConnectionEstablished(WebSocketSession session) { sessions.add(session); System.out.println("WebSocket连接建立,当前连接数:" + sessions.size()); try { session.sendMessage(new TextMessage("💬 欢迎来到WebSocket聊天室!")); } catch (IOException e) { e.printStackTrace(); } } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 广播收到的消息 String payload = message.getPayload(); System.out.println("收到消息:" + payload); for (WebSocketSession s : sessions) { if (s.isOpen()) { s.sendMessage(new TextMessage("用户说:" + payload)); } } } @Override public void afterConnectionClosed(WebSocketSession session, org.springframework.web.socket.CloseStatus status) { sessions.remove(session); System.out.println("WebSocket连接关闭"); } }

第四步:创建HTML测试页面(给客户端配个"收音机")


xml

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>消息推送测试 - 服务器的碎碎念</title> <style> body { font-family: 'Microsoft YaHei', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); } .container { background: white; border-radius: 15px; padding: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); } h1 { color: #2c3e50; text-align: center; margin-bottom: 30px; } .card { background: #f8f9fa; border-radius: 10px; padding: 20px; margin: 20px 0; border-left: 5px solid #3498db; } .message-area { height: 300px; overflow-y: auto; border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin: 20px 0; background: #fff; } .message { padding: 10px; margin: 10px 0; border-radius: 8px; animation: fadeIn 0.5s; } .system { background: #e3f2fd; } .welcome { background: #e8f5e9; } .user { background: #fff3e0; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } button { background: #3498db; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; transition: all 0.3s; margin: 5px; } button:hover { background: #2980b9; transform: translateY(-2px); } .btn-danger { background: #e74c3c; } .btn-success { background: #2ecc71; } .input-group { display: flex; gap: 10px; margin: 20px 0; } input { flex: 1; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 16px; } input:focus { border-color: #3498db; outline: none; } </style> </head> <body> <div class="container"> <h1>服务器广播站测试</h1> <div class="card"> <h3>连接状态</h3> <p id="status">准备连接服务器...</p> <button onclick="connectSSE()">连接SSE服务器</button> <button onclick="connectWebSocket()">连接WebSocket</button> <button class="btn-danger" onclick="disconnect()">断开连接</button> </div> <div class="card"> <h3>消息测试</h3> <div class="input-group"> <input type="text" id="messageInput" placeholder="输入要广播的消息..." /> <button onclick="sendBroadcast()">广播消息</button> <button class="btn-success" onclick="sendSystemNotice()"> 发送系统通知 </button> </div> </div> <div class="card"> <h3>收到的消息</h3> <div id="messageArea" class="message-area"></div> <button onclick="clearMessages()">清空消息</button> <span id="counter">消息数量: 0</span> </div> </div> <script> let eventSource = null; let ws = null; let messageCount = 0; // 添加消息到显示区域 function addMessage(content, type = 'system') { const area = document.getElementById('messageArea'); const message = document.createElement('div'); message.className = `message ${type}`; message.innerHTML = ` <strong>[${new Date().toLocaleTimeString()}]</strong> <span>${content}</span> `; area.appendChild(message); area.scrollTop = area.scrollHeight; messageCount++; document.getElementById('counter').textContent = `消息数量: ${messageCount}`; } // 连接SSE function connectSSE() { if (eventSource) { addMessage('已经连接过了,别着急嘛!'); return; } eventSource = new EventSource('/sse/connect'); eventSource.onopen = () => { document.getElementById('status').innerHTML = 'SSE连接成功!服务器现在可以主动推送消息了'; addMessage('SSE连接已建立', 'welcome'); }; // 监听不同类型的消息 eventSource.addEventListener('welcome', (e) => { addMessage(e.data, 'welcome'); }); eventSource.addEventListener('message', (e) => { addMessage(`收到广播: ${e.data}`, 'user'); }); eventSource.addEventListener('system', (e) => { addMessage(e.data, 'system'); }); eventSource.onerror = (e) => { document.getElementById('status').innerHTML = 'SSE连接出错,尝试重连中...'; console.error('SSE错误:', e); // 3秒后重连 setTimeout(() => { if (eventSource.readyState === EventSource.CLOSED) { disconnect(); connectSSE(); } }, 3000); }; } // 连接WebSocket function connectWebSocket() { if (ws && ws.readyState === WebSocket.OPEN) { addMessage('WebSocket已经连接了!'); return; } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host; ws = new WebSocket(`${protocol}//${host}/ws`); ws.onopen = () => { document.getElementById('status').innerHTML = 'WebSocket连接成功!可以双向通信了'; addMessage('WebSocket连接已建立', 'welcome'); }; ws.onmessage = (e) => { addMessage(`WebSocket消息: ${e.data}`, 'user'); }; ws.onerror = (e) => { addMessage('WebSocket连接错误', 'system'); }; ws.onclose = () => { document.getElementById('status').innerHTML = 'WebSocket连接已关闭'; }; } // 发送广播消息 async function sendBroadcast() { const input = document.getElementById('messageInput'); const message = input.value.trim(); if (!message) { addMessage('请输入要发送的消息!', 'system'); return; } try { const response = await fetch('/sse/broadcast', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: `message=${encodeURIComponent(message)}` }); const result = await response.text(); addMessage(`${result}`, 'system'); input.value = ''; } catch (error) { addMessage('发送失败: ' + error, 'system'); } } // 发送系统通知 async function sendSystemNotice() { const notice = prompt('请输入系统通知内容:', '服务器即将维护'); if (!notice) return; try { const response = await fetch(`/sse/system-notice?notice=${encodeURIComponent(notice)}`, { method: 'POST' }); const result = await response.text(); addMessage(`${result}`, 'system'); } catch (error) { addMessage('发送系统通知失败', 'system'); } } // 断开连接 function disconnect() { if (eventSource) { eventSource.close(); eventSource = null; addMessage('SSE连接已关闭', 'system'); } if (ws) { ws.close(); ws = null; addMessage('WebSocket连接已关闭', 'system'); } document.getElementById('status').innerHTML = '连接已断开'; } // 清空消息 function clearMessages() { document.getElementById('messageArea').innerHTML = ''; messageCount = 0; document.getElementById('counter').textContent = '消息数量: 0'; addMessage('消息已清空', 'system'); } // 页面加载时的小动画 window.onload = () => { addMessage('消息推送演示系统已启动', 'welcome'); addMessage('试试点击"连接SSE服务器"按钮开始体验吧!', 'system'); }; // 监听键盘事件 document.getElementById('messageInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') { sendBroadcast(); } }); </script> </body> </html>

三、进阶优化:让推送更"聪明"

1. 连接管理器(专业版)


scss

@Component public class ConnectionManager { private final Map<String, SseEmitter> userConnections = new ConcurrentHashMap<>(); private final Map<String, String> connectionUserMap = new ConcurrentHashMap<>(); /** * 添加用户连接 */ public SseEmitter addConnection(String userId) { // 移除旧连接(避免重复登录) if (userConnections.containsKey(userId)) { userConnections.get(userId).complete(); } SseEmitter emitter = new SseEmitter(30 * 60 * 1000L); // 30分钟超时 emitter.onCompletion(() -> removeConnection(userId)); emitter.onTimeout(() -> removeConnection(userId)); userConnections.put(userId, emitter); // 发送连接成功消息 try { emitter.send(SseEmitter.event() .name("connected") .data("用户 " + userId + " 连接成功!")); } catch (IOException e) { removeConnection(userId); } return emitter; } /** * 向指定用户推送消息 */ public void pushToUser(String userId, String message) { SseEmitter emitter = userConnections.get(userId); if (emitter != null) { try { emitter.send(SseEmitter.event() .data(message)); } catch (IOException e) { removeConnection(userId); } } } /** * 向所有用户广播 */ public void broadcast(String message) { userConnections.forEach((userId, emitter) -> { try { emitter.send(SseEmitter.event() .data(message)); } catch (IOException e) { removeConnection(userId); } }); } private void removeConnection(String userId) { userConnections.remove(userId); System.out.println("用户 " + userId + " 的连接已移除"); } }

2. 心跳检测(保持连接活跃)


typescript

@Component public class HeartbeatScheduler { @Autowired private ConnectionManager connectionManager; @Scheduled(fixedRate = 25000) // 每25秒发送一次心跳 public void sendHeartbeat() { connectionManager.broadcast("心跳检测 - " + new Date()); } }

四、不同方案的对比总结

方案优点缺点适用场景
SSE简单易用、自动重连、HTTP协议友好只能服务器到客户端单向实时通知、新闻推送、股票行情
WebSocket双向通信、实时性最好实现复杂、需要额外协议聊天室、在线游戏、协同编辑
长轮询兼容性好、实现简单延迟高、服务器压力大兼容性要求高的老系统
短轮询极其简单、无状态实时性差、资源浪费更新频率低的应用

五、总结:让服务器"开口说话"的艺术

通过这次探索,我们给SpringBoot服务器装上了"嘴巴",让它学会了主动和客户端聊天!总结一下关键点:

  1. SSE是你的好朋友 - 对于服务器向客户端的单向推送,SSE简单到让人感动
  2. 连接管理很重要 - 记得及时清理断开的连接,不然服务器内存会"爆炸"
  3. 错误处理不能忘 - 网络世界充满了不确定性,要优雅地处理各种异常
  4. 心跳检测保活力 - 定期发送心跳,防止连接被防火墙误杀
  5. 生产环境要优化 - 记得添加认证、限流、集群支持等

推送消息就像谈恋爱,不能太频繁(用户会烦),也不能太冷淡(用户会跑),要掌握好节奏!而且千万别"已读不回",那比不推送还糟糕!

现在,你的服务器已经从"闷葫芦"变成了"社交达人",快去让它和客户端愉快地聊天吧!记住:好的推送系统,应该是用户感觉不到它的存在,但需要时它永远在那里!

如果你想让推送更有趣,可以添加表情包识别、消息优先级、智能推送时间等功能。毕竟,谁不喜欢一个会"察言观色"的服务器呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值