WebSocket推送架构设计

目录

前言

我们在之前的文章中讲了 服务端推送方案之SSE点击查看),今天我们要讲的是另外一种方案 - 全双工的 WebSocket 来实现。WebSocket 应用非常广泛,常见的比如 IM 双端通信能力,今天我们不聊IM,而是专注另外一个常见的场景 - 服务端推送

为什么需要服务端推送?

想象一下,当服务端推送能力缺失时,如果想要客户端主动感知数据变化,则只能通过客户端轮询方式实现,由此带来多种问题:

  1. 资源浪费显著:大量轮询请求结果为空,造成客户端和服务端计算资源的无端消耗
  2. 实时性不足:轮询间隔导致信息更新存在时间差,无法满足实时通知的业务需求
  3. 体验与效率受损:在实时性要求高的场景下,信息延迟直接影响用户体验和业务作业效率

值得注意的是,许多大型企业为了更好适配自身业务需求,往往会自主研发基于TCP协议的应用层协议。这种定制化方案能够带来更优的性能表现,阿里、腾讯、字节跳动等科技巨头都采用了这一实践。

常见的有:

公司 自研组件 核心特点 应用场景
阿里巴巴 Mars 跨平台网络通信框架,支持双向通信、多路复用、弱网络优化 移动端长连接、即时通讯、实时数据推送
腾讯 TIM Protocol 轻量级即时通讯协议,支持双向实时通信、多端同步、消息加密 微信、QQ等即时通讯应用的实时消息传输
字节 Bytedance-RTCP 轻量级实时传输协议,支持低延迟双向通信、流量控制 抖音、今日头条的实时内容推送和交互

架构设计

通用推送平台

从电商平台的订单状态更新、社交媒体的消息通知,到金融行业的实时行情推送、各类业务场景都需要可靠的推送服务。这些业务虽然表现形式各异,但本质上都涉及到 服务端主动向客户端 推送数据这一共性需求。

推送系统需要解决的基础问题:如何建立和维护长连接、如何处理消息队列、如何实现消息路由和分发、如何确保消息的可靠投递等。这些技术挑战与具体的业务领域无关,具有高度的通用性。因此,将其设计为通用的独立推送平台是合理的架构选择

这种设计具有以下优势

  1. 避免重复建设:各业务线无需各自实现推送功能,业务开发团队可以专注于核心业务功能。
  2. 统一运维管理:集中式的平台更便于监控和维护,资源共享等。

典型的推送平台架构应该包含以下核心组件

  1. 连接管理:负责维护客户端长连接
  2. 队列服务:处理消息的缓冲和持久化
  3. 消息路由:根据订阅关系分发消息
  4. 状态监控:实时掌握系统运行状况
  5. 管理接口:提供配置和查询能力

这种架构模式已经在业界得到广泛验证,如微信的WNS推送系统、阿里的移动推送服务等,都采用了类似的独立平台设计。

WebSocket 推送架构整体图

一、连接/会话管理

连接/会话管理是客户端与服务器之间建立的通信通道进行创建、维护和终止的过程。它是网络通信和分布式系统中的核心组件,直接影响系统的性能、可靠性和安全性。

主要功能:

  1. 连接建立:三次握手协议(TCP)、安全连接建立(SSL/TLS)、身份验证过程、资源分配和初始化

  2. 会话维护:心跳机制保持连接活跃、超时检测和重连机制、会话状态同步、流量控制和拥塞管理

  3. 连接终止: 正常关闭流程、异常中断处理、资源回收、会话日志记录

二、消息队列

服务节点以分布式方式各自维护连接信息。当服务端推送消息时,如何定位到目标消息对应的具体连接?

方案一:维护路由表

流程:

  1. 每个节点创建连接后,将连接信息同步至路由表
  2. 业务方通过统一推送入口-PushFacade 推送消息。
  3. PushFacade 收到消息后,先从路由表查询连接所在节点,比如查询到节点 Node A
  4. 再将消息点对点推送至 Node A
优点 缺点
点对点推送,各节点处理效率高 路由表需要解决单点问题

