目录
- 前言
- 架构设计
- 实现原理
-
- 一、服务端
-
- 1、技术选型
-
- 1) javax.websocket javax.websocket)
- 2) Socket.IO Socket.IO)
- 3) netty-websocket netty-websocket)
- 4) spring-websocket spring-websocket)
- 5) spring-webflux websocket spring-webflux websocket)
- 6) Vert.x Vert.x)
- 选择
- Demo
- 流程图
- 2、服务端负载均衡问题
- 3、平稳发布
- 二、客户端
-
- 1、重连策略
- 2、多标签页连接共享的实现方案
-
- 1). localStorage 事件监听. localStorage 事件监听)
- 2). Broadcast Channel API. Broadcast Channel API)
- 3). Service Worker 中转. Service Worker 中转)
- 4). SharedWorker 方案. SharedWorker 方案)
- 总结
前言
我们在之前的文章中讲了 服务端推送方案之SSE (点击查看),今天我们要讲的是另外一种方案 - 全双工的 WebSocket 来实现。WebSocket 应用非常广泛,常见的比如 IM 双端通信能力,今天我们不聊IM,而是专注另外一个常见的场景 - 服务端推送
为什么需要服务端推送?
想象一下,当服务端推送能力缺失时,如果想要客户端主动感知数据变化,则只能通过客户端轮询方式实现,由此带来多种问题:
- 资源浪费显著:大量轮询请求结果为空,造成客户端和服务端计算资源的无端消耗
- 实时性不足:轮询间隔导致信息更新存在时间差,无法满足实时通知的业务需求
- 体验与效率受损:在实时性要求高的场景下,信息延迟直接影响用户体验和业务作业效率
值得注意的是,许多大型企业为了更好适配自身业务需求,往往会自主研发基于TCP协议的应用层协议。这种定制化方案能够带来更优的性能表现,阿里、腾讯、字节跳动等科技巨头都采用了这一实践。
常见的有:
| 公司 | 自研组件 | 核心特点 | 应用场景 |
|---|---|---|---|
| 阿里巴巴 | Mars | 跨平台网络通信框架,支持双向通信、多路复用、弱网络优化 | 移动端长连接、即时通讯、实时数据推送 |
| 腾讯 | TIM Protocol | 轻量级即时通讯协议,支持双向实时通信、多端同步、消息加密 | 微信、QQ等即时通讯应用的实时消息传输 |
| 字节 | Bytedance-RTCP | 轻量级实时传输协议,支持低延迟双向通信、流量控制 | 抖音、今日头条的实时内容推送和交互 |
架构设计
通用推送平台:
从电商平台的订单状态更新、社交媒体的消息通知,到金融行业的实时行情推送、各类业务场景都需要可靠的推送服务。这些业务虽然表现形式各异,但本质上都涉及到 服务端主动向客户端 推送数据这一共性需求。
推送系统需要解决的基础问题:如何建立和维护长连接、如何处理消息队列、如何实现消息路由和分发、如何确保消息的可靠投递等。这些技术挑战与具体的业务领域无关,具有高度的通用性。因此,将其设计为通用的独立推送平台是合理的架构选择。
这种设计具有以下优势:
- 避免重复建设:各业务线无需各自实现推送功能,业务开发团队可以专注于核心业务功能。
- 统一运维管理:集中式的平台更便于监控和维护,资源共享等。
典型的推送平台架构应该包含以下核心组件:
- 连接管理:负责维护客户端长连接
- 队列服务:处理消息的缓冲和持久化
- 消息路由:根据订阅关系分发消息
- 状态监控:实时掌握系统运行状况
- 管理接口:提供配置和查询能力
这种架构模式已经在业界得到广泛验证,如微信的WNS推送系统、阿里的移动推送服务等,都采用了类似的独立平台设计。
WebSocket 推送架构整体图:

一、连接/会话管理
连接/会话管理是客户端与服务器之间建立的通信通道进行创建、维护和终止的过程。它是网络通信和分布式系统中的核心组件,直接影响系统的性能、可靠性和安全性。
主要功能:
-
连接建立:三次握手协议(TCP)、安全连接建立(SSL/TLS)、身份验证过程、资源分配和初始化
-
会话维护:心跳机制保持连接活跃、超时检测和重连机制、会话状态同步、流量控制和拥塞管理
-
连接终止: 正常关闭流程、异常中断处理、资源回收、会话日志记录

二、消息队列
服务节点以分布式方式各自维护连接信息。当服务端推送消息时,如何定位到目标消息对应的具体连接?
方案一:维护路由表

