服务端与客户端端实时消息推送

2025博客之星年度评选已开启 10w+人浏览 2.6k人参与

服务端与客户端端实时消息推送

  通过这篇文章你将了解到服务端与客户端实时消息通信有哪些技术方案,如短轮询、长轮询、SSE、MQTT、Iframe、WebSocket、第三方平台等,这些方案的原理是什么,有什么特点,优缺点是什么,适用场景是什么等等。


—— 2025 年 12 月 21 日 乙巳蛇年十一月初二 冬至
扫码关注微信公众号   了解最新最全文章

qrcode

前言

  服务端与 web 端实时消息推送是网络通信技术的一种,允许服务端在有新数据时主动将消息发送给客户端,或服务端与客户端长时间连续的消息交换。比如用户的消息中心、站内信、新闻推送、在线客服、实时聊天等场景。在传统的服务端与客户端交互场景中,是客户端主动向服务端发起一次 http 请求进行消息交换,在只有一次的消息交换结束后,该请求将被销毁,若想再次进行消息交换则需要重新发起一次 http 请求。在服务端与 web 端实现实时消息推送的常用技术有 短轮询、长轮询、SSE、MQTT、Iframe、WebSocket、第三方平台 等,每种技术在实时性、复杂性、通信方式、适用场景等方面都有不同体现。

1 短轮询

1.1 原理

  短轮询是指客户端指定时间间隔,定时向服务器发送 http 请求,服务器收到请求后立即响应,若有新消息(未读消息)则返回新消息,否则返回空。其为短暂连接,即每一次请求完后就会立即关闭,频率一般固定为 5s,基本上所有浏览器都支持短轮询。

1.2 优缺点

  • 优点:
    • 实现简单,客户端通过轮询即可实现。
    • 兼容性好,基本上所有浏览器都支持。
    • 适合消息推送频率较低的场景。
  • 缺点:
    • 实时性差,实时性取决于轮询时间间隔,间隔越小实时性越好。
    • 耗费资源,对于消息生产时间不集中或生产频率较低的场景,会导致大量的无效请求。
    • 服务器压力大,频繁建立/关闭连接,以及无效请求会给服务器带来压力。

1.3 示例代码

客户端:如轮询间隔 interval 可为 1s

function shortPolling(url, interval) {
    setInterval(() => {
        fetch(url)
            .then(response => response.json())
            .then(data => {
            		// 处理消息
            });
    }, interval);
}

服务端:

@PostMapping("/message")
public APIResponse<List<MessageVo>> listMessage(@RequestParam("lastId") String lastId) {
  	// todo 获取新消息 记为 list
  	return APIResponse.success(list);
}

2 长轮询

2.1 原理

  长轮询是短轮询的改进版,客户端向服务器发送 http 请求,若无新消息,则服务器不会立即响应,而是将客户端请求挂起,直到有新消息或超时,当有新消息产生时,服务器找到所有挂起的请求,然后检查消息对客户端来说是否为新消息,若是则响应对应请求,若请求超时则客户端重新请求。其连接时间一般保持在 30s ~ 60s。

2.2 优缺点

  • 优点:
    • 实时性优于短轮询。
    • 减少无效请求,资源占用少。
  • 缺点:
    • 服务器需要保持大量连接。
    • 连接超时后需要重新建立。
    • 实现复杂度较高。

2.3 示例代码

  此处只展示了基于 React.jsJava SpringBoot (DeferredResult) 实现的简单版长轮询伪代码,实际生产中可能还需要考虑这些功能:心跳机制,用来保持与监测客户端与服务器的连接状态;重试机制,连接失败后重新尝试连接;连接状态管理,跟踪连接状态,支持自动重连;错误处理;事件驱动解耦等。

客户端:

import { messageAPI } from './api';

// 消息列表
const [messages, setMessages] = useState<MessageVo[]>([]);
const [isConnected, setIsConnected] = useState(false);
const [status, setStatus] = useState('未连接');