为了提高查询效率,我们可以采用 Redis 来存储路由表数据。整个系统设计无需依赖队列机制即可实现高效运行。

方案二:使用消息队列

流程:

  1. 业务服务通过 PushFacade 推送消息,消息投递至 MQ
  2. MQ 监听到消息后,广播至所有推送节点
  3. 每个推送节点监听到消息后,判断自己本地路由表是否有对应连接,存在连接则将消息推送给客户端
优点 缺点
架构简单,复杂度低 依赖MQ广播消息,所有节点都会收到,依赖节点自身过滤机制,推送量巨大时,有一定的性能损失
系统解耦,抗压能力更强
节点负载对等(通过负载均衡器实现)
无扩容负担,通过负载均衡器实现连接均衡分布

如果消息量级(QPS) 不是特别高,这种方案实现最为方便,可以满足大部分场景。

方案三:消息队列 + 路由表

有效结合方案一 + 方案二:通过消息队列(MQ)解耦上下游系统,同时结合路由表动态分发消息,确保不同业务场景的消息能精准投递到对应的处理节点。

优缺点对比:

优点 缺点
解耦彻底,扩展性强 需维护路由表,增加运维成本
支持动态调整路由策略 消息队列本身存在延迟(毫秒级)
故障隔离(单节点异常不影响整体) 需额外监控消息堆积和消费状态

方案三更为完善,但维护成本相对较高,建议根据系统实际情况综合考量。

队列的必要性

综上,在 WebSocket 服务端架构中,队列扮演着至关重要的角色,主要用于解决高并发场景下的消息处理问题。当大量客户端同时连接时,队列能够有效缓冲消息流量,避免服务端过载。

1. 消息缓冲与流量控制

  • 突发流量处理:当短时间内大量消息涌入时,队列作为缓冲区平滑处理峰值
  • 速率限制:通过队列控制消息处理速率,避免后端服务被压垮
  • 优先级处理:实现不同优先级消息的分类处理(如VIP用户消息优先)

2. 解耦生产与消费

  • 异步处理:生产者(客户端)和消费者(处理程序)通过队列解耦
  • 削峰填谷:高峰期积压的消息可在低峰期逐步消化
  • 错误隔离:单个消息处理失败不会影响队列中其他消息

队列机制确保了 WebSocket 服务在高并发下的稳定性、可靠性和可扩展性。

综上所述,综合考虑维护成本和实际需求,我们决定采用方案二进行实施。该方案具备良好的扩展性,即使后期需求变更,也能快速调整为方案三的架构。

三、消息路由/订阅

消息路由,即为消息转发 。作为经验丰富的从业者,你肯定清楚,一个业务系统往往需要处理多种不同类型的业务需求,例如下载通知、业务消息通知等。针对这种情况,我们可以将其设计 基于主题的订阅模式,有以下优点:

  1. 业务方可以灵活区分主题,做出不同的响应
  2. 可以实现更细粒度(主题)的管控、扩展性更强

模式如下:

其中:订阅消息格式: {"type":"subscribe", "topics":["topic1","topic2"]}、支持动态订阅和取消订阅

四、多租户设计

我们可以更进一步考虑,如果想要作为基础平台能力建设,比如想要推广至不同业务条线(如零售业务、对公业务、资管业务等),或者同一条线下不同的业务方向(如零售业务下的信用卡项目、个人贷款项目、理财项目等)使用,为了避免相互影响,可以设计为多租户架构

本文案例以支撑公司内部基础平台建设为目标,大致功能有:

  1. 基于租户ID字段进行租户数据隔离
  2. 为每个租户提供并发等流量管控
  3. 提供租户维度的接入、权限管理 及 租户管理功能
  4. 指标监控:租户及租户下不同主题维度的数据指标监控

架构图如下:

通过这种架构设计,既能实现资源共享降低成本,又能确保各业务单元的数据隔离和独立运营,为平台的规模化应用提供可靠支撑。

五、可视化数据监控