流程:
- 每个节点创建连接后,将连接信息同步至
路由表 - 业务方通过统一推送入口-PushFacade 推送消息。
- PushFacade 收到消息后,先从路由表
查询连接所在节点,比如查询到节点 Node A - 再将消息
点对点推送至 Node A
| 优点 | 缺点 |
|---|---|
| 点对点推送,各节点处理效率高 | 路由表需要解决单点问题 |
为了提高查询效率,我们可以采用 Redis 来存储路由表数据。整个系统设计无需依赖队列机制即可实现高效运行。
方案二:使用消息队列

流程:
- 业务服务通过 PushFacade 推送消息,消息投递至 MQ
- MQ 监听到消息后,广播至所有推送节点
- 每个推送节点监听到消息后,判断自己本地路由表是否有对应连接,存在连接则将消息推送给客户端
| 优点 | 缺点 |
|---|---|
| 架构简单,复杂度低 | 依赖MQ广播消息,所有节点都会收到,依赖节点自身过滤机制,推送量巨大时,有一定的性能损失 |
| 系统解耦,抗压能力更强 | |
| 节点负载对等(通过负载均衡器实现) | |
| 无扩容负担,通过负载均衡器实现连接均衡分布 |
如果消息量级(QPS) 不是特别高,这种方案实现最为方便,可以满足大部分场景。
方案三:消息队列 + 路由表
有效结合方案一 + 方案二:通过消息队列(MQ)解耦上下游系统,同时结合路由表动态分发消息,确保不同业务场景的消息能精准投递到对应的处理节点。
优缺点对比:
| 优点 | 缺点 |
|---|---|
| 解耦彻底,扩展性强 | 需维护路由表,增加运维成本 |
| 支持动态调整路由策略 | 消息队列本身存在延迟(毫秒级) |
| 故障隔离(单节点异常不影响整体) | 需额外监控消息堆积和消费状态 |
方案三更为完善,但维护成本相对较高,建议根据系统实际情况综合考量。
队列的必要性
综上,在 WebSocket 服务端架构中,队列扮演着至关重要的角色,主要用于解决高并发场景下的消息处理问题。当大量客户端同时连接时,队列能够有效缓冲消息流量,避免服务端过载。
1. 消息缓冲与流量控制
- 突发流量处理:当短时间内大量消息涌入时,队列作为缓冲区平滑处理峰值
- 速率限制:通过队列控制消息处理速率,避免后端服务被压垮
- 优先级处理:实现不同优先级消息的分类处理(如VIP用户消息优先)
2. 解耦生产与消费
- 异步处理:生产者(客户端)和消费者(处理程序)通过队列解耦
- 削峰填谷:高峰期积压的消息可在低峰期逐步消化
- 错误隔离:单个消息处理失败不会影响队列中其他消息
队列机制确保了 WebSocket 服务在高并发下的稳定性、可靠性和可扩展性。
综上所述,综合考虑维护成本和实际需求,我们决定采用方案二进行实施。该方案具备良好的扩展性,即使后期需求变更,也能快速调整为方案三的架构。
三、消息路由/订阅
消息路由,即为消息转发 。作为经验丰富的从业者,你肯定清楚,一个业务系统往往需要处理多种不同类型的业务需求,例如下载通知、业务消息通知等。针对这种情况,我们可以将其设计 基于主题的订阅模式,有以下优点:
- 业务方可以灵活区分主题,做出不同的响应
- 可以实现更细粒度(主题)的管控、扩展性更强
模式如下:

其中:订阅消息格式: {"type":"subscribe", "topics":["topic1","topic2"]}、支持动态订阅和取消订阅。
四、多租户设计
我们可以更进一步考虑,如果想要作为基础平台能力建设,比如想要推广至不同业务条线(如零售业务、对公业务、资管业务等),或者同一条线下不同的业务方向(如零售业务下的信用卡项目、个人贷款项目、理财项目等)使用,为了避免相互影响,可以设计为多租户架构。
本文案例以支撑公司内部基础平台建设为目标,大致功能有:
- 基于租户ID字段进行租户数据隔离
- 为每个租户提供并发等流量管控
- 提供租户维度的接入、权限管理 及 租户管理功能
- 指标监控:租户及租户下不同主题维度的数据指标监控
架构图如下:

