【WebSocket】多节点下WebSocket消息收发解决案例
单体Webscoket
- springboot版本: 2.1.1.RELEASE
- jdk: 1.8
示例代码
WebsocketServer
@ServerEndpoint("/client/{userName}")
@Component
@Slf4j
public class WebSocketServer {
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String userName = "";
/**
* @Description: 连接建立成功调用的方法,成功建立之后,将用户的userName 存储到redis
* @params: [session, userId]
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnOpen
public void onOpen(Session session, @PathParam("userName") String userName) {
this.session = session;
this.userName = userName;
webSocketMap.put(userName, this);
addOnlineCount();
log.info("用户连接:" + userName + ",当前在线人数为:" + getOnlineCount());
}
/**
* @Description: 连接关闭调用的方法
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userName)) {
webSocketMap.remove(userName);
//从set中删除
subOnlineCount();
}
log.info("用户退出:" + userName + ",当前在线人数为:" + getOnlineCount());
}
/**
* @Description: 收到客户端消息后调用的方法, 调用API接口 发送消息到
* @params: [message, session]
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnMessage
public void onMessage(String message, @PathParam("userName") String userName) {
log.info("用户消息:" + userName + ",报文:" + message);
if (StringUtils.isNotBlank(message)) {
try {
//解析发送的报文
JSONObject jsonObject = JSON.parseObject(message);
//追加发送人(防止串改)
jsonObject.put("sender", this.userName);
String receiver = jsonObject.getString("receiver");
//传送给对应toUserId用户的websocket
if (StringUtils.isNotBlank(receiver) && webSocketMap.containsKey(receiver)) {
webSocketMap.get(receiver).session.getBasicRemote().sendText(jsonObject.toJSONString());
} else {
log.error("用户:" + receiver + "不在该服务器上");
//否则不在这个服务器上,发送到mysql或者redis
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 发布websocket消息
* 消息格式: { "sender": "u2","receiver": "u1","msg": "hello world","createTime":"2021-10-12 11:12:11"}
*
* @param dto
* @return
*/
public static void sendWebsocketMessage(ChatMsg dto) {
if (dto != null) {
if (StringUtils.isNotBlank(dto.getReceiver()) && webSocketMap.containsKey(dto.getReceiver())) {
String json = JSON.toJSONString(dto);
try {
webSocketMap.get(dto.getReceiver()).session.getBasicRemote().sendText(json);
} catch (IOException e) {
log.error("消息发送异常:{}", e.toString());
}
} else {
log.error("用户:" + dto.getReceiver() + ",不在线!");
}
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:" + this.userName + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* @Description: 获取在线人数
* @params: []
* @return: int
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized int getOnlineCount() {
return onlineCount;
}
/**
* @Description: 在线人数+1
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
/**
* @Description: 在线人数-1
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
- WebSocketConfig
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
前端代码
var socket;
var userName;
establishConnection()
/***建立连接*/
function establishConnection() {
userName = $("#sender").val();
if (userName == '' || userName == null) {
alert("请输入发送者");
return;
}
//实现化WebSocket对象,指定要连接的服务器地址与端口 建立连接
var socketUrl = "" + window.location.protocol + "//" + window.location.host + "/client/" + userName;
socketUrl = socketUrl.replace("https", "ws").replace("http", "ws");
if (socket != null) {
socket.close();
socket = null;
}
socket = new WebSocket(socketUrl);
//打开事件
socket.onopen = function () {
console.log("开始建立链接....")
};
//关闭事件
socket.onclose = function () {
console.log("websocket已关闭");
};
//发生了错误事件
socket.onerror = function () {
console.log("websocket发生了错误");
};
/**
* 接收消息
* @param msg
*/
socket.onmessage = function (msg) {
msg = JSON.parse(msg.data);
console.log(msg);
if (msg.msg != '连接成功') {
$("#msgDiv").append('<p class="other">用户名:' + msg.sender + '</p><p class="chat">' + msg.msg + '</p>');
}
};
}
/**
* 发送消息
*/
function sendMessage() {
var msg = $("#msg").val();
if (msg == '' || msg == null) {
alert("消息内容不能为空");
return;
}
var receiver = $("#receiver").val();
if (receiver == '' || receiver == null) {
alert("接收人不能为空");
return;
}
var msgObj = {
"receiver": receiver,
"msg": msg
};
$("#msgDiv").append('<p class="user">用户名:' + userName + '</p><p class="chat">' + msg + '</p>');
try{
socket.send(JSON.stringify(msgObj));
$("#msg").val('');
}catch (e) {
alert("服务器内部错误");
}
}
测试效果
- 问题
如果两个客户端连接不在同一个服务器上,会出现什么问题?
结果就是如下所示:

如何解决多台客户端连接在不同服务器,互相发送消息问题!
分布式WebSocket 解决
方案一 Redis消息订阅与发布

描述:
客户端A 和客户端B 都订阅同一个Topic ,后台Websocket收到消息后,将消息发送至Redis中,同时服务端会监听该渠道内的消息,监听到消息后,会将消息推送至对应的客户端。
示例代码
application.yml
主要是Redis配置
server:
port: 8082
spring:
thymeleaf:
#模板的模式,支持 HTML, XML TEXT JAVASCRIPT
mode: HTML5
#编码 可不用配置
encoding: UTF-8
#内容类别,可不用配置
content-type: text/html
#开发配置为false,避免修改模板还要重启服务器
cache: false
# #配置模板路径,默认是templates,可以不用配置
prefix: classpath:/templates
suffix: .html
#Redis配置
redis:
host: localhost
port: 6379
password: 123456
timeout: 5000
RedisSubscriberConfig.java
/**
* @Description 消息订阅配置类
* @Author wxl
* @Date 2020/3/31 13:54
*/
@Configuration
public class RedisSubscriberConfig {
/**
* 消息监听适配器,注入接受消息方法
*
* @param receiver
* @return
*/
@Bean
public MessageListenerAdapter messageListenerAdapter(ChatMessageListener receiver) {
return new MessageListenerAdapter(receiver);
}
/**
* 创建消息监听容器
*
* @param redisConnectionFactory
* @param messageListenerAdapter
* @return
*/
@Bean
public RedisMessageListenerContainer getRedisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory, MessageListenerAdapter messageListenerAdapter) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
redisMessageListenerContainer.addMessageListener(messageListenerAdapter, new PatternTopic(TOPIC_CUSTOMER));
return redisMessageListenerContainer;
}
}
RedisUtil.java
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 发布
*
* @param key
*/
public void publish(String key, String value) {
stringRedisTemplate.convertAndSend(key, value);
}
}
ChatMessageListener.java
/**
* @Description 集群聊天消息监听器
* @Author wxl
* @Date 2020/3/29 15:07
*/
@Slf4j
@Component
public class ChatMessageListener implements MessageListener {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
RedisSerializer<String> valueSerializer = redisTemplate.getStringSerializer();
String value = valueSerializer.deserialize(message.getBody());
ChatMsg dto = null;
if (StringUtils.isNotBlank(value)) {
try {
dto = JacksonUtil.json2pojo(value, ChatMsg.class);
} catch (Exception e) {
e.printStackTrace();
log.error("消息格式转换异常:{}", e.toString());
}
log.info("监听集群websocket消息--- {}", value);
WebSocketServer.sendWebsocketMessage(dto);
}
}
}
WebSocketServer
@ServerEndpoint("/client/{userName}")
@Component
@Slf4j
public class WebSocketServer {
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 不能使用@AutoWire原因:发现注入不了redis,redis注入失败 可能是因为实例化的先后顺序吧,WebSocket先实例化了, 但是@Autowire是会触发getBean操作
* 因为@ServerEndpoint不支持注入,所以使用SpringUtils获取IOC实例
*/
private RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
/**
* 接收userId
*/
private String userName = "";
/**
* @Description: 连接建立成功调用的方法,成功建立之后,将用户的userName 存储到redis
* @params: [session, userId]
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnOpen
public void onOpen(Session session, @PathParam("userName") String userName) {
this.session = session;
this.userName = userName;
webSocketMap.put(userName, this);
addOnlineCount();
log.info("用户连接:" + userName + ",当前在线人数为:" + getOnlineCount());
}
/**
* @Description: 连接关闭调用的方法
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userName)) {
webSocketMap.remove(userName);
//从set中删除
subOnlineCount();
}
log.info("用户退出:" + userName + ",当前在线人数为:" + getOnlineCount());
}
/**
* @Description: 收到客户端消息后调用的方法, 调用API接口 发送消息到
* @params: [message, session]
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnMessage
public void onMessage(String message, @PathParam("userName") String userName) {
log.info("用户消息:" + userName + ",报文:" + message);
if (StringUtils.isNotBlank(message)) {
try {
//解析发送的报文
JSONObject jsonObject = JSON.parseObject(message);
//追加发送人(防止串改)
jsonObject.put("sender", this.userName);
//传送给对应toUserId用户的websocket
redisUtil.publish(TOPIC_CUSTOMER,jsonObject.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 发布websocket消息
* 消息格式: { "sender": "u2","receiver": "u1","msg": "hello world","createTime":"2021-10-12 11:12:11"}
*
* @param dto
* @return
*/
public static void sendWebsocketMessage(ChatMsg dto) {
if (dto != null) {
if (StringUtils.isNotBlank(dto.getReceiver()) && webSocketMap.containsKey(dto.getReceiver())) {
String json = JSON.toJSONString(dto);
try {
webSocketMap.get(dto.getReceiver()).session.getBasicRemote().sendText(json);
} catch (IOException e) {
log.error("消息发送异常:{}", e.toString());
}
} else {
log.error("用户:" + dto.getReceiver() + ",不在次服务器上!");
}
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:" + this.userName + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* @Description: 获取在线人数
* @params: []
* @return: int
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized int getOnlineCount() {
return onlineCount;
}
/**
* @Description: 在线人数+1
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
/**
* @Description: 在线人数-1
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
测试效果
方案二 RabbitMq
采用的是基于rabbitmq的扇形分发器,消息生产者发送到指定的队列,消息消费者监听此队列的消息,
将消息推送客户端。
交换机、队列
@Configuration
public class FanoutRabbitConfig {
/**
* 创建三个队列 :fanout.msg
* 将三个队列都绑定在交换机 fanoutExchange 上
* 因为是扇型交换机, 路由键无需配置,配置也不起作用
*/
@Bean
public Queue queueMsg() {
return new Queue(ConstantUtils.FANOUT_QUEUE_MSG);
}
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange(ConstantUtils.FANOUT_EXCHANGE);
}
@Bean
Binding bindingExchangeA() {
return BindingBuilder.bind(queueMsg()).to(fanoutExchange());
}
}
消息监听
@Component
@RabbitListener(queues = ConstantUtils.FANOUT_QUEUE_MSG)
public class FanoutReceiverMsg {
@RabbitHandler
public void process(Map msg) throws IOException {
if (msg !=null){
WebSocketServer.sendMessage((String)msg.get("receiver"),msg.toString());
}
}
}
【WebSocket】多节点下WebSocket消息收发解决案例的更多相关文章
- spring boot下WebSocket消息推送(转)
原文地址:https://www.cnblogs.com/betterboyz/p/8669879.html WebSocket协议 WebSocket是一种在单个TCP连接上进行全双工通讯的协议.W ...
- spring boot下WebSocket消息推送
WebSocket协议 WebSocket是一种在单个TCP连接上进行全双工通讯的协议.WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范.WebSo ...
- 谈下WebSocket介绍,与Socket的区别
这个话题应该是面试中出现频率比较高的吧....不管咋样还是有必要深入了解下两者之间的关联.废话不多说,直接入题吧: WebSocket介绍与原理 目的:即时通讯,替代轮询 网站上的即时通讯是很常见的, ...
- java集成WebSocket向指定用户发送消息
一.WebSocket简单介绍 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了.近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通 ...
- [转帖]关于一个 websocket 多节点分布式问题的头条前端面试题
关于一个 websocket 多节点分布式问题的头条前端面试题 https://juejin.im/post/5dcb5372518825352f524614 你来说说 websocket 有什么用? ...
- Unity3d 下websocket的使用
今天介绍一下如何在Unity3D下使用WebSocket. 首先介绍一下什么是websocket,以及与socket,和http的区别与联系,然后介绍一下websocket的一些开源的项目. WebS ...
- 用websocket实现后台推送消息
1前台实现 connect:function() { var webSocketIP = window.CRM_CONFIG.WebSocketIP; var target = 'ws://'+web ...
- java使用Websocket获取HttpSession出现的问题与解决
websocket的写法就不多说了,主要记一记其中出现的问题 1.获取不到httpSession 解决办法:先重写握手方法,将httpsession放入ServerEndpointConfig.get ...
- Tomcat下WebSocket最大连接数测试
WebSocket现在很常用,想要测试tomcat的最大连接数,今天试了一个可行的办法和配置(之前是用全公司的设备一起来测试的,真机环境的测试收到网络的影响很大,其实真实环境应用中,网络才是webso ...
- spring boot 下websocket实现的两种方法
websocket前台实现代码,保存为html执行就好 html代码来自:https://blog.csdn.net/M348915654/article/details/53616837 <h ...
随机推荐
- FFmpeg开发笔记(一)搭建Linux系统的开发环境
对于初学者来说,如何搭建FFmpeg的开发环境是个不小的拦路虎,因为FFmpeg用到了许多第三方开发包,所以要先编译这些第三方源码,之后才能给FFmpeg集成编译好的第三方库.不过考虑到刚开始仅仅调用 ...
- vue中使用西瓜视频api
https://v2.h5player.bytedance.com/en/api/ 1 npm install xgplayer 1 <div id="mse">< ...
- [SWPUCTF 2021 新生赛]简简单单的逻辑
得到一个.py文件,一般是没壳的,不过还是要养成习惯,查个壳: 意料之中,啥也没有,打开文件: 给了我们一个加密逻辑,然后最后一行给了一个结果:那么就是根据上述的逻辑,反解密出flag就好了 分析一下 ...
- #Python实例 计算外卖配送距离(基于百度API接口)---第二篇
https://www.cnblogs.com/simone331/p/17218019.html 在上一篇中,我们计算了两点的距离(链接为上篇文章),但是具体业务中,往往会存在一次性计算多组,上百甚 ...
- #Powerbi 季度时间进度计算
上一篇我们学习了月度时间进度的计算方法,今天我们学习季度时间进度的测算. 思路:找出目前共计消耗了多少天(季度),目前日期所在的季度共有多少天,两者相除即是季度的时间进度 首先列出DAX函数: 本 ...
- linux随心记
linux前言 1.计算机有哪两部分组成? 硬件和软件 2.常见的操作系统有哪些? pc端:window ,linux,MacOS 移动端:Android,ios,鸿蒙 3.什么是Linux系统内核 ...
- 从GFS到GPT,AI Infra的激荡20年
导读 最近AIGC和LLM的浪潮层层迭起,大有把AI行业过去十年画的饼,一夜之间完全变现的势头.而 AI Infra (构建AI所需的基础设施),也成了讨论的焦点之一.大众对AI Infra的关注点 ...
- phpstudy-pikachu-字符型注入(get)
在查询栏输入1,点击查询获得查询格式 ?name=1'&submit=查询 *捷径 ' or 1=1 --+ *非捷径 ?name=1' and 1=2 --+ 2--+&submit ...
- 【VS Code 与 Qt6】QCheckBox的图标为什么不会切换?
本篇专门扯一下有关 QCheckBox 组件的一个问题.老周不水字数,直接上程序,你看了就明白. #include <QApplication> #include <QWidget& ...
- 使用 conda 和 Jupyter 在 R 中实现数据科学分析
前两篇文章我们介绍了 Jupyter Notebook 的一些基础用法,今天我们来介绍一下如何使用 conda 和 Jupyter 在 R 中开始一个数据科学项目. 在开始之前我们先要明确一个概念:K ...

