在现代 Web 应用开发中,实现服务器向客户端实时推送消息的需求越来越普遍。无论是聊天应用、实时监控、股票行情还是协同编辑,都需要高效可靠的消息传递机制。本文将探讨三种主流的技术方案:轮询 (Polling)服务器发送事件 (Server-Sent Events, SSE) 和 WebSocket,分析它们的工作原理、优缺点及适用场景。

1. 轮询 (Polling)

轮询是最基础、历史最悠久的实现“实时”通信的方法。它的核心思想很简单:客户端定期向服务器发送请求,询问是否有新数据可用。

轮询

  • 工作机制:浏览器按照指定的时间间隔,不断向服务器发送 HTTP 请求(如示例中的 GET/poll)。服务器收到请求后,会实时返回数据给浏览器。
  • 特点不管服务器有没有新数据,浏览器都会定时发请求这种方式可能导致请求过于频繁即便服务器无数据更新,也会产生大量请求,增加服务器和网络的负担,而且如果轮询间隔设置不合理,还可能出现数据更新的延迟(如图中 “延迟” 所示)。

长轮询

  • 工作机制:浏览器发送 Ajax 请求(如示例中的 GET/lpoll)后,服务器接收到请求会 “阻塞” 该请求。也就是说,服务器不会立即返回响应,而是等待有数据更新或者请求超时的时候,才会把数据返回给浏览器。
  • 特点:相比轮询,长轮询减少了不必要的请求次数。只有当有数据变化或者超时,服务器才会响应,能在一定程度上降低服务器和网络的压力,也能更及时地获取数据更新(只要数据更新在超时前发生)。

缺点

  • 效率低下: 大量请求可能是无效的(服务器无新数据),浪费网络带宽和服务器资源。
  • 实时性差: 消息到达客户端有显著延迟,取决于轮询间隔。间隔短则资源消耗大,间隔长则实时性差。
  • 服务器压力大: 在高并发场景下,大量的轮询请求会给服务器带来巨大压力。

适用场景

  • 对实时性要求不高的简单应用。
  • 老旧的浏览器环境或受限的设备。
  • 作为其他更先进技术不可用时的备选方案。

2. 服务器发送事件 (Server-Sent Events, SSE)

SSE 是一种基于 HTTP 的轻量级协议,它允许服务器单向地向客户端推送数据流。它建立在 HTTP 之上,利用了 HTTP 的长连接特性。

工作原理

  1. 客户端使用 EventSource API 向服务器发起一个特殊的 HTTP GET 请求。请求头中包含 Accept: text/event-stream
  2. 服务器收到请求后,保持连接打开(长连接),并设置响应头 Content-Type: text/event-stream
  3. 当服务器有数据需要推送时,它通过这个持久化的连接发送遵循特定格式(如 data: <message>\n\n)的数据块。
  4. 客户端通过 EventSource 对象的事件监听器(如 onmessage)实时接收并处理这些数据。

服务器端示例:

import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@RestController
@EnableScheduling
public class SseController {

    @GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter stream() {
        SseEmitter emitter = new SseEmitter();
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

        scheduler.scheduleAtFixedRate(() -> {
            try {
                String message = "Current time is " + 
                    LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME);
                emitter.send(SseEmitter.event().data(message));
            } catch (IOException e) {
                scheduler.shutdown();
                emitter.completeWithError(e);
            }
        }, 0, 1, TimeUnit.SECONDS);

        return emitter;
    }
}

客户端示例:

const eventSource = new EventSource('/stream');

eventSource.onmessage = function(event) {
    console.log('New message:', event.data);
    // 更新 UI
};

eventSource.onerror = function(error) {
    console.error('EventSource error:', error);
    eventSource.close();
};

优点

  • 真正的服务器推送: 服务器可以在有新数据时立即发送,无需客户端请求。
  • 基于 HTTP/HTTPS: 更容易通过防火墙和代理,通常不需要特殊配置。
  • 轻量级: 协议简单,实现相对容易。
  • 自动重连: EventSource API 内置了连接断开后的重连机制。
  • 文本数据友好: 非常适合推送文本格式的更新(如 JSON)。