useEffect(() => {
        let isActive = true;

        const startLongPolling = async () => {
            setIsConnected(true);
            setStatus('已连接');

            // todo 首次需要加载所有历史消息 此处略

            // 开始长轮询
            while (isActive) {
                try {
                  	// 生成一个 clientId;
                  	const clientId = ...;
                    
                    // 获取最后一条消息的ID
                    const lastId = messages.length > 0
                        ? Math.max(...messages.map(m => m.id))
                        : null;

                    // 创建AbortController用于取消请求
                    abortControllerRef.current = new AbortController();

                    setStatus('等待新消息...');

                    // 长轮询请求
                    const response = await messageAPI.longPoll(
                        clientId, lastId, 30000,
                        abortControllerRef.current.signal
                    );

                    // todo 处理消息
                } catch (error) {
                    if (isActive) {
                        await new Promise(resolve => setTimeout(resolve, 3000));
                    }
                }
            }
        };

        startLongPolling();

        // 清理函数
        return () => {
            isActive = false;
            if (abortControllerRef.current) {
                abortControllerRef.current.abort();
            }
            setIsConnected(false);
            setStatus('已断开');
        };
    }, []); // 空依赖数组 表示只在组件挂在时执行一次

服务端:

@RestController
@RequestMapping("/you")
public class YouController {

  	// 维持的客户端连接
    private final Map<String, DeferredResult<APIResponse<List<MessageVo>>>> pendingRequests = new ConcurrentHashMap<>();

    @PostMapping("/longPoll")
    public DeferredResult<APIResponse<List<MessageVo>>> longPoll(@RequestParam("clientId") String clientId,
                                                              @RequestParam("lastId") long lastId,
                                                              @RequestParam("timeout") long timeout) {
        DeferredResult<APIResponse<List<MessageVo>>> deferredResult = new DeferredResult<>(timeout);

        // todo 获取新消息 记为 messages
        List<MessageVo> messages = new ArrayList<>();

        // 若有新消息则立即返回
        if (!CollectionUtils.isEmpty(messages)) {
            deferredResult.setResult(APIResponse.success(messages));
            return deferredResult;
        } else {
            // 记录客户端连接
            pendingRequests.put(clientId, deferredResult);

            // 设置完成(即消息返回后)回调
            deferredResult.onCompletion(() -> {
                // 移除维持的客户端连接
                pendingRequests.remove(clientId);
            });

            // 设置超时回调
            deferredResult.onTimeout(() -> {
                // 移除维持的客户端连接
                pendingRequests.remove(clientId);
            });

            // 设置超时回调
            deferredResult.onTimeout(() -> {
                // 超时后返回空
                deferredResult.setResult(APIResponse.success());
            });
        }

        return deferredResult;
    }

    // 新消息产生后通知所有等待的客户端
  	// 这里的新消息可以是来自其它客户端的消息 也可以是系统内部产生的消息(如通知等)
    private void notifyPendingRequests(List<MessageVo> messages) {
        List<String> clientIds = new ArrayList<>(pendingRequests.keySet());

        DeferredResult<APIResponse<List<MessageVo>>> deferredResult;
        for (String clientId : clientIds) {
            // 通知客户端意味着该请求已完成 故需移除维持的客户端连接
            deferredResult = pendingRequests.remove(clientId);
            deferredResult.setResult(APIResponse.success(messages));
        }
    }
}

3 SSE

3.1 原理

  SSE(Server-Sent Events) 是一种基于 http 协议的单向实时通信技术,允许服务器通过持久性连接向客户端主动推送数据,而无需客户端反复发起请求。它是 HTML5 标准的一部分,适用于服务器向客户端持续推送更新的场景,如实时通知、数据仪表盘、日志流或 AI 打字机输出等。

  SSE 的通信机制是服务器与客户端之间建立一个长时间保持的 http 连接,服务器在响应中设置特定的 http 头(如 Content-Type: text/event-stream),并以特定格式持续发送数据块,客户端通过 EventSource API 监听事件流以接收数据。

  SSE 相比传统轮询机制减少了 http 请求开;相比 WebSocket 双向通信,简单轻量易实现。

3.2 优缺点

  • 优点:
    • 简单轻量易实现;
    • 支持自动重连机制;
    • http 友好,易于调试;
  • 缺点:
    • 仅支持服务器到客户端的单向通信;
    • 仅支持文本数据;
    • 连接数限制(http/1.1);
    • IE 浏览器不支持(需要 polyfill);