通过这种架构设计,既能实现资源共享降低成本,又能确保各业务单元的数据隔离和独立运营,为平台的规模化应用提供可靠支撑。
五、可视化数据监控
推送平台具备可视化的连接管理能力,为每个应用提供全面的连接监控和管理功能。具体包括:
-
实时连接监控视图:
- 连接的物理/逻辑节点位置(如北京节点A、上海节点B等)
- 客户端运行环境(操作系统版本、设备型号、SDK版本等)
- 网络质量指标(延迟、丢包率、带宽等)
- 连接持续时间及活跃状态
-
精细化连接管理:
- 剔除下线:强制终止指定连接,适用于异常连接处理
- 复位重连:重置连接状态,触发客户端重新建立连接
- 连接迁移:将连接转移到其他节点,实现负载均衡
-
历史连接分析:
- 提供连接历史记录查询功能
- 支持按时间、节点、客户端属性等多维度筛选
1)租户管理控制台:

2)连接/消息流量监控:

五、客户端SDK
为简化客户端接入流程,实现标准化接入并降低接入成本,我们将客户端连接能力进行抽象化封装,提供独立的客户端连接SDK。通过引入该SDK,客户端可便捷使用相关功能,无需关注底层业务实现。SDK需具备以下核心能力:
-
客户端连接生命周期管理:
- 提供完整的连接状态管理机制,包括初始化、连接建立、重连、断开连接等全流程控制
- 实现自动重连策略,支持指数退避算法(如初始重连间隔1秒,最大间隔30秒)
- 内置心跳检测机制,默认30秒发送一次心跳包,可配置心跳超时时间(如120秒无响应自动断开)
- 提供连接状态回调接口,支持 onConnected、onDisconnected、onReconnecting 等事件通知
-
协议适配与封装:
- 支持主流通信协议(WebSocket、HTTP/2、MQTT等)的统一封装
- 提供协议转换层,屏蔽底层协议差异
- 内置消息序列化/反序列化处理(JSON/Protobuf等)
-
安全认证机制:
- 集成TLS/SSL加密传输
- 支持多种认证方式(Token、OAuth2.0、API Key等)
- 提供自动令牌刷新功能
-
服务质量保障:
- 实现消息重传机制(最大重试次数可配置)
- 支持消息优先级队列
- 提供离线消息缓存(本地存储最近100条消息)
-
跨平台支持:
- 提供Android、iOS、Web等多平台SDK
- 统一API设计,保持各平台接口一致性
- 支持自动版本检测和热更新
-
监控与诊断:
- 内置连接质量监测(延迟、丢包率等)
- 提供诊断日志输出(可配置日志级别)
- 支持网络环境模拟(用于测试弱网情况)
-
多标签共享连接
- 浏览器多标签页共享同一个 websocket 连接,提高资源利用率
实现原理
一、服务端
1、技术选型
我们知道,websocket 是作用于传输层的全双工通信协议,既然是协议就表明是一种规范。要么我们自己基于此规范去实现通信细节,要么寻找市面上主流的实现以快速接入业务。
目前市面上常见的 websocket 实现如下:
| 实现方案 | 类型 | 核心特点 | 易用性 | 性能 | 扩展性 | 生态支持 | 适用场景 |
|---|---|---|---|---|---|---|---|
| javax.websocket | Java EE标准API | 定义WebSocket接口规范,容器无关 | 中等 | 依赖容器实现 | 容器限制 | Java EE生态 | Java Web应用基础WebSocket功能 |
| Socket.IO | 跨平台库 | 支持WebSocket+轮询降级,自动重连,事件驱动 | 高 | 中(额外协议开销) | 中 | 前端生态强 | 浏览器与服务器实时通信,需兼容性保障的场景 |
| netty-websocket | Netty组件 | 基于NIO,零拷贝,自定义编解码,底层可控 | 低(需手动处理底层) | 极高 | 极高 | Netty生态 | 高性能要求的系统,如游戏服务器、大规模实时推送 |
| spring-websocket | Spring框架封装 | 集成STOMP协议,支持消息代理,与Spring生态无缝整合 | 极高 | 中高 | 高 | Spring生态 | 企业级Web应用,需与Spring集成的实时功能 |
| spring-webflux websocket | 响应式Web框架组件 | 基于Reactor的响应式编程模型,支持背压,非阻塞I/O,与WebFlux生态整合 | 中高(需理解响应式编程) | 极高 | 高 | Spring WebFlux生态 | 响应式应用架构,高并发WebSocket连接,需要背压支持的实时数据流 |
| Vert.x | 响应式框架 | 基于Netty,异步非阻塞,支持多语言,轻量级 | 中高 | 极高 | 高 | Vert.x生态 | 微服务、高性能API、实时应用,需响应式编程的场景 |
1) javax.websocket
- 优势 :
Java官方标准,兼容性好,所有Java EE容器都支持 - 劣势 :API较基础,功能单一,需要自行处理很多底层逻辑
- 适用场景 :简单的 WebSocket 应用,对标准化要求高的企业环境
2) Socket.IO
- 优势 :全栈解决方案,客户端库成熟,自动降级机制(兼容不支持WebSocket的浏览器),房间/命名空间等高级功能
- 劣势 :不是纯 WebSocke t实现,有额外开销,在Java后端集成不如原生方案紧密
- 适用场景 :需要跨平台、全栈实时通信,对兼容性要求高的 Web 应用
3) netty-websocket
- 优势 :
性能极高,资源消耗低,可处理海量并发连接,高度可定制 - 劣势 :学习曲线陡峭,API复杂,需要手写大量代码
- 适用场景 :高性能要求,需要处理百万级并发连接的服务端应用
4) spring-websocket
- 优势 :与 Spring 生态深度集成,提供 STOMP 支持,声明式编程模型,安全性好,开发效率高
- 劣势 :基于同步模型,高并发下资源占用大,性能瓶颈明显;依赖 Spring 容器;
- 适用场景 :企业级应用,需要与 Spring 框架集成,需要高级消息特性(订阅/发布)
5) spring-webflux websocket
- 优势 :基于Reactor的响应式编程模型,天然支持高并发连接(上万并发),与WebFlux生态深度整合,适合构建全响应式架构
- 劣势 :有一定的学习成本,如 Reactor、Netty等
- 适用场景 :需要处理数千/数万并发连接的实时应用,如实时监控、数据流处理等(已采用WebFlux技术栈时优先)。
6) Vert.x
- 优势 :反应式编程模型,高性能,支持多种编程语言,API简洁,生态丰富
- 劣势 :反应式编程思维有学习成本,社区规模不如 Spring 庞大;与 Spring 体系整合不如 Spring-webflux 等嫡系
- 适用场景 :需要非阻塞高性能,多语言开发团队,微服务架构
成本对比:
| 实现方案 | 学习曲线 | 开发复杂度 | 维护成本 | 调试难度 | 团队技能要求 |
|---|---|---|---|---|---|
| javax.websocket | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| Socket.IO | ⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐ |
| netty-websocket | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| spring-websocket | ⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐ |
| spring-webflux websocket | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Vert.x | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
选择
基于目前公司使用 Spring 系列,为更好的利用 Spring 生态,同时考虑通用型平台推送能力,需要考虑并发性。
综上,我们采用 Spring-webflux websocket 方案,核心理由如下:
- 极致的高并发处理能力
- 响应式编程模型 :基于Reactor的 Flux / Mono ,单线程可处理数千并发连接,资源利用率提升3-5倍
- 非阻塞I/O :底层基于 Netty 的 NIO 架构,避免线程阻塞,支持百万级连接(需合理调优)
- 内置背压机制 :自动调节消息推送速率,防止下游消费者过载,保障系统稳定性
- 高效内存管理 :连接复用与零拷贝技术,降低内存占用和 GC 压力
- 与Spring生态深度整合
- 无缝集成 :与Spring Boot、Spring Cloud、Spring Security等组件开箱即用
- 统一配置体系 :复用Spring的配置、监控、日志等基础设施,无需额外适配
- 安全支持 :内置Spring Security集成,支持 JWT、OAuth2 等认证授权机制,适合多租户平台
- 强大的功能特性
- 多推送模式支持 :原生支持广播( /topic )、点对点( /queue )、用户专属( /user )推送
- STOMP协议兼容 :可通过 spring-messaging 启用STOMP,提供标准化消息模型和客户端生态
- 消息代理集成 :支持与 RabbitMQ、Kafka 等外部代理对接,实现大规模消息路由和集群协同
- 连接生命周期管理 :提供完整的连接建立、心跳检测、断开重连机制,便于在线状态统计
- 社区活跃
- 简洁API设计 :基于注解和函数式编程,开发效率比原生 Netty 提升50%+
- 完善文档支持 :Spring官方文档详尽,社区活跃,问题易排查
- 良好扩展性 :支持自定义消息处理器、编解码器,轻松扩展业务逻辑
- 版本兼容性 :严格遵循 Spring 版本兼容策略,升级成本低
特殊场景下的替代选择:
- 极致性能需求 :选择 netty-websocket
- 跨平台全栈应用 :选择 Socket.IO
- 反应式编程架构 :选择 Vert.x
- 纯标准合规要求 :选择 javax.websocket
Demo
以下是 Spring-webflux websocket 的使用 case:
xml
<dependencies>
<!-- WebFlux 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 消息支持(STOMP) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
启动类:
java
package com.example.websocket;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebfluxPushApplication {
public static void main(String[] args) {
SpringApplication.run(WebfluxPushApplication .class, args);
}
}
websocket 配置类:
java
package com.example.websocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker // 启用WebSocket消息代理
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 1. 启用内存消息代理(生产环境建议使用RabbitMQ/Kafka)
// 配置广播目的地前缀:/topic(多用户接收)、/queue(单用户接收)
config.enableSimpleBroker("/topic", "/queue");
// 2. 配置应用目的地前缀:客户端发送消息到服务端时使用
config.setApplicationDestinationPrefixes("/app");
// 3. 配置用户专属目的地前缀:用于单用户推送
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 1. 注册WebSocket端点:客户端通过此地址建立连接
registry.addEndpoint("/ws")
.setAllowedOrigins("*") // 允许所有来源(生产环境需限制)
.withSockJS(); // 启用SockJS支持(可选,用于浏览器不支持WebSocket时降级)
}
}
消息模型类:
java
package com.example.websocket.model;
import java.time.LocalDateTime;
/**
* 推送消息模型
*/
public class PushMessage {
// 消息ID
private String id;
// 消息类型(如:notification, alert, data)
private String type;
// 消息内容
private String content;
// 目标用户ID(为空表示广播)
private String targetUserId;
// 发送时间
private LocalDateTime timestamp;
...
}
消息推送入口:
java
package com.example.websocket.controller;
import com.example.websocket.model.PushMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import java.util.UUID;
@RestController
@RequestMapping("/api/push")
public class PushController {
// 注入消息模板,用于主动推送消息
@Autowired
private SimpMessagingTemplate messagingTemplate;
/**
* 1. 广播消息接口(REST API)
* 客户端调用此接口,服务端向所有连接的客户端广播消息
*/
@PostMapping("/broadcast")
public Mono<PushMessage> broadcastMessage(@RequestBody PushMessage message) {
// 生成消息ID
message.setId(UUID.randomUUID().toString());
// 向所有订阅了 /topic/broadcast 的客户端推送消息
messagingTemplate.convertAndSend("/topic/broadcast", message);
return Mono.just(message);
}
/**
* 2. 单用户推送接口(REST API)
* 客户端调用此接口,服务端向指定用户推送消息
*/
@PostMapping("/user/{userId}")
public Mono<PushMessage> pushToUser(@PathVariable String userId, @RequestBody PushMessage message) {
// 生成消息ID
message.setId(UUID.randomUUID().toString());
message.setTargetUserId(userId);
// 向指定用户推送消息,目的地格式:/user/{userId}/queue/private
messagingTemplate.convertAndSendToUser(
userId,
"/queue/private",
message
);
return Mono.just(message);
}
/**
* 3. WebSocket客户端发送消息,服务端广播响应(STOMP接口)
* 客户端通过 STOMP 发送消息到 /app/chat,服务端广播到 /topic/chat
*/
@MessageMapping("/chat") // 客户端发送消息到 /app/chat
@SendTo("/topic/chat") // 服务端广播到 /topic/chat
public PushMessage handleChatMessage(PushMessage message) {
message.setId(UUID.randomUUID().toString());
message.setType("chat");
return message;
}
}
流程图
Spring WebFlux + STOMP + WebSocket 通信流程。
1)WebSocket 连接建立流程 :
消息代理 STOMP 协议层 WebFlux 框架 Netty 服务器 SockJS 客户端 浏览器客户端 消息代理 STOMP 协议层 WebFlux 框架 Netty 服务器 SockJS 客户端 浏览器客户端 创建 SockJS 连接 HTTP 握手请求 (/ws) 路由到 WebSocketHandler 初始化 STOMP 处理链 启动内存消息代理 返回 STOMP 帧 (CONNECTED) 连接建立成功
Reactor 作用 :
- 使用 Mono 和 Flux 处理异步连接事件
- 提供背压机制控制连接建立速率
- 响应式地处理连接生命周期事件
Netty 作用 :
- 作为底层网络通信框架,处理 TCP 连接
- 使用 NIO 多路复用技术处理并发连接
- 零拷贝技术优化数据传输性能
- 负责 HTTP 握手升级为 WebSocket 协议
2)客户端消息发送:
消息代理 消息处理器 STOMP 协议层 Reactor 响应式流 Netty 服务器 浏览器客户端 消息代理 消息处理器 STOMP 协议层 Reactor 响应式流 Netty 服务器 浏览器客户端 发送 STOMP 消息 (SEND /app/chat) 将字节流转换为 Flux<DataBuffer> 解码为 STOMP 帧 路由到 @MessageMapping 方法 处理后发送到目的地 (/topic/chat) 广播消息到所有订阅者
Reactor 作用 :
- 将二进制数据流转为 Flux 进行异步处理
- 使用响应式操作符 ( map , filter , flatMap ) 处理消息流
3)服务端推送流程
浏览器客户端 Netty 服务器 STOMP 协议层 消息代理 Reactor 响应式流 SimpMessagingTemplate REST API 调用 浏览器客户端 Netty 服务器 STOMP 协议层 消息代理 Reactor 响应式流 SimpMessagingTemplate REST API 调用 调用 convertAndSend() 发送消息到目的地 (/topic/broadcast) 将消息转换为响应式流 编码为 STOMP 帧 转换为二进制数据 通过 WebSocket 推送消息 解码并显示消息
STOMP 协议层与消息路由
- 目的地路由 :基于 /topic 、 /queue 、 /app 等前缀的消息路由
- 订阅管理 :维护客户端订阅关系,支持动态订阅和取消
- 消息转换 :自动处理 JSON/XML 与 Java 对象的转换
- 事务支持 :确保消息处理的原子性
4)单用户推送流程
目标客户端 Netty 服务器 Reactor 响应式流 用户会话管理器 SimpMessagingTemplate REST API 调用 目标客户端 Netty 服务器 Reactor 响应式流 用户会话管理器 SimpMessagingTemplate REST API 调用 调用 convertAndSendToUser() 查找用户会话 构建用户专属消息流 发送到特定用户连接 单用户推送
2、服务端负载均衡问题
连接的负载均衡轮询策略存在连接数不均衡的问题,主要原因包括:
- 传统轮询策略的局限性
- 采用简单的轮询分发算法,仅按顺序分配新连接
- 无法感知后端实例的实时连接状态
- 不考虑各实例当前的连接负载情况
- 扩容/发版场景下的问题
- 新扩容实例初始连接数为0,但轮询策略仍平均分配
- 发版时重启的实例会断开所有现有连接
- 重新加入集群后被当作新实例对待,导致连接分配不均
- 连接数失衡的后果
- 单实例可能承担过多连接(如其他实例的2-3倍)
- 当连接数超过实例处理能力(如CPU/内存阈值)时,会引发服务响应延迟、请求超时等问题,最终导致实例崩溃,产生连锁反应
典型场景示例:
- 初始4个实例,每个承载100连接(共400),扩容至5个实例,轮询分配新连接
- 最终可能出现4个旧实例120连接,新实例40连接,若某个旧实例达到150连接阈值就会崩溃
常见的解决方案:
- 方案一:设置自动保护机制,如达到连接数阈值,则该节点拒接建立连接;依赖客户端重连策略实现重新连接到负载低的节点。(实现简单,易用)
- 方案二:采用智能负载均衡算法(如最小连接数),实现连接数的实时监控和动态调整,实现整体节点连接数均衡。
- 方案三:支持平滑扩容时的连接预热机制,节点新增、删除后进行连接数重平衡,如 连接数迁移等。
方案一:阈值保护策略,实现较为简单,当某节点的连接达到阈值时,直接拒绝创建连接;同时依赖客户端的重连机制,重新发起连接。

