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

Websocket集群解决方案以及实战(附图文源码)

最近在项目中在做一个消息推送的功能,比如客户下单之后通知给给对应的客户发送系统通知,这种消息推送需要使用到全双工的websocket推送消息。

所谓的全双工表示客户端和服务端都能向对方发送消息。不使用同样是全双工的http是因为http只能由客户端主动发起请求,服务接收后返回消息。websocket建立起连接之后,客户端和服务端都能主动向对方发送消息。

websocket在单机模式下进行消息的发送和接收:
在这里插入图片描述

用户A和用户B和web服务器建立连接之后,用户A发送一条消息到服务器,服务器再推送给用户B,在单机系统上所有的用户都和同一个服务器建立连接,所有的session都存储在同一个服务器中。

单个服务器是无法支撑几万人同时连接同一个服务器,需要使用到分布式或者集群将请求连接负载均衡到到不同的服务下。消息的发送方和接收方在同一个服务器,这就和单体服务器类似,能成功接收到消息:

在这里插入图片描述
但负载均衡使用轮询的算法,无法保证消息发送方和接收方处于同一个服务器,当发送方和接收方不是在同一个服务器时,接收方是无法接受到消息的:

在这里插入图片描述

websocket集群问题解决思路
客户端和服务端每次建立连接时候,会创建有状态的会话session,服务器的保存维持连接的session。客户端每次只能和集群服务器其中的一个服务器连接,后续也是和该服务器进行数据传输。

要解决集群的问题,应该考虑session共享的问题,客户端成功连接服务器之后,其他服务器也知道客户端连接成功。

方案一:session 共享(不可行)
和websocket类似的http是如何解决集群问题的?解决方案之一就是共享session,客户端登录服务端之后,将session信息存储在Redis数据库中,连接其他服务器时,从Redis获取session,实际就是将session信息存储在Redis中,实现redis的共享。

session可以被共享的前提是可以被序列化,而websocket的session是无法被序列化的,http的session记录的是请求的数据,而websocket的session对应的是连接,连接到不同的服务器,session也不同,无法被序列化。

方案二:ip hash(不可行)
http不使用session共享,就可以使用Nginx负载均衡的ip hash算法,客户端每次都是请求同一个服务器,客户端的session都保存在服务器上,而后续请求都是请求该服务器,都能获取到session,就不存在分布式session问题了。

websocket相对http来说,可以由服务端主动推动消息给客户端,如果接收消息的服务端和发送消息消息的服务端不是同一个服务端,发送消息的服务端无法找到接收消息对应的session,即两个session不处于同一个服务端,也就无法推送消息。如下图所示:

在这里插入图片描述

解决问题的方法是将所有消息的发送方和接收方都处于同一个服务器下,而消息发送方和接收方都是不确定的,显然是无法实现的。

方案三:广播模式
将消息的发送方和接收方都处于同一个服务器下才能发送消息,那么可以转换一下思路,可以将消息以消息广播的方式通知给所有的服务器,可以使用消息中间件发布订阅模式,消息脱离了服务器的限制,通过发送到中间件,再发送给订阅的服务器,类似广播一样,只要订阅了消息,都能接收到消息的通知:
在这里插入图片描述
发布者发布消息到消息中间件,消息中间件再将发送给所有订阅者:
在这里插入图片描述

广播模式的实现
搭建单机 websocket
参考以前写的websocket单机搭建 文章,先搭建单机websocket实现消息的推送。

  1. 添加依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 创建 ServerEndpointExporter 的 bean 实例
    ServerEndpointExporter 的 bean 实例自动注册 @ServerEndpoint 注解声明的 websocket endpoint,使用springboot自带tomcat启动需要该配置,使用独立 tomcat 则不需要该配置。