3.3 示例代码

  此处只展示了基于 React.jsJava SpringBoot (SseEmitter) 实现的简单版 sse 伪代码,实际生产中可能还需要考虑这些功能:心跳机制,用来保持与监测客户端与服务器的连接状态;重试机制,连接失败后重新尝试连接;连接状态管理,跟踪连接状态,支持自动重连;错误处理;事件驱动解耦等。

客户端:

// 自定义 sse
export const useSSE = (clientId) => {
    const [isConnected, setIsConnected] = useState(false);
    const [messages, setMessages] = useState([]);
    const [error, setError] = useState(null);
    const [eventSource, setEventSource] = useState(null);

    useEffect(() => {
        // 自定义生成 clientId
        const id = ...;
        const timeout = ...;
        const url = `http://localhost:8081/api/sse/connect?clientId=${id}&timeout=${timeout}`;

        // 创建 EventSource 连接
        const es = new EventSource(url);

        // 连接成功
        es.onopen = () => {
            setIsConnected(true);
            setError(null);
        };

        // 监听 "message" 事件 (普通消息)
        es.addEventListener('message', (event) => {
            try {
                const message = JSON.parse(event.data);
                // 处理消息 将新消息追加到 messages 数组中
            } catch (e) {
                console.error('解析消息失败:', e);
            }
        });

        setEventSource(es);

        // 清理函数
        return () => {
            es.close();
            setIsConnected(false);
        };
    }, [clientId]);

    return {
        isConnected,
        messages,
        error,
        eventSource
    };
};
// 消息接收
import { messageAPI } from './api';
import { useSSE } from './useSSE';

const [messages, setMessages] = useState([]);
const { isConnected, messages, error } = useSSE();
const messagesEndRef = useRef(null);

const scrollToBottom = () => {
  	messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};

// 模拟接收到消息后滚动到底部
useEffect(() => {
  	scrollToBottom();
}, [messages]);

服务端:

@RestController
@RequestMapping("/sse")
public class SseController {

    private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
    
    private final ObjectMapper objectMapper = new ObjectMapper();

    @PostMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter connect(@RequestParam("clientId") String clientId,
                              @RequestParam("timeout") long timeout) {
        // 创建 sse 设置超时时间
        SseEmitter emitter = new SseEmitter(timeout);
        
        // 注册连接
        emitters.put(clientId, emitter);
        
        // 设置完成时回调
        emitter.onCompletion(() -> {
            emitters.remove(clientId);
        });
        
        // 设置超时时回调
        emitter.onTimeout(() -> {
            emitters.remove(clientId);
        });
        
        // 设置错误时回调
        emitter.onError((e) -> {
            emitters.remove(clientId);
        });
        
        return emitter;
    }

    // 新消息产生后通知所有等待的客户端
    private void broadcastMessage(MessageVo message) {
        List<String> deadEmitters = new ArrayList<>();
        
        emitters.forEach((clientId, emitter) -> {
            try {
                // 推送消息
                emitter.send(SseEmitter.event()
                        .name("message")
                        .data(objectMapper.writeValueAsString(message)));
            } catch (Exception e) {
                // 推送失败时记录连接状态失效的客户端
                deadEmitters.add(clientId);
            }
        });
        
        // 移除失效的连接
        deadEmitters.forEach(emitters::remove);
    }
}

4 MQTT

4.1 原理

  MQTT(Message Queuing Telemetry Transport) 即消息队列遥测传输是一个轻量级的、基于发布/订阅模式的消息传输协议。主要用于物联网(IoT)、低宽带、不可靠的网络环境中。MQTT 属于应用层协议,构建于 TCP/IP 协议之上,即只要支持 TCP/IP 协议栈的地方都可以使用 MQTT 协议。

4.2 优缺点

  • 优点:
    • 协议开销极小,适合低宽带环境;
    • 适合不稳定的网络环境;
    • 轻量级发布/订阅模式;
    • 支持持久会话和离线消息;
    • 生态丰富,工具和库非常完善
  • 缺点:
    • 依赖MQTT代理服务器;
    • 实现相对复杂;

