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实现消息的推送。
- 添加依赖
<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>
- 创建 ServerEndpointExporter 的 bean 实例
ServerEndpointExporter 的 bean 实例自动注册 @ServerEndpoint 注解声明的 websocket endpoint,使用springboot自带tomcat启动需要该配置,使用独立 tomcat 则不需要该配置。
@Configuration
public class WebSocketConfig {//tomcat启动无需该配置@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
- 创建服务端点 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");
效果展示
相关文章:
![](https://img-blog.csdnimg.cn/30d92c3181dd4731b957d43c8d2f9544.png)
Websocket集群解决方案以及实战(附图文源码)
最近在项目中在做一个消息推送的功能,比如客户下单之后通知给给对应的客户发送系统通知,这种消息推送需要使用到全双工的websocket推送消息。 所谓的全双工表示客户端和服务端都能向对方发送消息。不使用同样是全双工的http是因为http只能由客户端主动发…...
![](https://www.ngui.cc/images/no-images.jpg)
科技的成就(五十一)
397、初等数论的不可解问题 1936 年 4 月,邱奇证明判定性问题不可解。33 岁的邱奇发表论文《初等数论的不可解问题》,运用λ演算给出了判定性问题一个否定的答案。λ演算是一套从数学逻辑中发展起来的形式系统,采用变量绑定和替换,…...
![](https://img-blog.csdnimg.cn/3e8a40ea6c9c4ac6a116eec4946fe393.png)
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 直接发送以下数据包,然后shell将被写入Web根目录。4.2.2 访问是否通,可以访…...
![](https://www.ngui.cc/images/no-images.jpg)
SAP服务器修改主机名操作手册
1、业务背景 SAP服务器P2V:虚拟化后的服务器主机名(或叫计算机名、设备名,hostname,下文同)会和原参照克隆的服务器主机名一样,若两台服务器处于同一网域,会出现域冲突,导致以下事故发生 (1)、使得原服务器出现掉域情况(DEV->CLN->PRD后台服务器访问失效) …...
![](https://img-blog.csdnimg.cn/0a17f23e32cc42bd804b87148b3c075f.png#pic_center)
【大数据】Doris 构建实时数仓落地方案详解(一):实时数据仓库概述
本系列包含: Doris 构建实时数仓落地方案详解(一):实时数据仓库概述Doris 构建实时数仓落地方案详解(二):Doris 核心功能解读Doris 构建实时数仓落地方案详解(三)&#…...
![](https://img-blog.csdnimg.cn/1b853ed154cc4197bbc9a5cd441f83d0.png)
C++ list容器的实现及讲解
所需要的基础知识 对C类的基本了解 默认构造函数 操作符重载 this指针 引用 模板等知识具有一定的了解,阅读该文章会很轻松。 链表节点 template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T&…...
![](https://img-blog.csdnimg.cn/ec5d1c22dc93493aa62e460987e051ae.png)
前端项目练习(练习-002-NodeJS项目初始化)
首先,创建一个web-002项目,内容和web-001一样。 下一步,规范一下项目结构,将html,js,css三个文件放到 src/view目录下面: 由于html引入css和js时,使用的是相对路径,所以…...
![](https://img-blog.csdnimg.cn/5ab92a30e02d484889e1a7743937dcc6.png)
C++QT day11
绘制时钟 widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QPaintEvent>//绘制事件类 #include <QDebug>//信息调试类 #include <QPainter>//画家类 #include <QTimer>//定时器类 #include <QTime> #include &…...
![](https://img-blog.csdnimg.cn/15f5202872d74787a88b502c92b46ed6.png)
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,可…...
![](https://img-blog.csdnimg.cn/51baf48a44884f7eac75b3718fe50307.png)
C#通过重写Panel改变边框颜色与宽度的方法
在C#中,Panel控件是一个容器控件,用于在窗体或用户控件中创建一个可用于容纳其他控件的面板。Panel提供了一种将相关控件组合在一起并进行布局的方式。以下是Panel控件的详细使用方法: 在窗体上放置 Panel 控件: 在 Visual Studio 的窗体设计器中,从工具箱中拖动并放置一…...
![](https://img-blog.csdnimg.cn/fa372a73669a43e88b9e549a1c05ceae.png)
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…...
![](https://img-blog.csdnimg.cn/a785b7240bce4cb68fdcfad22a2af115.png)
Linux的socket通信
关于套接字通信定义如下: 套接字对应程序猿来说就是一套网络通信的接口,使用这套接口就可以完成网络通信。网络通信的主体主要分为两部分:客户端和服务器端。在客户端和服务器通信的时候需要频繁提到三个概念:IP、端口、通信数据&…...
![](https://www.ngui.cc/images/no-images.jpg)
MySQL学习大纲
了解 MySQL 的基础知识和命令是使用此数据库的前提。以下是一些必须了解的 MySQL 概念和命令,包括基础的 CRUD(创建,读取,更新,删除)操作,以及一些高级功能: 1. 安装和启动 命令su…...
![](https://img-blog.csdnimg.cn/img_convert/b6592ab25a41dcb293d25ffcb30f5fac.png)
【Ambari】银河麒麟V10 ARM64架构_安装Ambari2.7.6HDP3.3.1(HiDataPlus)
🍁 博主 "开着拖拉机回家"带您 Go to New World.✨🍁 🦄 个人主页——🎐开着拖拉机回家_大数据运维-CSDN博客 🎐✨🍁 🪁🍁 希望本文能够给您带来一定的帮助🌸文…...
![](https://img-blog.csdnimg.cn/f9197261127343fcba2bcd9c296e7590.jpeg)
驱动开发练习,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…...
![](https://www.ngui.cc/images/no-images.jpg)
QT之QString的用法介绍
QT之QString的用法介绍 成员函数常见用法 成员函数 1)QString &append(const QString &str) 将 str 字符串追加到当前字符串末尾,并返回修改后的 QString 对象的引用。 2)QString &prepend(const QString &str) 将 str 字符…...
![](https://img-blog.csdnimg.cn/e9eb0a0d29e64a3a94cffdb152c8443f.png)
基于Java+SpringBoot+Vue3+Uniapp前后端分离考试学习一体机设计与实现2.0版本(视频讲解,已发布上线)
博主介绍:✌全网粉丝4W,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验…...
![](https://img-blog.csdnimg.cn/0cb4d0a24ef14d52817fbfdf92822a04.png)
springboot 获取参数
1.获取简单参数 2.实体对象参数...
![](https://img-blog.csdnimg.cn/3ae4e37900db414f9ce88a53828e8856.png)
【笔记】离线Ubuntu20.04+mysql 5.7.36 + xtrabackup定时增量备份脚本
一、环境 ● Ubuntu版本查看 lsb_release -a● mysql 版本查看 mysql --version我的是ubuntu 20.04,mysql是5.7.36,所以要用 install_percona-xtrabackup-24 二、原理 备份 通过ubuntu自带的定时器运行增量备份脚本备份文件可以存储在映射后的其他…...
![](https://www.ngui.cc/images/no-images.jpg)
树哈希与换根dp:CF763D
采用的树哈希函数是: 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 dpxwxy∈x∑dpy2wx2 发现从 x x x 到 y y y 时只有 x x x 与 y y y 的哈希值会变化,分别维护即可 #include<bits/stdc.h&…...
![](https://img-blog.csdnimg.cn/img_convert/9441b781c5451cdb2aa59dc7d02573c3.png)
npm、yarn、pnpm如何清除缓存?
前端工程化创建项目会经常使用各种安装包管理工具,安装各种前端依赖包。例如,npm、yarn、pnpm等。时间一长,各种安装包管理工具的在安装依赖时,留下的缓存文件就会变得很大,以至于影响系统的运行,因此必要时…...
![](https://img-blog.csdnimg.cn/0c032c56d74f4183addb366df1218531.png)
12款最火的AI画图软件,助你探索创新设计
ChatGPT火爆出圈,AI画图软件也如雨后春笋般流行起来。各类AI画图的软件工具横空出世,设计师与其焦虑工作会不会被人工智能取代,不如践行“工欲善其事必先利其器”,开拓思路,打开格局,好好地探索下如何利用好…...
![](https://img-blog.csdnimg.cn/img_convert/0bb07cbd10e1706a54f260b551690c47.png)
cookie信息无法获取问题研究
背景 在oneapi这个前后端都有的开源项目中,我想接入chatnextweb到oneapi的后端。 由于需要二开chatnextweb,添加登录注册功能,考虑到java后端的性能问题和内存占用,决定不重启写个服务,而是将登录注册接入到oneapi的…...
![](https://img-blog.csdnimg.cn/7d1558fb00214ff9905195b5cf02343a.png)
Linux:冯诺依曼系统和操作系统的概念
文章目录 冯诺依曼体系结构冯诺依曼体系的理解 操作系统操作系统的基本定位操作系统的理解1 操作系统的理解2总结 本篇主要总结的是操作系统的基本认知和一些概念 冯诺依曼体系结构 那么上图表示的就是冯诺依曼体系结构,那这个体系结构是什么?为什么要先…...
![](https://img-blog.csdnimg.cn/1a8c810a943e4bb49f07cac966d23a7d.png)
【操作系统笔记十一】进程间通信
Linux文件系统 inode 节点 (index node):给每个文件赋予一个称为 i 节点的数据结构。 inode 一开始是存储在硬盘中的,只有当文件被打开的时候,其对应的 i 节点才加载到内存中。 总结: Linux 中,…...
![](https://www.ngui.cc/images/no-images.jpg)
【操作系统】聊聊Linux软中断
什么是中断 中断是系统用来响应硬件设备请求的一种机制,会打断进程的正常调度和执行,转而去执行内核中的中断处理程序。 比如你正在看书,你女朋友叫你出去逛街。你就需要先放下手里的事情,然后逛街。回来之后,在接着看…...
![](https://img-blog.csdnimg.cn/img_convert/d55ad9d3cafd401a1c0db2ac2f108280.jpeg)
公众号迁移个人可以迁移吗?
公众号账号迁移的作用是什么?只能变更主体吗?很多小伙伴想做公众号迁移,但是不知道公众号迁移有什么作用,今天跟大家具体讲解一下。首先公众号迁移最主要的就是修改公众号的主体了,比如我们公众号原来是A公司的&#x…...
![](https://img-blog.csdnimg.cn/efebd83fb0874933bfbe7335ec13eece.png)
全国职业技能大赛云计算--高职组赛题卷⑤(容器云)
全国职业技能大赛云计算--高职组赛题卷⑤(容器云) 第二场次题目:容器云平台部署与运维任务2 基于容器的web应用系统部署任务(15分)任务3 基于容器的持续集成部署任务(15分)任务4 Kubernetes容器…...
![](https://img-blog.csdnimg.cn/fb97278803e04a829c1c8f2d42a789e2.jpeg)
支撑位和阻力位在Renko和烛台图如何使用?FPmarkets澳福3秒回答
很多投资者都知道,Renko图表和普通日本烛台都会采用相同的交易信号,即支撑位和阻力位。那么支撑位和阻力位在Renko和烛台图如何使用?FPmarkets澳福3秒回答。 这些信号在任何时间框架上都会出现,且在蜡烛图交易中颇受欢迎。对于Renko图表而言…...
![](https://img-blog.csdnimg.cn/1df5a790efc447db9089fd024d06f55c.png)
如何在32位MCU用printf()函数打印64位数据
1. 在32位MCU上定义64位变量: unsigned long long time_base; unsigned long long temp_time;2. 调用打印函数: printf("RFID:time_base:%d\r\n",time_base); printf("RFID:temp_time:%d\r\n",temp_time); printf("RFID:Ru…...
![](https://images.cnblogs.com/cnblogs_com/hrmai/201101/201101062126065134.jpg)
做古玩的网站/2021年网络营销案例
其实我用过的,那一天晚上,看着摇摇欲坠的衣架,还有上次换床时留下的两根木头,突发奇想,用菜刀把木头给锯了,然后把衣架给修好了,用了两个小时。(我的菜刀上有一道锯齿状的东西&#…...
![](/images/no-images.jpg)
江苏省建设工程信息服务平台/怎样优化关键词到首页
1.RLC电路 2.孙鑫视频 3.ADO数据库 4.SQL Server 5.验证读txt文件的同时压缩保存是否正常。写一个压缩程序z-ip_utils_src.zip 参考这个。搞清楚一般C压缩文件是调系统的压缩软件还是自己利用开源算法写压缩方法? 6.使用数据库开发的桌面程序发布的时候怎么办&…...
![](/images/no-images.jpg)
做网站备案需要哪些材料/互联网营销培训课程
一,基本数据类型 8种,byte,short,long,int char,boolean,double,float 二,引用数据类型 3种,类 接口基本数据类型作为实参传递形参时候,对形参的值改变不影响实参,而引用数据类型作为实参传递参数时…...
![](https://img-blog.csdnimg.cn/img_convert/f987a6b92e2ee9986a5aa3b0d03b0161.png)
长春建站的费用/百度竞价网站
什么是MybatisMyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包…...
![](https://s1.51cto.com/attachment/201005/201005041272980295671.gif)
广州市政府门户网站介绍/简述网络营销的特点
很多搞过数据仓库的朋友曾经问我有没有数据模型的资料,很惭愧,一来是实施项目确实不多;二来就算有也是公司机密的东西,不便泄露;今天登陆到 inmon 的网站,刚好看到这方面的资料,于是就当了下来&…...
![](https://img-blog.csdnimg.cn/img_convert/6db87bcd88f54a149167d4a5c490fe6e.png)
wordpress替换头像/个人主页网页设计
Linux Mint 19.2 “Tina” 在 2019 年 8 月 2 日发布,它是一个基于 Ubuntu 18.04 LTS (Bionic Beaver) 的长期支持版本。-- 2daygeek(作者)Linux Mint 19.2 “Tina” 在 2019 年 8 月 2 日发布,它是一个基于 Ubuntu 18.04 LTS (Bi…...