推送平台具备可视化的连接管理能力,为每个应用提供全面的连接监控管理功能。具体包括:

  1. 实时连接监控视图:

    • 连接的物理/逻辑节点位置(如北京节点A、上海节点B等)
    • 客户端运行环境(操作系统版本、设备型号、SDK版本等)
    • 网络质量指标(延迟、丢包率、带宽等)
    • 连接持续时间及活跃状态
  2. 精细化连接管理:

    • 剔除下线:强制终止指定连接,适用于异常连接处理
    • 复位重连:重置连接状态,触发客户端重新建立连接
    • 连接迁移:将连接转移到其他节点,实现负载均衡
  3. 历史连接分析:

    • 提供连接历史记录查询功能
    • 支持按时间、节点、客户端属性等多维度筛选

1)租户管理控制台

2)连接/消息流量监控

五、客户端SDK

为简化客户端接入流程,实现标准化接入并降低接入成本,我们将客户端连接能力进行抽象化封装,提供独立的客户端连接SDK。通过引入该SDK,客户端可便捷使用相关功能,无需关注底层业务实现。SDK需具备以下核心能力:

  1. 客户端连接生命周期管理

    • 提供完整的连接状态管理机制,包括初始化、连接建立、重连、断开连接等全流程控制
    • 实现自动重连策略,支持指数退避算法(如初始重连间隔1秒,最大间隔30秒)
    • 内置心跳检测机制,默认30秒发送一次心跳包,可配置心跳超时时间(如120秒无响应自动断开)
    • 提供连接状态回调接口,支持 onConnected、onDisconnected、onReconnecting 等事件通知
  2. 协议适配与封装

    • 支持主流通信协议(WebSocket、HTTP/2、MQTT等)的统一封装
    • 提供协议转换层,屏蔽底层协议差异
    • 内置消息序列化/反序列化处理(JSON/Protobuf等)
  3. 安全认证机制

    • 集成TLS/SSL加密传输
    • 支持多种认证方式(Token、OAuth2.0、API Key等)
    • 提供自动令牌刷新功能
  4. 服务质量保障

    • 实现消息重传机制(最大重试次数可配置)
    • 支持消息优先级队列
    • 提供离线消息缓存(本地存储最近100条消息)
  5. 跨平台支持

    • 提供Android、iOS、Web等多平台SDK
    • 统一API设计,保持各平台接口一致性
    • 支持自动版本检测和热更新
  6. 监控与诊断

    • 内置连接质量监测(延迟、丢包率等)
    • 提供诊断日志输出(可配置日志级别)
    • 支持网络环境模拟(用于测试弱网情况)
  7. 多标签共享连接

    • 浏览器多标签页共享同一个 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 方案,核心理由如下:

  1. 极致的高并发处理能力
  • 响应式编程模型 :基于Reactor的 Flux / Mono ,单线程可处理数千并发连接,资源利用率提升3-5倍
  • 非阻塞I/O :底层基于 Netty 的 NIO 架构,避免线程阻塞,支持百万级连接(需合理调优)
  • 内置背压机制 :自动调节消息推送速率,防止下游消费者过载,保障系统稳定性
  • 高效内存管理 :连接复用与零拷贝技术,降低内存占用和 GC 压力
  1. 与Spring生态深度整合
  • 无缝集成 :与Spring Boot、Spring Cloud、Spring Security等组件开箱即用
  • 统一配置体系 :复用Spring的配置、监控、日志等基础设施,无需额外适配
  • 安全支持 :内置Spring Security集成,支持 JWT、OAuth2 等认证授权机制,适合多租户平台
  1. 强大的功能特性
  • 多推送模式支持 :原生支持广播( /topic )、点对点( /queue )、用户专属( /user )推送
  • STOMP协议兼容 :可通过 spring-messaging 启用STOMP,提供标准化消息模型和客户端生态
  • 消息代理集成 :支持与 RabbitMQ、Kafka 等外部代理对接,实现大规模消息路由和集群协同
  • 连接生命周期管理 :提供完整的连接建立、心跳检测、断开重连机制,便于在线状态统计
  1. 社区活跃
  • 简洁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、服务端负载均衡问题