4.3 示例代码

  MQTT 与常用的消息队列类似,其核心组成部分包括以下组件:

  • Broker(代理):MQTT 服务器,负责接收、过滤、分发消息;
  • Client(客户端):发布者与订阅者;
  • Topic(主题):消息的分类标签;
  • Publish(发布):客户端向主题写入/发送消息;
  • Subscribe(订阅):客户端订阅主题以消费消息;

  示例代码技术组件选择:

  • Broker:公共测试 Broker,无需安装,直接使用,适合开发测试;

    • TCP:tcp://broker.emqx.io:1883
    • WebSocket:ws://broker.emqx.io:8083/mqtt

    ⚠️ 注意

    公共测试 Broker 安全性较低,消息可能被他人接收,故不能用于生产环境。与 kafka 等消息队列组件类似,MQTT 的消息服务器也需要作为独立组件单独部署。

  • 后端客户端:Eclipse Paho MQTT Client + Spring Intergration MQTT

  • 前端客户端:MQTT.js (即 JavaScript MQTT 客户端库)

  因篇幅原因,此处不展示前后端相关示例代码,但提供设计建议:

  • 后端:

    • 配置 MQTT 客户端工厂:MqttPahoClientFactory
    • 创建入站/出站消息通道:MessageChannel
    • 配置消息订阅和发布处理器:MessageProducerMessageHandler
    • 设置自动重连、QoS 等参数;
  • 前端:

    • 使用 mqtt.js 库连接 MQTT Broker;
    • 可通过 WebSocket 连接,即使用 ws:// 协议;
    • 订阅多个主题;
    • 自动处理连接、断开、重连;

    ⚠️ 注意

    虽然前端可以直接发布消息到 MQTT 服务器,但建议通过后端 API 转发到 MQTT 服务器,因为前端安全性较低,且前端不方便实现数据验证、持久化、追踪审计等业务逻辑。

5 Iframe

5.1 原理

  Iframe 是 HTML5 提供的技术,在服务端与客户端消息通信中,主要通过跨域消息通信机制实现,其核心方法是 window.postMessage API。该技术允许嵌入在页面中的 iframe 与父页面(或不同源的窗口)安全地交换数据,突破同源策略的限制。亦可理解为通过隐藏的 iframe 标签,向服务器发送请求,服务器通过不断向 iframe 写入数据来推送消息。这种方式称为 永久帧 或 流。

5.2 优缺点

  • 优点:
    • 实现简单易理解;
    • 异步加载,支持非阻塞式加载,即可以进行延时渲染;
    • 跨域通信,可与父页面实现安全的跨域消息传递;
    • 兼容性与稳定性,对 “老” 浏览器友好;
  • 缺点:
    • 性能损耗,每个 iframe 需独立解析 HTML,导致内存占用和页面渲染时间增加;
    • 响应式布局困难,iframe 对响应式布局不太友好,需要设置响应式容器;

5.3 示例代码

⚠️ 注意

此处只展示了简单版的伪代码,实际生产中可能还需要考虑这些功能:心跳机制,用来保持与监测客户端与服务器的连接状态;重试机制,连接失败后重新尝试连接;连接状态管理,跟踪连接状态,支持自动重连;错误处理;事件驱动解耦等。

客户端:

<iframe id="myIframe" src="/iframe/stream" width="500" height="300"></iframe>
<script>
  document.getElementById('myIframe').onload = function() {
    // 在iframe加载完成后,你可以通过以下方式与iframe内容进行交互:
    var iframeDoc = this.contentDocument || this.contentWindow.document;
    console.log(iframeDoc.body.innerHTML); // 例如,打印iframe中的内容到控制台
  };
</script>

服务端:

@RestController
@RequestMapping("/iframe")
public class IframeController {

    private final Map<String, PrintWriter> activeStreams = new HashMap<>();

    @PostMapping(value = "/stream", produces = "text/html;charset=UTF-8")
    public void message(HttpServletResponse response,
                        @RequestParam("clientId") String clientId) throws IOException {

        // 设置响应头
        response.setContentType("text/html;charset=UTF-8");
        response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache");
        response.setHeader(HttpHeaders.CONNECTION, "keep-alive");

        PrintWriter writer = response.getWriter();

        this.sendMessage(writer, "连接成功!");
        
        activeStreams.put(clientId, writer);
    }