比如,阈值我们可以设置为 1000,当达到阈值时,新连接进来直接拒绝,并进行阈值告警,便于监控。
方案二:最小连接数均衡算法
最小连接数均衡算法(Least Connections Load Balancing)是一种常用的负载均衡策略,其核心思想是将新的请求分配给当前活跃连接数最少的服务器,以实现服务器负载的动态平衡。
算法原理
- 实时监控 :负载均衡器持续跟踪每个后端服务器当前的活跃连接数量
- 动态分配 :当新请求到达时,算法会选择当前活跃连接数最少的服务器来处理请求
- 自动调节 :随着各服务器连接数的变化,请求分发会自动调整以维持平衡
实现特点
- 动态响应 :能够根据服务器实际负载情况动态调整请求分发,比静态轮询(Round Robin)更智能
- 考虑连接持续时间 :适合处理连接持续时间不一致的场景,如Web应用中不同请求处理时间差异大的情况
- 需要额外状态维护 :负载均衡器需要维护每个服务器的连接状态信息
优化变体
- 加权最小连接数(Weighted Least Connections) :考虑服务器性能差异,为性能更好的服务器分配更高权重
- 平滑加权最小连接数(Smooth Weighted Least Connections) :避免在权重相差较大时出现的请求分配不均
- 最小响应时间(Least Response Time) :结合连接数和响应时间进行综合判断
适用场景
- 处理长连接服务(如数据库连接池、WebSocket服务)
- 请求处理时间差异较大的Web应用
- 服务器性能不完全一致的集群环境
- 需要精确负载分布的高并发系统
与其他算法对比
相比于轮询(Round Robin)、IP哈希(IP Hash)等静态算法,最小连接数算法能更精确地反映服务器实际负载状态,提供更均衡的资源利用。但它需要额外的连接状态跟踪,实现复杂度略高。
实现流程图如下:

