【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 ...
随机推荐
- LeetCode 双周赛 102,模拟 / BFS / Dijkstra / Floyd
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,欢迎来到小彭的 LeetCode 周赛解题报告. 昨晚是 LeetCode 双周赛第 102 场,你 ...
- 基于QtAV的简易播放器(开源)
这个开源代码,是我利用QtAV源码,提取其中一部分代码,进行整合到我自己项目中,做的一个小型播放器测试,至于怎么安装一些环境以及QtAV源码编译在我以前写的一篇博客中可以看到(Qt第三方库QtAV-- ...
- Vue的生命周期的详解
Vue的生命周期 Vue的生命周期是每个使用Vue框架的前端人员都需要掌握的知识,以此作为记录. Vue的生命周期就是vue实例从创建到销毁的全过程,也就是new Vue() 开始就是vue生 ...
- 如何实现Spring中服务关闭时对象销毁执行代码
spring提供了两种方式用于实现对象销毁时去执行操作 1.实现DisposableBean接口的destroy 2.在bean类的方法上增加@PreDestroy方法,那么这个方法会在Disposa ...
- 基于.Net开发的数据库导入导出的开源项目
在项目开发过程中,我们经常碰到从数据库导入导出的需求,虽然这样的功能不是很复杂,但是往往我们都会碰到一些问题. 比如导入的Excel格式问题.Excetl中图片导入问题,导出的需求为了方便客户查看,会 ...
- HMS Core 6.10.0版本发布公告
分析服务 ◆ 事件分析下新增商品订阅分析报告,帮助开发者了解应用内用户付费订阅概况,评估订阅付费价值: ◆ 营销分析.用户质量.转化分析以及过滤器中,新增广告系列/广告任务通过ID进行搜索的功能,通过 ...
- FreeSWITCH使用L16编码通信及raw数据提取
环境:CentOS 7.6_x64 FreeSWITCH版本 :1.10.9 Python版本:3.9.12 一.背景描述 PCM(Pulse Code Modulation,脉冲编码调制)音频数据是 ...
- Latex-beamer的教程
Beamer头文件 Latex是一个非常精确且高效的排版工具,其中的beamer作为一个非常强大的模块承担着PPT任务的排版 首先引入头文件来开始: \documentclass{beamer} %h ...
- 2022-05-26:void add(int L, int R, int C)代表在arr[L...R]上每个数加C, int get(int L, int R)代表查询arr[L...R]上的累加
2022-05-26:void add(int L, int R, int C)代表在arr[L-R]上每个数加C, int get(int L, int R)代表查询arr[L-R]上的累加和, 假 ...
- 2022-04-04:k8s中kubectl源码用到了哪些设计模式?除了工厂和单例。
2022-04-04:k8s中kubectl源码用到了哪些设计模式?除了工厂和单例. 答案2022-04-04: 1.建造者模式.resource.Builder.D:\go_path\src\git ...