    // 新消息产生后可调用此方法通知所有等待的客户端
    private void broadcastMessage(String message) {
        activeStreams.forEach((clientId, writer) -> {
            this.sendMessage(writer, message);
        });
    }

    // 发送消息
    private void sendMessage(PrintWriter writer, String message) {
        // 输出script标签,调用父窗口的receiveMessage函数
        writer.println("<script>");
        writer.println("if (window.parent && window.parent.receiveMessage) {");
        writer.println("  window.parent.receiveMessage('" + message + "');");
        writer.println("}");
        writer.println("</script>");
        
        writer.flush();
    }
}

6 WebSocket

6.1 原理

  WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它允许客户端和服务器之间建立永久性连接,实现双向数据传输。在实现上,WebSocket 使用统一的资源标识符 ws://wss://,并包含消息帧格式(如消息头、负载长度等)以确保数据完整传输,适用于聊天室、多人协作工具、弹幕系统、视频会议等。

6.2 优缺点

  • 优点:
    • 全双工通信,即在同一个连接上客户端和服务器可同时收发;
    • 持久连接,一次握手,长期通信;
    • 低延迟,实时推送,无轮询开销;
    • 自动重连,断线后可自动重连;
    • 跨平台兼容,支持 web 端、移动端等;
  • 缺点:
    • 连接状态管理较复杂;
    • 需要客户端和服务端同时支持;
    • 老旧浏览器需要适配;

6.3 示例代码

  服务端主要依赖于 spring-boot-starter-websocket 实现,客户端使用 WebSocket 实现,因篇幅原因,此处只展示服务端相关代码。

// ws 处理器
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {

    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

    // 建立连接后调用
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String sessionId = session.getId();
        sessions.put(sessionId, session);

        this.broadcastMessage("新用户加入:" + sessionId);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String sessionId = session.getId();
        sessions.remove(sessionId);

        this.broadcastMessage("用户离开:" + sessionId);
    }

    // 广播消息
    private void broadcastMessage(String message) {
        sessions.forEach((sessionId, session) -> {
            this.sendMessage(session, message);
        });
    }

    // 发送消息
    private void sendMessage(WebSocketSession session, String message) {
        try {
            if (session.isOpen()) {
                session.sendMessage(new TextMessage(message));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// ws 配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    private final ChatWebSocketHandler chatWebSocketHandler;

    public WebSocketConfig(ChatWebSocketHandler chatWebSocketHandler) {
        this.chatWebSocketHandler = chatWebSocketHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册 ws 处理器
        registry.addHandler(chatWebSocketHandler, "/ws/chat")
                .setAllowedOrigins("*");
    }
}

7 第三方平台

  除了以上技术方案外,还可以使用第三方平台提供的实时消息服务,如 FCM(Firebase Cloud Mesage)PusherSocket.is.Cloud 等,其特点是可快速集成,无需自建实时消息服务器,且通常提供跨平台支持,当然可能会产生费用。

8 综合对比

  技术这么多,该如何选择呢,别担心,我给你一个综合对比表,你根据你的业务场景进行选择即可。

技术双向通信实时性服务器压力实现难度兼容性适用场景推荐度
短轮询简单实时性要求较低
数据更新频率较低
简单状态查询
不推荐
长轮询中等无法使用 WebSocket 的环境
严格的防火墙/代理机制
旧版浏览器
一般
SSE简单挺好的股票行情、新闻/通知流
服务器日志、进度条更新
社交媒体
挺好的
MQTT较难还行智能家居控制
传感器数据采集
车联网、物联网、远程设备监控等
挺好的
Iframe中等仅用于兼容 IE6/7 等远古浏览器不推荐
WebSocket中等挺好的即时聊天应用
在线游戏、视频弹幕
协作编辑、实时绘图/白板
强烈推荐
第三方-简单挺好的快速/敏捷开发
比较懒的
推荐度与钱包成线性增长

qrcode

扫码关注微信公众号   了解最新最全文章
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红衣女妖仙

行行好,给点吃的吧!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值