@Configuration
public class WebSocketConfig {//tomcat启动无需该配置@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
  1. 创建服务端点 ServerEndpoint 和 客户端端
    服务端点
@Component
@ServerEndpoint(value = "/message")
@Slf4j
public class WebSocket {private static Map<String, WebSocket> webSocketSet = new ConcurrentHashMap<>();private Session session;@OnOpenpublic void onOpen(Session session) throws SocketException {this.session = session;webSocketSet.put(this.session.getId(),this);log.info("【websocket】有新的连接,总数:{}",webSocketSet.size());}@OnClosepublic void onClose(){String id = this.session.getId();if (id != null){webSocketSet.remove(id);log.info("【websocket】连接断开:总数:{}",webSocketSet.size());}}@OnMessagepublic void onMessage(String message){if (!message.equals("ping")){log.info("【wesocket】收到客户端发送的消息,message={}",message);sendMessage(message);}}/*** 发送消息* @param message* @return*/public void sendMessage(String message){for (WebSocket webSocket : webSocketSet.values()) {webSocket.session.getAsyncRemote().sendText(message);}log.info("【wesocket】发送消息,message={}", message);}}

客户端点

<div><input type="text" name="message" id="message"><button id="sendBtn">发送</button>
</div>
<div style="width:100px;height: 500px;" id="content">
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script type="text/javascript">var ws = new WebSocket("ws://127.0.0.1:8080/message");ws.onopen = function(evt) {console.log("Connection open ...");};ws.onmessage = function(evt) {console.log( "Received Message: " + evt.data);var p = $("<p>"+evt.data+"</p>")$("#content").prepend(p);$("#message").val("");};ws.onclose = function(evt) {console.log("Connection closed.");};$("#sendBtn").click(function(){var aa = $("#message").val();ws.send(aa);})</script>

服务端和客户端中的OnOpen、onclose、onmessage都是一一对应的。

服务启动后,客户端ws.onopen调用服务端的@OnOpen注解的方法,储存客户端的session信息,握手建立连接。
客户端调用ws.send发送消息,对应服务端的@OnMessage注解下面的方法接收消息。
服务端调用session.getAsyncRemote().sendText发送消息,对应的客户端ws.onmessage接收消息。
添加 controller

@GetMapping({"","index.html"})
public ModelAndView index() {ModelAndView view = new ModelAndView("index");return view;
}

效果展示
打开两个客户端,其中的一个客户端发送消息,另一个客户端也能接收到消息。

在这里插入图片描述

添加 RabbitMQ 中间件
这里使用比较常用的RabbitMQ作为消息中间件,而RabbitMQ支持发布订阅模式:

添加消息订阅
交换机使用扇形交换机,消息分发给每一条绑定该交换机的队列。以服务器所在的IP + 端口作为唯一标识作为队列的命名,启动一个服务,使用队列绑定交换机,实现消息的订阅:

@Configuration
public class RabbitConfig {@Beanpublic FanoutExchange fanoutExchange() {return new FanoutExchange("PUBLISH_SUBSCRIBE_EXCHANGE");}@Beanpublic Queue psQueue() throws SocketException {// ip + 端口 为队列名 String ip = IpUtils.getServerIp() + "_" + IpUtils.getPort();return new Queue("ps_" + ip);}@Beanpublic Binding routingFirstBinding() throws SocketException {return BindingBuilder.bind(psQueue()).to(fanoutExchange());}
}

修改服务端点 ServerEndpoint
在WebSocket添加消息的接收方法,@RabbitListener 接收消息,队列名称使用常量命名,动态队列名称使用 #{name},其中的name是Queue的bean 名称:

@RabbitListener(queues= "#{psQueue.name}")
public void pubsubQueueFirst(String message) {System.out.println(message);sendMessage(message);
}

然后再调用sendMessage方法发送给所在连接的客户端。

修改消息发送
在WebSocket类的onMessage方法将消息发送改成RabbitMQ方式发送:

@OnMessage
public void onMessage(String message){if (!message.equals("ping")){log.info("【wesocket】收到客户端发送的消息,message={}",message);//sendMessage(message);if (rabbitTemplate == null) {rabbitTemplate = (RabbitTemplate) SpringContextUtil.getBean("rabbitTemplate");}rabbitTemplate.convertAndSend("PUBLISH_SUBSCRIBE_EXCHANGE", null, message);}
}

消息通知流程如下所示:

在这里插入图片描述
启动两个实例,模拟集群环境
打开idea的Edit Configurations:
在这里插入图片描述
点击左上角的COPY,然后添加端口server.port=8081:
在这里插入图片描述
启动两个服务,端口分别是8080和8081。在启动8081端口的服务,将前端连接端口改成8081:

var ws = new WebSocket("ws://127.0.0.1:8081/message");

效果展示

在这里插入图片描述

相关文章:

Websocket集群解决方案以及实战(附图文源码)

最近在项目中在做一个消息推送的功能&#xff0c;比如客户下单之后通知给给对应的客户发送系统通知&#xff0c;这种消息推送需要使用到全双工的websocket推送消息。 所谓的全双工表示客户端和服务端都能向对方发送消息。不使用同样是全双工的http是因为http只能由客户端主动发…...

科技的成就(五十一)

397、初等数论的不可解问题 1936 年 4 月&#xff0c;邱奇证明判定性问题不可解。33 岁的邱奇发表论文《初等数论的不可解问题》&#xff0c;运用λ演算给出了判定性问题一个否定的答案。λ演算是一套从数学逻辑中发展起来的形式系统&#xff0c;采用变量绑定和替换&#xff0c…...

Tomcat8 任意写文件PUT方法 (CVE-2017-12615)

Tomcat 任意写文件PUT方法 (CVE-2017-12615) 文章目录 Tomcat 任意写文件PUT方法 (CVE-2017-12615)1 在线漏洞解读:2 版本影响3 环境搭建4 漏洞复现4.1 访问4.2 POC攻击点4.2.1 直接发送以下数据包&#xff0c;然后shell将被写入Web根目录。4.2.2 访问是否通&#xff0c;可以访…...

SAP服务器修改主机名操作手册

1、业务背景 SAP服务器P2V:虚拟化后的服务器主机名(或叫计算机名、设备名,hostname,下文同)会和原参照克隆的服务器主机名一样,若两台服务器处于同一网域,会出现域冲突,导致以下事故发生 (1)、使得原服务器出现掉域情况(DEV->CLN->PRD后台服务器访问失效) …...

【大数据】Doris 构建实时数仓落地方案详解(一):实时数据仓库概述

本系列包含&#xff1a; Doris 构建实时数仓落地方案详解&#xff08;一&#xff09;&#xff1a;实时数据仓库概述Doris 构建实时数仓落地方案详解&#xff08;二&#xff09;&#xff1a;Doris 核心功能解读Doris 构建实时数仓落地方案详解&#xff08;三&#xff09;&#…...

C++ list容器的实现及讲解

所需要的基础知识 对C类的基本了解 默认构造函数 操作符重载 this指针 引用 模板等知识具有一定的了解&#xff0c;阅读该文章会很轻松。 链表节点 template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T&…...

前端项目练习(练习-002-NodeJS项目初始化)

首先&#xff0c;创建一个web-002项目&#xff0c;内容和web-001一样。 下一步&#xff0c;规范一下项目结构&#xff0c;将html&#xff0c;js&#xff0c;css三个文件放到 src/view目录下面&#xff1a; 由于html引入css和js时&#xff0c;使用的是相对路径&#xff0c;所以…...

C++QT day11

绘制时钟 widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QPaintEvent>//绘制事件类 #include <QDebug>//信息调试类 #include <QPainter>//画家类 #include <QTimer>//定时器类 #include <QTime> #include &…...

Stable DIffusion 炫酷应用 | AI嵌入艺术字+光影光效

目录 1 生成AI艺术字基本流程 1.1 生成黑白图 1.2 启用ControlNet 参数设置 1.3 选择大模型 写提示词 2 不同效果组合 2.1 更改提示词 2.2 更改ControlNet 2.2.1 更改模型或者预处理器 2.2.2 更改参数 3. 其他应用 3.1 AI光影字 本节需要用到ControlNet&#xff0c;可…...

C#通过重写Panel改变边框颜色与宽度的方法

在C#中,Panel控件是一个容器控件,用于在窗体或用户控件中创建一个可用于容纳其他控件的面板。Panel提供了一种将相关控件组合在一起并进行布局的方式。以下是Panel控件的详细使用方法: 在窗体上放置 Panel 控件: 在 Visual Studio 的窗体设计器中,从工具箱中拖动并放置一…...

Vue2+ElementUI 静态首页案例

源码 <template><div class"app-container home"><el-row type"flex" justify"space-around" class"row-bg"><el-card class"box-card cardDiv1"><el-col :span"5"><div clas…...

Linux的socket通信

关于套接字通信定义如下&#xff1a; 套接字对应程序猿来说就是一套网络通信的接口&#xff0c;使用这套接口就可以完成网络通信。网络通信的主体主要分为两部分&#xff1a;客户端和服务器端。在客户端和服务器通信的时候需要频繁提到三个概念&#xff1a;IP、端口、通信数据&…...

MySQL学习大纲

了解 MySQL 的基础知识和命令是使用此数据库的前提。以下是一些必须了解的 MySQL 概念和命令&#xff0c;包括基础的 CRUD&#xff08;创建&#xff0c;读取&#xff0c;更新&#xff0c;删除&#xff09;操作&#xff0c;以及一些高级功能&#xff1a; 1. 安装和启动 命令su…...

【Ambari】银河麒麟V10 ARM64架构_安装Ambari2.7.6HDP3.3.1(HiDataPlus)

&#x1f341; 博主 "开着拖拉机回家"带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——&#x1f390;开着拖拉机回家_大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341; 希望本文能够给您带来一定的帮助&#x1f338;文…...

驱动开发练习,platform实现如下功能

实验要求 驱动代码 #include <linux/init.h> #include <linux/module.h> #include <linux/platform_device.h> #include <linux/mod_devicetable.h> #include <linux/of_gpio.h> #include <linux/unistd.h> #include <linux/interrupt…...

QT之QString的用法介绍

QT之QString的用法介绍 成员函数常见用法 成员函数 1&#xff09;QString &append(const QString &str) 将 str 字符串追加到当前字符串末尾&#xff0c;并返回修改后的 QString 对象的引用。 2&#xff09;QString &prepend(const QString &str) 将 str 字符…...

基于Java+SpringBoot+Vue3+Uniapp前后端分离考试学习一体机设计与实现2.0版本(视频讲解,已发布上线)

博主介绍&#xff1a;✌全网粉丝4W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面有丰富的经验…...

springboot 获取参数

1.获取简单参数 2.实体对象参数...

【笔记】离线Ubuntu20.04+mysql 5.7.36 + xtrabackup定时增量备份脚本

一、环境 ● Ubuntu版本查看 lsb_release -a● mysql 版本查看 mysql --version我的是ubuntu 20.04&#xff0c;mysql是5.7.36&#xff0c;所以要用 install_percona-xtrabackup-24 二、原理 备份 通过ubuntu自带的定时器运行增量备份脚本备份文件可以存储在映射后的其他…...

树哈希与换根dp:CF763D

采用的树哈希函数是&#xff1a; d p x w x ∑ y ∈ x d p y 2 w x 2 \Large dp_xw_x\times \sum_{y\in x}dp_y^2w_x^2 dpx​wx​y∈x∑​dpy2​wx2​ 发现从 x x x 到 y y y 时只有 x x x 与 y y y 的哈希值会变化&#xff0c;分别维护即可 #include<bits/stdc.h&…...

项目介绍 MATLAB实现基于蜘蛛猴优化算法(SMO)进行无人机三维路径规划的详细项目实例(含模型描述及部分示例代码) 专栏近期有大量优惠 还请多多点一下关注 加油 谢谢 你的鼓励是我前行的动力 谢谢

MATLAB实现基于蜘蛛猴优化算法&#xff08;SMO&#xff09;进行无人机三维路径规划的详细项目实例 更多详细内容可直接联系博主本人 或者访问对应标题的完整博客或者文档下载页面&#xff08;含完整的程序&#xff0c;GUI设计和代码详解&#xff09; 无人机&#xff08;UAV…...

别再只会用串口助手了!用STM32F103C8T6+HC-06做个蓝牙遥控器(HAL库实战)

从串口玩具到实战利器&#xff1a;STM32HC-06蓝牙遥控器开发指南 在创客和嵌入式开发领域&#xff0c;蓝牙通信一直是最受欢迎的无线连接方案之一。许多开发者最初接触蓝牙模块时&#xff0c;往往止步于简单的数据收发实验——通过串口助手发送几个字符&#xff0c;看到LED闪烁…...

2026年APP兼容性测试平台选型指南:精准破局兼容性难题困扰

随着移动互联网的飞速发展&#xff0c;APP的种类和数量呈爆炸式增长。然而&#xff0c;不同手机品牌、型号以及操作系统版本的差异&#xff0c;让APP在兼容性方面面临巨大挑战。许多开发者都遇到过这样的困扰&#xff1a;APP在某些手机上闪退、界面显示错乱&#xff0c;或是功能…...

Galaxy新手必看:5分钟搞定生物信息学工作流搭建(附Circos图实战)

Galaxy新手必看&#xff1a;5分钟搞定生物信息学工作流搭建&#xff08;附Circos图实战&#xff09; 第一次接触生物信息学分析时&#xff0c;面对命令行和复杂的数据格式&#xff0c;很多初学者都会感到无从下手。Galaxy平台的出现彻底改变了这一局面——这个开源的Web工具让生…...

Windows HEIC缩略图扩展:让苹果照片在PC上清晰呈现

Windows HEIC缩略图扩展&#xff1a;让苹果照片在PC上清晰呈现 【免费下载链接】windows-heic-thumbnails Enable Windows Explorer to display thumbnails for HEIC/HEIF files 项目地址: https://gitcode.com/gh_mirrors/wi/windows-heic-thumbnails 问题场景&#xf…...

零基础学唱歌全套教程 声乐技巧入门到进阶资源

很多想自学唱歌的朋友&#xff0c;应该都有过这样的困扰&#xff1a;想入门却不知道从哪一步开始&#xff0c;网上找的教程要么太零散&#xff0c;知识点前后接不上&#xff1b;要么内容太晦涩&#xff0c;完全摸不着门道。这段时间我整理了一批适配不同学习阶段的唱歌相关教程…...

深入解析YOLO中mode.predict()的关键参数与应用场景

1. 理解YOLO的mode.predict()方法 YOLO&#xff08;You Only Look Once&#xff09;作为目标检测领域的标杆算法&#xff0c;其mode.predict()方法是实际应用中最常接触的核心接口。这个方法封装了从数据输入到结果输出的完整推理流程&#xff0c;相当于整个检测系统的"总…...

如何利用Clef Handbook进行有效会议管理:5个关键原则 [特殊字符]

如何利用Clef Handbook进行有效会议管理&#xff1a;5个关键原则 &#x1f680; 【免费下载链接】handbook An employee handbook built for inclusion 项目地址: https://gitcode.com/gh_mirrors/handbook6/handbook 在当今快节奏的工作环境中&#xff0c;高效会议管理…...

【TTS 语音】合成技术解析与开源方案全景

文章目录TTS 语音合成技术解析与开源方案全景一、引言二、TTS 技术架构演进2.1 三代架构对比2.2 关键技术组件解析三、主流开源方案技术解析3.1 Kokoro-82M3.2 F5-TTS3.3 CosyVoice 系列&#xff08;阿里巴巴 FunAudioLLM&#xff09;3.4 Spark-TTS&#xff08;0.5B&#xff09…...

从特征多项式到行列式:揭秘矩阵特征值之积的几何意义

1. 特征多项式&#xff1a;打开矩阵奥秘的钥匙 我第一次接触特征多项式时&#xff0c;完全被这个抽象的概念搞晕了。直到有一天&#xff0c;我的导师用了一个简单的比喻&#xff1a;"特征多项式就像是矩阵的DNA检测报告&#xff0c;它能告诉我们这个矩阵最本质的特性。&qu…...