缺点

  • 单向通信: 只能服务器向客户端推送,客户端无法通过同一个连接向服务器发送数据(仍需使用 AJAX 等)。
  • 文本格式限制: 主要设计用于 UTF-8 文本数据,二进制数据支持有限或不方便。
  • 最大并发连接数限制: 浏览器通常对每个源的并发 HTTP 连接数有限制(如 6 个),可能影响多个 SSE 流。
  • 不支持旧浏览器: 较旧的浏览器(如 IE)不支持 EventSource API。

适用场景

  • 实时通知(新闻推送、社交媒体更新)。
  • 监控仪表盘(实时显示服务器状态、日志)。
  • 实时更新文本内容(股票价格、比分、聊天消息接收端)。

3. WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它在客户端和服务器之间建立持久连接后,双方可以随时互发数据,彻底打破了 HTTP 请求/响应模式的限制。

工作原理

  1. 握手 (Handshake): 客户端发起一个特殊的 HTTP GET 请求(包含 Upgrade: websocket 等头部)。服务器如果支持 WebSocket,会返回 101 Switching Protocols 响应,完成协议升级。
  2. 建立连接: 握手成功后,底层的 TCP 连接保持打开,但通信协议从 HTTP 切换为 WebSocket 协议 (通常使用 ws:// 或 wss:// 协议头)。
  3. 双向通信: 连接建立后,服务器和客户端可以随时、任意地向对方发送数据帧(可以是文本或二进制)。数据帧的开销很小。
  4. 关闭连接: 任何一方都可以发起关闭连接的握手。

客户端示例 :

const socket = new WebSocket('wss://your-server-endpoint');

socket.onopen = function(event) {
    console.log('WebSocket connection opened');
    socket.send('Hello Server!'); // 向服务器发送消息
};

socket.onmessage = function(event) {
    console.log('Received from server:', event.data);
    // 处理服务器推送的消息
};

socket.onerror = function(error) {
    console.error('WebSocket error:', error);
};

socket.onclose = function(event) {
    console.log('WebSocket connection closed', event.code, event.reason);
};

服务器端 :

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;

@ServerEndpoint("/websocket")
public class WebSocketServer {

    @OnOpen
    public void onOpen(Session session) throws IOException {
        session.getBasicRemote().sendText("Welcome!"); // 主动推送消息
    }

    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        // 处理消息并回复
        session.getBasicRemote().sendText("Echo: " + message);
    }

    @OnClose
    public void onClose(Session session) {
        // 连接关闭处理
    }

    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }
}

优点

  • 真正的全双工实时通信: 双向、低延迟的数据交换。
  • 低开销: 建立连接后,数据帧头部很小,显著减少带宽消耗。
  • 高性能: 避免了 HTTP 的开销(如每次请求的头信息)。
  • 支持二进制数据: 高效传输文本和二进制数据。
  • 标准协议: 现代浏览器广泛支持。

缺点

  • 复杂性更高: 实现和管理 WebSocket 服务器相比 HTTP 服务更复杂。
  • 连接管理: 需要处理连接状态、心跳保活、重连逻辑等。
  • 代理和防火墙穿透: 某些严格的网络环境可能需要特殊配置才能允许 WebSocket 连接(wss:// 通常更容易)。
  • 状态保持: 持久连接意味着服务器需要管理更多状态(连接会话)。

适用场景

  • 实时聊天应用。
  • 多人在线游戏。
  • 协同编辑工具。
  • 高频金融交易平台。
  • 需要实时双向数据流的任何应用。

总结对比

特性轮询 (Polling)服务器发送事件 (SSE)WebSocket
通信方向客户端主动拉取服务器单向推送服务器与客户端双向通信
协议基础HTTPHTTP (长连接)独立的 WebSocket 协议
实时性低 (延迟取决于间隔)非常高
效率/开销高 (大量无效请求)中 (长连接,文本为主) (连接后开销小)
数据格式任意文本为主 (UTF-8)文本与二进制
实现复杂度
浏览器支持广泛较广泛 (IE 不支持)广泛 (旧浏览器部分支持)
适用场景简单、低实时性需求服务器向客户端单向推送高实时性、双向交互应用

如何选择?

选择哪种消息推送技术取决于具体的应用需求:

  • 需要简单、兼容性最好,且对实时性要求不高? 轮询可以作为起点或备选。
  • 服务器需要主动向客户端推送文本更新,且不需要客户端频繁回传数据? SSE 是一个高效且相对简单的选择,尤其适用于通知、监控等场景。
  • 需要高频率、低延迟、双向的实时交互? WebSocket 是最佳选择,尽管实现复杂度更高。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