连接的负载均衡轮询策略存在连接数不均衡的问题,主要原因包括:

  1. 传统轮询策略的局限性
  • 采用简单的轮询分发算法,仅按顺序分配新连接
  • 无法感知后端实例的实时连接状态
  • 不考虑各实例当前的连接负载情况
  1. 扩容/发版场景下的问题
  • 新扩容实例初始连接数为0,但轮询策略仍平均分配
  • 发版时重启的实例会断开所有现有连接
  • 重新加入集群后被当作新实例对待,导致连接分配不均
  1. 连接数失衡的后果
  • 单实例可能承担过多连接(如其他实例的2-3倍)
  • 当连接数超过实例处理能力(如CPU/内存阈值)时,会引发服务响应延迟、请求超时等问题,最终导致实例崩溃,产生连锁反应

典型场景示例:

  • 初始4个实例,每个承载100连接(共400),扩容至5个实例,轮询分配新连接
  • 最终可能出现4个旧实例120连接,新实例40连接,若某个旧实例达到150连接阈值就会崩溃

常见的解决方案:

  1. 方案一:设置自动保护机制,如达到连接数阈值,则该节点拒接建立连接;依赖客户端重连策略实现重新连接到负载低的节点。(实现简单,易用)
  2. 方案二:采用智能负载均衡算法(如最小连接数),实现连接数的实时监控和动态调整,实现整体节点连接数均衡。
  3. 方案三:支持平滑扩容时的连接预热机制,节点新增、删除后进行连接数重平衡,如 连接数迁移等。

方案一:阈值保护策略,实现较为简单,当某节点的连接达到阈值时,直接拒绝创建连接;同时依赖客户端的重连机制,重新发起连接。

比如,阈值我们可以设置为 1000,当达到阈值时,新连接进来直接拒绝,并进行阈值告警,便于监控

方案二:最小连接数均衡算法

最小连接数均衡算法(Least Connections Load Balancing)是一种常用的负载均衡策略,其核心思想是将新的请求分配给当前活跃连接数最少的服务器,以实现服务器负载的动态平衡。

算法原理

  1. 实时监控 :负载均衡器持续跟踪每个后端服务器当前的活跃连接数量
  2. 动态分配 :当新请求到达时,算法会选择当前活跃连接数最少的服务器来处理请求
  3. 自动调节 :随着各服务器连接数的变化,请求分发会自动调整以维持平衡

实现特点

  • 动态响应 :能够根据服务器实际负载情况动态调整请求分发,比静态轮询(Round Robin)更智能
  • 考虑连接持续时间 :适合处理连接持续时间不一致的场景,如Web应用中不同请求处理时间差异大的情况
  • 需要额外状态维护 :负载均衡器需要维护每个服务器的连接状态信息

优化变体

  1. 加权最小连接数(Weighted Least Connections) :考虑服务器性能差异,为性能更好的服务器分配更高权重
  2. 平滑加权最小连接数(Smooth Weighted Least Connections) :避免在权重相差较大时出现的请求分配不均
  3. 最小响应时间(Least Response Time) :结合连接数和响应时间进行综合判断

适用场景

  • 处理长连接服务(如数据库连接池、WebSocket服务)
  • 请求处理时间差异较大的Web应用
  • 服务器性能不完全一致的集群环境
  • 需要精确负载分布的高并发系统

与其他算法对比

相比于轮询(Round Robin)、IP哈希(IP Hash)等静态算法,最小连接数算法能更精确地反映服务器实际负载状态,提供更均衡的资源利用。但它需要额外的连接状态跟踪,实现复杂度略高

实现流程图如下:

3、平稳发布

