写在前面的话
你有没有遇到过这样的场景:正在开发一个在线客服系统,用户发送消息后,客服端需要实时收到通知;或者开发一个电商后台, 订单状态变化时管理员要立即知晓。这些需求的本质都指向一个问题:如何让服务器主动把数据推送给浏览器?
传统的HTTP请求都是"客户端问,服务器答"的模式,但现在我们需要反过来——服务器要主动开口说话。今天就来聊聊后端开发中常 见的6种消息推送方案,从最原始的轮询到现代化的WebSocket,看看它们各自的优劣势和适用场景。
方案一:短轮询——简单粗暴的定时查询
短轮询可以说是最朴素的解决方案,实现思路极其简单:前端每隔几秒就问一次服务器"有新消息吗?"
实现思路
服务端提供一个查询接口,客户端通过定时器反复调用。我们来看一个实际的例子:
Java后端实现(Spring Boot):
@RestController @RequestMapping("/api/notification") public class NotificationController {
@Autowired
private NotificationService notificationService;
/**
* 查询用户未读通知数量
*/
@GetMapping("/unread-count")
public ResponseEntity<NotificationDTO> getUnreadCount(@RequestParam Long userId) {
int unreadCount = notificationService.countUnreadByUserId(userId);
List<String> latestMessages = notificationService.getLatestMessages(userId, 5);
NotificationDTO dto = new NotificationDTO();
dto.setUserId(userId);
dto.setUnreadCount(unreadCount);
dto.setMessages(latestMessages);
dto.setTimestamp(System.currentTimeMillis());
return ResponseEntity.ok(dto);
}
}
前端JavaScript实现:
// 每3秒轮询一次 const userId = localStorage.getItem('userId'); setInterval(async () => { try { const response = await fetch(/api/notification/unread-count?userId=${userId}); const data = await response.json();
if (data.unreadCount > 0) {
updateBadge(data.unreadCount);
showNotificationPreview(data.messages);
}
} catch (error) {
console.error('轮询失败:', error);
}
}, 3000);
方案评价
这种方案最大的优点是实现零门槛,后端就是一个普通的查询接口,前端加个定时器就搞定。但缺点也很明显:
-
资源浪费严重:即使没有新消息,请求也要发
-
实时性差:3秒的轮询间隔意味着消息可能延迟3秒才被看到
-
服务器压力大:1000个在线用户每3秒请求一次,QPS就是333
适用场景:开发环境快速验证、对实时性要求不高的内部系统。
方案二:长轮询——优雅的等待艺术
长轮询是对短轮询的改进,核心思想是:客户端发起请求后,如果服务端暂时没有新数据,不要立即返回,而是挂起请求等待,直 到有新数据或超时才响应。
深入实现
这里使用Spring的DeferredResult来实现异步响应:
@RestController @RequestMapping("/api/long-polling") public class LongPollingController {
// 存储每个用户的等待请求
private final Map<Long, DeferredResult<ResponseEntity<?>>> userRequests =
new ConcurrentHashMap<>();
// 超时时间:30秒
private static final long TIMEOUT = 30000L;
/**
* 长轮询接口
*/
@GetMapping("/wait-message")
public DeferredResult<ResponseEntity<?>> waitForMessage(@RequestParam Long userId) {
DeferredResult<ResponseEntity<?>> deferredResult =
new DeferredResult<>(TIMEOUT);
// 超时处理:返回304状态码,告诉前端继续轮询
deferredResult.onTimeout(() -> {
userRequests.remove(userId);
deferredResult.setResult(ResponseEntity.status(HttpStatus.NOT_MODIFIED).build());
});
// 请求完成时清理
deferredResult.onCompletion(() -> {
userRequests.remove(userId);
});
// 先检查是否有待推送的消息
Message pendingMessage = messageService.getPendingMessage(userId);
if (pendingMessage != null) {
deferredResult.setResult(ResponseEntity.ok(pendingMessage));
} else {
// 没有消息,挂起请求
userRequests.put(userId, deferredResult);
}
return deferredResult;
}
/**
* 当有新消息时,主动唤醒等待的请求
*/
public void pushMessage(Long userId, Message message) {
DeferredResult<ResponseEntity<?>> deferredResult = userRequests.get(userId);
if (deferredResult != null) {
deferredResult.setResult(ResponseEntity.ok(message));
userRequests.remove(userId);
} else {
// 用户没有在等待,消息存入待推送队列
messageService.savePendingMessage(userId, message);
}
}
}
配置异步支持:
@Configuration @EnableAsync public class AsyncConfig implements WebMvcConfigurer {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// 配置异步请求超时时间
configurer.setDefaultTimeout(30000);
// 配置异步请求线程池
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("long-polling-");
executor.initialize();
configurer.setTaskExecutor(executor);
}
}
前端实现(递归调用):
function startLongPolling(userId) { fetch(/api/long-polling/wait-message?userId=${userId}) .then(response => { if (response.status === 200) { return response.json(); } else if (response.status === 304) { // 超时,立即发起下一次请求 return null; } }) .then(data => { if (data) { // 处理收到的消息 handleNewMessage(data); } // 继续下一轮长轮询 startLongPolling(userId); }) .catch(error => { console.error('长轮询异常:', error); // 出错后延迟5秒重连 setTimeout(() => startLongPolling(userId), 5000); }); }
方案评价
长轮询在中间件领域应用广泛,比如Nacos配置中心、Apollo配置中心都采用这种方案。
优点:
-
减少了无效请求
-
相比短轮询更省资源
-
实时性有所提升
缺点:
-
仍然需要反复建立HTTP连接
-
服务端需要维护大量挂起的请求
-
对Web容器的并发能力有要求
适用场景:配置中心、消息队列消费者、对实时性要求适中的场景。
方案三:Server-Sent Events(SSE)——单向数据流的优雅方案
SSE是HTML5提供的服务器推送技术,它允许服务器通过HTTP连接向客户端发送事件流。ChatGPT的打字机效果就是用SSE实现的。
核心特性
SSE基于HTTP协议,但响应的Content-Type是text/event-stream,这是一个持久化的连接,服务器可以持续向客户端推送数据。
Java后端实现(Spring Boot):
@RestController @RequestMapping("/api/sse") @Slf4j public class SseController {
// 存储所有活跃的SSE连接
private final Map<Long, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
/**
* 客户端建立SSE连接
*/
@GetMapping("/connect")
public SseEmitter connect(@RequestParam Long userId) {
// 设置超时时间为0,表示永不超时
SseEmitter emitter = new SseEmitter(0L);
// 连接建立成功的回调
emitter.onCompletion(() -> {
log.info("用户{}的SSE连接已关闭", userId);
sseEmitters.remove(userId);
});
// 连接超时的回调
emitter.onTimeout(() -> {
log.warn("用户{}的SSE连接超时", userId);
sseEmitters.remove(userId);
});
// 连接异常的回调
emitter.onError(throwable -> {
log.error("用户{}的SSE连接异常", userId, throwable);
sseEmitters.remove(userId);
});
// 保存连接
sseEmitters.put(userId, emitter);
// 发送连接成功消息
try {
emitter.send(SseEmitter.event()
.name("connect")
.data("连接建立成功,用户ID: " + userId)
.build());
} catch (IOException e) {
log.error("发送连接消息失败", e);
}
return emitter;
}
/**
* 向指定用户推送消息
*/
public void pushToUser(Long userId, String eventName, Object data) {
SseEmitter emitter = sseEmitters.get(userId);
if (emitter != null) {
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(data)
.id(String.valueOf(System.currentTimeMillis()))
.build());
} catch (IOException e) {
log.error("向用户{}推送消息失败", userId, e);
sseEmitters.remove(userId);
}
}
}
/**
* 广播消息给所有在线用户
*/
public void broadcast(String eventName, Object data) {
List<Long> failedUsers = new ArrayList<>();
sseEmitters.forEach((userId, emitter) -> {
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(data)
.build());
} catch (IOException e) {
failedUsers.add(userId);
}
});
// 清理发送失败的连接
failedUsers.forEach(sseEmitters::remove);
}
/**
* 获取当前在线用户数
*/
@GetMapping("/online-count")
public int getOnlineCount() {
return sseEmitters.size();
}
}
业务层使用示例:
@Service public class OrderService {
@Autowired
private SseController sseController;
/**
* 订单状态变更时推送通知
*/
public void updateOrderStatus(Long orderId, Long userId, String newStatus) {
// 更新订单状态
orderRepository.updateStatus(orderId, newStatus);
// 通过SSE推送给用户
Map<String, Object> notification = new HashMap<>();
notification.put("orderId", orderId);
notification.put("status", newStatus);
notification.put("message", "您的订单状态已更新为:" + newStatus);
notification.put("timestamp", LocalDateTime.now());
sseController.pushToUser(userId, "order-update", notification);
}
}
前端实现:
let eventSource = null;
function connectSSE(userId) { // 创建SSE连接 eventSource = new EventSource(/api/sse/connect?userId=${userId});
// 监听连接建立事件
eventSource.addEventListener('connect', (e) => {
console.log('SSE连接建立:', e.data);
});
// 监听订单更新事件
eventSource.addEventListener('order-update', (e) => {
const data = JSON.parse(e.data);
showOrderNotification(data);
updateOrderList();
});
// 监听通用消息事件
eventSource.addEventListener('message', (e) => {
console.log('收到服务器消息:', e.data);
});
// 连接错误处理
eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
// SSE会自动重连,不需要手动处理
};
}
// 页面卸载时关闭连接 window.addEventListener('beforeunload', () => { if (eventSource) { eventSource.close(); } });
方案评价
SSE是一个被低估的技术,ChatGPT的成功让更多人认识到它的价值。
优点:
-
基于HTTP,无需额外协议支持
-
自动重连机制
-
服务端实现简单
-
支持自定义事件类型
-
比WebSocket更轻量
缺点:
-
只支持单向推送(服务器到客户端)
-
IE浏览器不支持
-
连接数受浏览器限制(通常每个域名6个)
适用场景:股票行情推送、直播弹幕、AI对话流式输出、系统通知推送。
方案四:WebSocket——全双工通信的王者
WebSocket是目前最成熟的双向通信方案,它在HTTP握手后升级为独立的TCP连接,可以实现客户端和服务器之间的真正双向实时通 信。
完整实现
-
引入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
-
WebSocket配置类:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private ChatWebSocketHandler chatWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatWebSocketHandler, "/ws/chat")
.setAllowedOrigins("*") // 生产环境需要配置具体域名
.addInterceptors(new HandshakeInterceptor() {
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
// 从请求中获取用户ID
String query = request.getURI().getQuery();
String userId = extractUserId(query);
attributes.put("userId", userId);
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception exception) {
}
});
}
private String extractUserId(String query) {
if (query != null && query.contains("userId=")) {
return query.split("userId=")[1].split("&")[0];
}
return null;
}
}
-
WebSocket处理器:
@Component @Slf4j public class ChatWebSocketHandler extends TextWebSocketHandler {
// 存储用户ID和WebSocket会话的映射
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
// 存储在线用户列表
private final Set<String> onlineUsers = ConcurrentHashMap.newKeySet();
/**
* 连接建立后
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String userId = (String) session.getAttributes().get("userId");
sessions.put(userId, session);
onlineUsers.add(userId);
log.info("用户{}建立WebSocket连接", userId);
// 发送欢迎消息
sendMessage(session, createMessage("system", "连接成功,欢迎来到聊天室"));
// 广播用户上线通知
broadcastUserStatus(userId, "online");
// 发送当前在线用户列表
sendOnlineUserList(session);
}
/**
* 收到客户端消息
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String userId = (String) session.getAttributes().get("userId");
String payload = message.getPayload();
log.info("收到用户{}的消息: {}", userId, payload);
// 解析消息
ChatMessage chatMessage = JSON.parseObject(payload, ChatMessage.class);
chatMessage.setSenderId(userId);
chatMessage.setTimestamp(LocalDateTime.now());
// 根据消息类型处理
switch (chatMessage.getType()) {
case "private":
// 私聊消息
sendToUser(chatMessage.getReceiverId(), chatMessage);
break;
case "group":
// 群聊消息
broadcastMessage(chatMessage);
break;
case "heartbeat":
// 心跳消息
sendMessage(session, createMessage("heartbeat", "pong"));
break;
default:
log.warn("未知的消息类型: {}", chatMessage.getType());
}
}
/**
* 连接关闭后
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String userId = (String) session.getAttributes().get("userId");
sessions.remove(userId);
onlineUsers.remove(userId);
log.info("用户{}断开WebSocket连接, 原因: {}", userId, status);
// 广播用户下线通知
broadcastUserStatus(userId, "offline");
}
/**
* 传输错误处理
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
String userId = (String) session.getAttributes().get("userId");
log.error("用户{}的连接发生错误", userId, exception);
if (session.isOpen()) {
session.close();
}
}
/**
* 向指定用户发送消息
*/
public void sendToUser(String userId, Object message) {
WebSocketSession session = sessions.get(userId);
if (session != null && session.isOpen()) {
try {
String json = JSON.toJSONString(message);
session.sendMessage(new TextMessage(json));
} catch (IOException e) {
log.error("向用户{}发送消息失败", userId, e);
}
}
}
/**
* 广播消息给所有在线用户
*/
public void broadcastMessage(Object message) {
String json = JSON.toJSONString(message);
sessions.values().forEach(session -> {
if (session.isOpen()) {
try {
session.sendMessage(new TextMessage(json));
} catch (IOException e) {
log.error("广播消息失败", e);
}
}
});
}
/**
* 广播用户状态变更
*/
private void broadcastUserStatus(String userId, String status) {
Map<String, Object> statusMessage = new HashMap<>();
statusMessage.put("type", "user-status");
statusMessage.put("userId", userId);
statusMessage.put("status", status);
statusMessage.put("onlineCount", onlineUsers.size());
broadcastMessage(statusMessage);
}
/**
* 发送在线用户列表
*/
private void sendOnlineUserList(WebSocketSession session) {
Map<String, Object> message = new HashMap<>();
message.put("type", "online-users");
message.put("users", new ArrayList<>(onlineUsers));
sendMessage(session, message);
}
private void sendMessage(WebSocketSession session, Object message) {
try {
session.sendMessage(new TextMessage(JSON.toJSONString(message)));
} catch (IOException e) {
log.error("发送消息失败", e);
}
}
private Map<String, Object> createMessage(String type, String content) {
Map<String, Object> message = new HashMap<>();
message.put("type", type);
message.put("content", content);
message.put("timestamp", System.currentTimeMillis());
return message;
}
}
-
前端实现:
class WebSocketClient { constructor(userId) { this.userId = userId; this.ws = null; this.heartbeatTimer = null; this.reconnectTimer = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; }
connect() {
const wsUrl = `ws://localhost:8080/ws/chat?userId=${this.userId}`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('WebSocket连接已建立');
this.reconnectAttempts = 0;
this.startHeartbeat();
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
this.ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
this.ws.onclose = () => {
console.log('WebSocket连接已关闭');
this.stopHeartbeat();
this.attemptReconnect();
};
}
handleMessage(message) {
switch (message.type) {
case 'private':
showPrivateMessage(message);
break;
case 'group':
showGroupMessage(message);
break;
case 'user-status':
updateUserStatus(message);
break;
case 'online-users':
updateOnlineUserList(message.users);
break;
case 'system':
showSystemMessage(message.content);
break;
}
}
sendMessage(type, content, receiverId = null) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const message = {
type: type,
content: content,
receiverId: receiverId
};
this.ws.send(JSON.stringify(message));
} else {
console.error('WebSocket未连接');
}
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
this.sendMessage('heartbeat', 'ping');
}, 30000); // 每30秒发送一次心跳
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
}
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
this.reconnectTimer = setTimeout(() => {
this.connect();
}, 3000 * this.reconnectAttempts);
} else {
console.error('重连失败,已达到最大重连次数');
}
}
disconnect() {
if (this.ws) {
this.ws.close();
}
this.stopHeartbeat();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
}
}
// 使用示例 const wsClient = new WebSocketClient('user123'); wsClient.connect();
// 发送私聊消息 wsClient.sendMessage('private', 'Hello!', 'user456');
// 发送群聊消息 wsClient.sendMessage('group', '大家好!');
方案评价
WebSocket是目前最强大的实时通信方案,在即时通讯、在线游戏、协同编辑等场景中应用广泛。
优点:
-
真正的全双工通信
-
性能极高,延迟极低
-
支持二进制数据传输
-
协议开销小
缺点:
-
实现复杂度较高
-
需要考虑断线重连、心跳保活
-
部分代理服务器可能不支持
适用场景:即时通讯、在线游戏、实时协作、金融交易系统。
方案五:MQTT——物联网场景的首选
MQTT是专为物联网设计的轻量级消息协议,采用发布/订阅模式,非常适合网络不稳定、带宽受限的环境。
核心概念
MQTT有三个核心角色:
-
发布者(Publisher):发送消息的客户端
-
订阅者(Subscriber):接收消息的客户端
-
代理(Broker):消息中转服务器,如Eclipse Mosquitto、EMQX
实现方案
-
引入依赖(使用Eclipse Paho):
<dependency> <groupId>org.eclipse.paho</groupId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId> <version>1.2.5</version> </dependency>
-
MQTT配置类:
@Configuration @ConfigurationProperties(prefix = "mqtt") @Data public class MqttConfig { private String broker = "tcp://localhost:1883"; private String clientId = "spring-boot-server"; private String username = "admin"; private String password = "admin"; private int qos = 1; private boolean retained = false; }
-
MQTT服务类:
@Service @Slf4j public class MqttService {
@Autowired
private MqttConfig mqttConfig;
private MqttClient mqttClient;
@PostConstruct
public void init() {
try {
mqttClient = new MqttClient(
mqttConfig.getBroker(),
mqttConfig.getClientId(),
new MemoryPersistence()
);
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(mqttConfig.getUsername());
options.setPassword(mqttConfig.getPassword().toCharArray());
options.setCleanSession(true);
options.setAutomaticReconnect(true);
options.setConnectionTimeout(10);
options.setKeepAliveInterval(20);
mqttClient.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
log.error("MQTT连接丢失", cause);
}
@Override
public void messageArrived(String topic, MqttMessage message) {
log.info("收到MQTT消息 - 主题: {}, 内容: {}",
topic, new String(message.getPayload()));
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
log.debug("消息发送完成");
}
});
mqttClient.connect(options);
log.info("MQTT客户端连接成功");
} catch (MqttException e) {
log.error("MQTT客户端初始化失败", e);
}
}
/**
* 发布消息到指定主题
*/
public void publish(String topic, String payload) {
try {
if (mqttClient != null && mqttClient.isConnected()) {
MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8));
message.setQos(mqttConfig.getQos());
message.setRetained(mqttConfig.isRetained());
mqttClient.publish(topic, message);
log.info("发布消息到主题 {} - 内容: {}", topic, payload);
}
} catch (MqttException e) {
log.error("发布消息失败", e);
}
}
/**
* 订阅主题
*/
public void subscribe(String topic, IMqttMessageListener listener) {
try {
if (mqttClient != null && mqttClient.isConnected()) {
mqttClient.subscribe(topic, mqttConfig.getQos(), listener);
log.info("订阅主题: {}", topic);
}
} catch (MqttException e) {
log.error("订阅主题失败", e);
}
/**
* 取消订阅
*/
public void unsubscribe(String topic) {
try {
if (mqttClient != null && mqttClient.isConnected()) {
mqttClient.unsubscribe(topic);
log.info("取消订阅主题: {}", topic);
}
} catch (MqttException e) {
log.error("取消订阅失败", e);
}
}
@PreDestroy
public void destroy() {
try {
if (mqttClient != null && mqttClient.isConnected()) {
mqttClient.disconnect();
mqttClient.close();
log.info("MQTT客户端已关闭");
}
} catch (MqttException e) {
log.error("关闭MQTT客户端失败", e);
}
}
}
-
业务应用示例(智能家居场景):
@Service public class SmartHomeService {
@Autowired
private MqttService mqttService;
// 主题定义
private static final String TOPIC_TEMPERATURE = "home/sensor/temperature";
private static final String TOPIC_LIGHT = "home/control/light";
private static final String TOPIC_ALARM = "home/alarm";
@PostConstruct
public void init() {
// 订阅温度传感器数据
mqttService.subscribe(TOPIC_TEMPERATURE, (topic, message) -> {
String payload = new String(message.getPayload());
double temperature = Double.parseDouble(payload);
handleTemperatureData(temperature);
});
// 订阅报警信息
mqttService.subscribe(TOPIC_ALARM, (topic, message) -> {
String alarmMessage = new String(message.getPayload());
handleAlarmMessage(alarmMessage);
});
}
/**
* 处理温度数据
*/
private void handleTemperatureData(double temperature) {
log.info("当前温度: {}℃", temperature);
// 温度过高,自动开启空调
if (temperature > 28) {
controlAirConditioner("on", 26);
}
// 保存到数据库
saveSensorData("temperature", temperature);
}
/**
* 处理报警消息
*/
private void handleAlarmMessage(String message) {
log.warn("收到报警: {}", message);
// 推送给所有管理员
notifyAdmins("安全警报", message);
// 记录日志
saveAlarmLog(message);
}
/**
* 控制灯光
*/
public void controlLight(String room, String action) {
String topic = TOPIC_LIGHT + "/" + room;
mqttService.publish(topic, action);
}
/**
* 控制空调
*/
public void controlAirConditioner(String action, int temperature) {
Map<String, Object> command = new HashMap<>();
command.put("action", action);
command.put("temperature", temperature);
mqttService.publish("home/control/air-conditioner", JSON.toJSONString(command));
}
}
方案评价
MQTT在物联网领域是事实标准,适合设备数量巨大、网络环境复杂的场景。
优点:
-
协议极其轻量,开销小
-
支持QoS(消息质量保证)
-
支持离线消息
-
适合低带宽、高延迟网络
缺点:
-
需要额外的Broker服务器
-
学习成本相对较高
-
Web端支持需要额外配置
适用场景:物联网设备通信、智能家居、车联网、工业监控。
方案六:iframe流——古老但仍有用的技术
iframe流是一种比较古老的技术,通过在页面中嵌入隐藏的iframe,服务器持续向iframe推送数据。
简单实现
Java后端:
@Controller @RequestMapping("/iframe") public class IframeStreamController {
private final AtomicInteger counter = new AtomicInteger(0);
@GetMapping("/stream")
public void stream(HttpServletResponse response) throws IOException {
response.setContentType("text/html;charset=UTF-8");
response.setHeader("Cache-Control", "no-cache");
PrintWriter writer = response.getWriter();
// 持续推送数据
while (true) {
try {
int count = counter.incrementAndGet();
String script = String.format(
"<script>parent.updateCount(%d);</script>", count);
writer.print(script);
writer.flush();
Thread.sleep(2000);
} catch (InterruptedException e) {
break;
}
}
}
}
前端HTML:
消息数量: 0
<!-- 隐藏的iframe -->
<iframe src="/iframe/stream" style="display:none;"></iframe>
<script>
function updateCount(count) {
document.getElementById('count').innerText = count;
}
</script>
</body> </html>
方案评价
iframe流是一种过时的技术,但在某些特定场景下仍可作为降级方案。
优点:
-
实现极其简单
-
兼容性好
缺点:
-
浏览器会一直显示加载状态
-
体验极差
-
资源占用高
适用场景:几乎不推荐使用,仅作为极端降级方案。
技术选型建议
面对这么多方案,该如何选择?我根据实际项目经验给出以下建议:
按场景选择
┌────────────────────┬──────────────────┬─────────────────────────────┐ │ 场景 │ 推荐方案 │ 理由 │ ├────────────────────┼──────────────────┼─────────────────────────────┤ │ 即时通讯、在线客服 │ WebSocket │ 需要双向实时通信 │ ├────────────────────┼──────────────────┼─────────────────────────────┤ │ AI对话、流式输出 │ SSE │ 单向推送,实现简单 │ ├────────────────────┼──────────────────┼─────────────────────────────┤ │ 后台系统通知 │ SSE 或 长轮询 │ 实时性要求不高,SSE更优雅 │ ├────────────────────┼──────────────────┼─────────────────────────────┤ │ 股票行情、监控大屏 │ SSE 或 WebSocket │ 取决于是否需要双向通信 │ ├────────────────────┼──────────────────┼─────────────────────────────┤ │ 物联网设备通信 │ MQTT │ 专为物联网设计 │ ├────────────────────┼──────────────────┼─────────────────────────────┤ │ 配置中心 │ 长轮询 │ 成熟方案,Nacos、Apollo都用 │ ├────────────────────┼──────────────────┼─────────────────────────────┤ │ 快速原型验证 │ 短轮询 │ 实现最简单 │ └────────────────────┴──────────────────┴─────────────────────────────┘
按团队能力选择
-
前端主导团队:优先SSE,前端EventSourceAPI很友好
-
全栈均衡团队:WebSocket,功能最强大
-
后端主导团队:长轮询,后端掌控力强
-
物联网团队:MQTT,行业标准
按技术栈选择
-
Spring Boot:WebSocket和SSE都有很好的支持
-
Node.js:Socket.io(WebSocket封装)生态成熟
-
微服务架构:考虑消息队列+WebSocket的组合方案
-
Serverless:SSE,无状态特性更匹配
性能优化建议
无论选择哪种方案,都要注意以下性能要点:
-
连接管理
// 使用ConcurrentHashMap管理连接 private final Map<String, Session> sessions = new ConcurrentHashMap<>();
// 定期清理无效连接 @Scheduled(fixedRate = 60000) public void cleanInactiveSessions() { sessions.entrySet().removeIf(entry -> !entry.getValue().isOpen()); }
-
消息队列缓冲
// 使用阻塞队列缓冲消息 private final BlockingQueue<Message> messageQueue = new LinkedBlockingQueue<>(1000);
// 异步消费 @Async public void consumeMessages() { while (true) { try { Message message = messageQueue.take(); processMessage(message); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }
-
限流保护
// 使用Guava的RateLimiter限流 private final RateLimiter rateLimiter = RateLimiter.create(100.0); // 每秒100个请求
public void sendMessage(String userId, String message) { if (rateLimiter.tryAcquire()) { doSendMessage(userId, message); } else { log.warn("发送频率过高,消息被丢弃"); } }
写在最后
消息推送看似简单,实则涉及网络协议、并发控制、资源管理等多个技术点。没有完美的方案,只有最适合的方案。
从我的实践经验来看:
-
80%的场景用SSE就够了,简单、够用、优雅
-
需要双向通信时果断用WebSocket,别犹豫
-
物联网场景必须MQTT,专业的事交给专业的协议
-
短轮询和iframe流基本可以忘掉了,除非你在维护老系统
最后建议大家,选择技术方案时不要追求"高大上",而要追求"合适"。能用SSE解决的问题,就不要上WebSocket;能用长轮询解决 的问题,就不要引入MQ。技术的本质是解决问题,而不是炫技。
希望这篇文章能帮你理清消息推送的各种方案,在实际项目中做出更好的技术选型。如果有任何疑问,欢迎留言交流!

5648

被折叠的 条评论
为什么被折叠?