3、平稳发布
长连接服务发布需要采用渐进式灰度发布策略,确保服务连续性。与短链接服务不同,长连接服务需要特别关注每个客户端的连接状态和重连过程。具体步骤如下:
-
创建新节点
- 在负载均衡器上注册新节点
- 启动服务并运行健康检查(如心跳检测、内存监控等)
- 等待新节点稳定运行(通常需要5-10分钟观察期)
- 确认新节点能够正常接收和处理连接请求
-
下线旧节点
- 从负载均衡器摘除旧节点(停止新流量接入)
- 监控客户端重连情况(通过日志或监控系统)
- 观察连接迁移情况(确保90%以上连接已迁移至新节点)
- 等待旧节点连接数降为零(典型等待时间为2-5分钟)
- 对残留连接发送优雅关闭通知
-
循环迭代
- 重复上述过程,每次处理集群中10-20%的节点
- 在每批节点更新后,进行全链路测试
- 监控关键指标(QPS、延迟、错误率等)
通过这种滚动发布方式,可以最大限度减少服务中断时间,保证用户体验的连续性。
二、客户端
1、重连策略
指数退避重连
- 重连间隔随尝试次数指数增长(如1s, 2s, 4s, 8s...)
- 设置最大延迟上限,避免等待时间过长
抖动机制(Jitter)
- 在指数退避基础上添加随机抖动,避免多客户端同时重连造成服务器压力
js
// 添加抖动的重连延迟计算
const jitter = Math.random() * 0.3; // 0-30%的随机抖动
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts) * (1 + jitter),
this.maxReconnectDelay
);
心跳机制配合重连
- 定期发送ping消息检测连接活性
- 长时间无响应则主动触发重连
js
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send('ping');
this.lastHeartbeatTime = Date.now();
}
}, 30000); // 30秒发送一次心跳
// 检测心跳响应超时
this.heartbeatTimeoutInterval = setInterval(() => {
if (this.lastHeartbeatTime && Date.now() - this.lastHeartbeatTime > 60000) {
console.warn('心跳超时,主动断开连接');
this.ws.close();
}
}, 5000); // 5秒检查一次
}
// 在onmessage中处理pong响应
this.ws.onmessage = (event) => {
if (event.data === 'pong') {
this.lastHeartbeatTime = Date.now();
}
// 其他消息处理...
};
断线重连时的消息队列
- 连接断开期间,将待发送消息缓存到本地队列
- 重连成功后按序发送缓存消息 网络状态监听
- 监听浏览器online/offline事件,优化重连时机
js
window.addEventListener('online', () => {
console.log('网络已恢复,尝试重新连接');
if (this.ws && this.ws.readyState === WebSocket.CLOSED) {
this.connect();
}
});
window.addEventListener('offline', () => {
console.log('网络已断开');
});
2、多标签页连接共享的实现方案
在Web应用场景中,浏览器标签多开现象会导致严重的资源浪费问题。具体表现为:
- 连接数激增问题:
- 单个用户可能同时打开数十个浏览器标签,每个标签都会建立独立的 websocket 连接,导致服务器需要维护大量冗余连接会话
- 服务器资源影响:
- 连接池被无效连接占用,内存消耗呈线性增长(每个连接约占用50-100KB内存);线程/进程资源被大量消耗,数据库连接池压力倍增
- 客户端资源消耗:
- 每个标签独立加载全套前端资源(JS/CSS等),内存占用随标签数成倍增加,CPU利用率持续高位运行
优化方向:
- 尽可能保证连接的全局单例,以减少连接的创建和销毁频率,减少客户端 TIME_WAIT 数量和服务压力
- 尽可能在订阅多个主题时复用同一个连接,避免额外创建连接
常见的连接共享有以下几种方案:
1). localStorage 事件监听
-
原理:利用 localStorage 的跨标签页通信特性
-
实现步骤 :
- 主标签页将连接信息存储在 localStorage 中
- 其他标签页监听
storage事件 - 当检测到连接信息更新时,各标签页同步状态
-
示例代码 :
javascript// 发送方 localStorage.setItem('sharedConnection', JSON.stringify(connectionData)); // 接收方 window.addEventListener('storage', (event) => { if (event.key === 'sharedConnection') { const data = JSON.parse(event.newValue); // 处理连接数据 } });
2). Broadcast Channel API
-
原理:使用专门的跨标签页通信 API
-
特点 :
- 专为跨上下文通信设计
- 性能优于 localStorage 方案
- 支持传输复杂数据结构
-
实现示例 :
javascriptconst channel = new BroadcastChannel('connection_channel'); // 发送消息 channel.postMessage(connectionData); // 接收消息 channel.onmessage = (event) => { const data = event.data; // 处理连接数据 };
3). Service Worker 中转
- 适用场景 :
- PWA 应用
- 需要后台持续同步的场景
- 工作流程 :
- 主标签页通过 postMessage 通知 Service Worker
- Service Worker 广播给所有客户端
- 各标签页通过 message 事件接收更新
4). SharedWorker 方案
SharedWorker是浏览器提供的一种可以在多个同源标签页之间共享的Worker,非常适合用来共享WebSocket连接:
- 优势 :
- 真正的共享内存模型
- 适合高频通信场景
- 实现要点 :
- 创建 SharedWorker 实例
- 通过端口(port)进行双向通信
- 需要处理连接生命周期管理
shared-worker.js:
js
// SharedWorker实现 (shared-worker.js)
let clients = [];
let socket = null;
self.onconnect = function(e) {
const port = e.ports[0];
clients.push(port);
// 初始化WebSocket连接(如果尚未建立)
if (!socket) {
initWebSocket();
}
port.onmessage = function(e) {
// 处理来自页面的消息
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(e.data);
}
};
port.start();
};
function initWebSocket() {
socket = new WebSocket('wss://example.com/socket');
socket.onmessage = function(e) {
// 向所有连接的页面广播消息
clients.forEach(client => {
try {
client.postMessage(e.data);
} catch (err) {
// 移除失效的连接
clients = clients.filter(c => c !== client);
}
});
};
socket.onclose = function() {
socket = null;
// 通知所有页面连接已关闭
clients.forEach(client => client.postMessage('CONNECTION_CLOSED'));
};
}
main.js:
js
// 页面中使用 (main.js)
const worker = new SharedWorker('shared-worker.js');
worker.port.onmessage = function(e) {
if (e.data === 'CONNECTION_CLOSED') {
console.log('连接已关闭');
} else {
console.log('收到消息:', e.data);
// 处理接收到的WebSocket消息
}
};
worker.port.start();
// 发送消息的函数
function sendMessage(message) {
worker.port.postMessage(message);
}
方案比较:
| 方案 | 适用场景 | 性能 | 复杂度 | 浏览器支持 |
|---|---|---|---|---|
| localStorage | 简单数据同步 | 中等 | 低 | 广泛 |
| BroadcastChannel | 中复杂度通信 | 高 | 中 | 现代浏览器 |
| ServiceWorker | PWA/后台同步 | 中高 | 高 | 现代浏览器 |
| SharedWorker | 高频/复杂通信 | 最高 | 最高 | 现代浏览器 |
基于通用性考虑,我们通过 SharedWorker 来实现连接共享。
总结
本文不只是针对 Websocket 技术,而是设计一款服务端推送平台,考虑通用性和可扩展性。同时利用 Websocket 长连接的能力,来实现这一个目标。
在服务端的具体实现上,对于 Spring 生态下的企业级应用, Spring WebFlux + STOMP 方案提供了最佳的平衡点:
- 利用响应式编程模型处理高并发连接
- 通过标准化协议简化开发和维护
- 与 Spring 生态深度整合,降低技术栈复杂度
- 支持灵活的扩展和优化,满足不同规模应用的需求
客户端实现则需要重点关注 连接稳定性 和 用户体验 ,通过合理的重连策略和连接共享方案提升系统可靠性和性能。