大家好,我是小悟。
想象一下这个场景:你的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服务器装上了"嘴巴",让它学会了主动和客户端聊天!总结一下关键点:
- SSE是你的好朋友 - 对于服务器向客户端的单向推送,SSE简单到让人感动
- 连接管理很重要 - 记得及时清理断开的连接,不然服务器内存会"爆炸"
- 错误处理不能忘 - 网络世界充满了不确定性,要优雅地处理各种异常
- 心跳检测保活力 - 定期发送心跳,防止连接被防火墙误杀
- 生产环境要优化 - 记得添加认证、限流、集群支持等
推送消息就像谈恋爱,不能太频繁(用户会烦),也不能太冷淡(用户会跑),要掌握好节奏!而且千万别"已读不回",那比不推送还糟糕!
现在,你的服务器已经从"闷葫芦"变成了"社交达人",快去让它和客户端愉快地聊天吧!记住:好的推送系统,应该是用户感觉不到它的存在,但需要时它永远在那里!
如果你想让推送更有趣,可以添加表情包识别、消息优先级、智能推送时间等功能。毕竟,谁不喜欢一个会"察言观色"的服务器呢?

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海