长连接服务发布需要采用渐进式灰度发布策略,确保服务连续性。与短链接服务不同,长连接服务需要特别关注每个客户端的连接状态和重连过程。具体步骤如下:

  1. 创建新节点

    • 在负载均衡器上注册新节点
    • 启动服务并运行健康检查(如心跳检测、内存监控等)
    • 等待新节点稳定运行(通常需要5-10分钟观察期)
    • 确认新节点能够正常接收和处理连接请求
  2. 下线旧节点

    • 从负载均衡器摘除旧节点(停止新流量接入)
    • 监控客户端重连情况(通过日志或监控系统)
    • 观察连接迁移情况(确保90%以上连接已迁移至新节点)
    • 等待旧节点连接数降为零(典型等待时间为2-5分钟)
    • 对残留连接发送优雅关闭通知
  3. 循环迭代

    • 重复上述过程,每次处理集群中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应用场景中,浏览器标签多开现象会导致严重的资源浪费问题。具体表现为:

  1. 连接数激增问题:
  • 单个用户可能同时打开数十个浏览器标签,每个标签都会建立独立的 websocket 连接,导致服务器需要维护大量冗余连接会话
  1. 服务器资源影响:
  • 连接池被无效连接占用,内存消耗呈线性增长(每个连接约占用50-100KB内存);线程/进程资源被大量消耗,数据库连接池压力倍增
  1. 客户端资源消耗:
  • 每个标签独立加载全套前端资源(JS/CSS等),内存占用随标签数成倍增加,CPU利用率持续高位运行
  1. 优化方向
  • 尽可能保证连接的全局单例,以减少连接的创建和销毁频率,减少客户端 TIME_WAIT 数量和服务压力
  • 尽可能在订阅多个主题时复用同一个连接,避免额外创建连接

常见的连接共享有以下几种方案:

1). localStorage 事件监听
  • 原理:利用 localStorage 的跨标签页通信特性

  • 实现步骤

    1. 主标签页将连接信息存储在 localStorage 中
    2. 其他标签页监听 storage 事件
    3. 当检测到连接信息更新时,各标签页同步状态
  • 示例代码

    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 方案
    • 支持传输复杂数据结构
  • 实现示例

    javascript 复制代码
    const channel = new BroadcastChannel('connection_channel');
    
    // 发送消息
    channel.postMessage(connectionData);
    
    // 接收消息
    channel.onmessage = (event) => {
      const data = event.data;
      // 处理连接数据
    };
3). Service Worker 中转
  • 适用场景
    • PWA 应用
    • 需要后台持续同步的场景
  • 工作流程
    1. 主标签页通过 postMessage 通知 Service Worker
    2. Service Worker 广播给所有客户端
    3. 各标签页通过 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 生态深度整合,降低技术栈复杂度
  • 支持灵活的扩展和优化,满足不同规模应用的需求

客户端实现则需要重点关注 连接稳定性 和 用户体验 ,通过合理的重连策略和连接共享方案提升系统可靠性和性能。

相关推荐
代码游侠2 小时前
应用——SQLite3 C 编程学习
linux·服务器·c语言·数据库·笔记·网络协议·sqlite
水星灭绝2 小时前
测试http下载
网络·网络协议·http
秋深枫叶红2 小时前
嵌入式第四十一篇——网络编程——udp和tcp
网络·网络协议·学习·udp
开***能2 小时前
精准控能耗,协议零阻碍!EtherCAT转 Profinet网关技术赋能
服务器·网络·人工智能
QT 小鲜肉2 小时前
【Linux命令大全】001.文件管理之split命令(实操篇)
linux·运维·服务器·网络·笔记
草莓熊Lotso2 小时前
Qt 入门核心指南:从框架认知到环境搭建 + Qt Creator 实战
xml·开发语言·网络·c++·人工智能·qt·页面
寂寞恋上夜2 小时前
边界条件检查清单:数据为空/超长/特殊字符/越界(附测试用例)
服务器·网络·测试用例·markdown转xmind·在线思维导图生成器
松涛和鸣2 小时前
42、SQLite3 :字典入库与数据查询
linux·前端·网络·数据库·udp·sqlite
QT 小鲜肉2 小时前
【Linux命令大全】001.文件管理之rcp命令(实操篇)
linux·服务器·网络·chrome·笔记