当前位置: 首页 > news >正文

后端开发必看!6种服务端主动推送方案的实战对比

写在前面的话

你有没有遇到过这样的场景:正在开发一个在线客服系统,用户发送消息后,客服端需要实时收到通知;或者开发一个电商后台, 订单状态变化时管理员要立即知晓。这些需求的本质都指向一个问题:如何让服务器主动把数据推送给浏览器?

传统的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连接,可以实现客户端和服务器之间的真正双向实时通 信。

完整实现

  1. 引入依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>

  1. 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; }

}

  1. 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; }

}

  1. 前端实现:

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

实现方案

  1. 引入依赖(使用Eclipse Paho):

<dependency> <groupId>org.eclipse.paho</groupId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId> <version>1.2.5</version> </dependency>

  1. 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; }

  1. 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); } } }
  1. 业务应用示例(智能家居场景):

@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,无状态特性更匹配

性能优化建议

无论选择哪种方案,都要注意以下性能要点:

  1. 连接管理

// 使用ConcurrentHashMap管理连接 private final Map<String, Session> sessions = new ConcurrentHashMap<>();

// 定期清理无效连接 @Scheduled(fixedRate = 60000) public void cleanInactiveSessions() { sessions.entrySet().removeIf(entry -> !entry.getValue().isOpen()); }

  1. 消息队列缓冲

// 使用阻塞队列缓冲消息 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; } } }

  1. 限流保护

// 使用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。技术的本质是解决问题,而不是炫技。

希望这篇文章能帮你理清消息推送的各种方案,在实际项目中做出更好的技术选型。如果有任何疑问,欢迎留言交流!

http://www.jsqmd.com/news/1068711/

相关文章:

  • Ubuntu 18.04 部署 code-server 实战指南:Docker+HTTPS+ROS 全栈配置
  • Ubuntu 20.04 LEMP部署实战:Nginx+PHP7.4+MySQL8.0完整配置
  • Wireshark网络协议分析实战:从抓包入门到故障排查精要
  • LLM生产环境稳定性指南:从OOM到长尾延迟的防御体系
  • App Platform自定义域名、SSL与CDN配置原理与实战
  • Cursor编辑器深度解析:项目级语义感知与AI原生编码工作流
  • FileZilla Client 3.70.4 官方版下载(Windows/macOS/Linux,夸克网盘)
  • JMeter安装配置全攻略:从零搭建性能测试环境
  • Ubuntu 14.04 上用 Terraform 部署 Node.js 的实战方案
  • Gemini 3.1 Pro五大核心技巧:解锁高阶推理与结构化输出
  • 三步构建AI API使用数据自动化分析流水线:从账单到洞察
  • MCU低功耗设计:SIM_SD寄存器精准控制外设时钟与唤醒机制
  • 2024年AIGC商业落地指南:从多模态大模型到实战应用
  • MC68010循环模式:硬件级指令优化与嵌入式性能提升
  • XSS攻击脚本全解析:从原理到实战绕过技巧与防御指南
  • Vue 3国际化实战:vue-i18n核心原理与工程化落地
  • Weave Scope容器监控:实时拓扑可视化与交互式诊断实战指南
  • Postman自动化CSRF Token认证:环境变量与脚本实战指南
  • Java FutureTask 深度解析:状态机、超时控制与线程中断原理
  • 零样本学习在软件工程情感分析中的创新应用
  • 跨越LLM产品评估可操作性差距:从数据到行动的系统方法
  • DMXAPI+Qwen3.7-Max智能体实战:从PLC文档化看AI编程落地
  • Prisma + PostgreSQL 生产级落地指南:从连接配置到向量搜索
  • RTA广告技术解析:从实时API原理到电商金融实战部署
  • GLM-5.1代码能力跃迁:从SWE-Bench Pro登顶看大模型工程化落地
  • Qwen3.5+llama.cpp实测:216G显存跑262K上下文与120 tokens/s推理
  • SRC漏洞挖掘入门指南:从零到一掌握白帽子实战技能
  • FEC以太网控制器:缓冲区描述符机制与嵌入式网络驱动开发实战
  • Claude Opus 4.8 effort机制深度解析:成本与性能的临界点优化
  • 混元3.0编程能力跃迁:MoE架构与262K上下文如何重塑开发者工作流