websocket通过redis实现集群方案

websocket通过redis实现集群方案

一、前言

1、使用websocket前后端通信时,若后台是集群部署,那么连接只能与其中一台握手建立连接,当nginx做负载后触发节点在未与websocket建立连接的服务上,此时就会出现问题。

二、解决方案

1、使用redis的发布订阅方式。(mq同理)
2、当需要websocket需求发送消息时,使用redis的发布订阅功能,将消息推送到redis中,所有需求消息的服务都监听这个Topic,接收到消息后,判断是否与前端建立连接,如果建立连接,将消息通过websocket发送出去,前端接收。若没有连接直接跳过即可。

三、解决步骤

1、引入websocket和redis依赖

		<!--webSocket-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>
		<!--redis相关-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2、建一个websocket的配置

@Configuration
public class WebSocketConfig {

	@Bean
	public ServerEndpointExporter serverEndpointExporter() {
		return new ServerEndpointExporter();
	}

}

3、建websocket核心服务

@Slf4j
@Component
@ServerEndpoint("/socketServer/{userCode}")
public class WebSocketServer {

	/**
	 * nginx
	 * location / {
	 *           #  root   html;
	 *                 proxy_pass  http://base-web;
	 * 				proxy_set_header Upgrade $http_upgrade;
	 *         	proxy_set_header Connection "upgrade";
	 * 			proxy_set_header   Host    $host;
	 * 			proxy_set_header   X-Real-IP   $remote_addr;
	 * 			proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
	 *            # index  index.html index.htm;
	 *  }
	 *
	 *
	 */

	/**
	 * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的
	 */
	private static int onlineCount = 0;
	/**
	 * 用来存放每个客户端对应的MyWebSocket对象
	 */
	private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
	/**
	 * 与某个客户端的连接会话,需要通过它来给客户端发送数据
	 */

	private Session session;
	/**
	 * 接收 userCode
	 */
	private String userCode = "";

	/**
	 * 连接建立成功调用的方法
	 */
	@OnOpen
	public void onOpen(Session session, @PathParam("userCode") String userCode) {
		this.session = session;
		this.userCode = userCode;
		if (webSocketMap.containsKey(userCode)) {
			webSocketMap.remove(userCode);
			webSocketMap.put(userCode, this);
		} else {
			webSocketMap.put(userCode, this);
			addOnlineCount();
		}
		log.info("用户连接:" + userCode + ",当前在线人数为:" + getOnlineCount());
		try {
			sendMessage("连接成功");
		} catch (IOException e) {
			log.error("用户:" + userCode + ",网络异常");
		}
	}

	/**
	 * 连接关闭调用的方法
	 */
	@OnClose
	public void onClose() {
		if (webSocketMap.containsKey(userCode)) {
			webSocketMap.remove(userCode);
			subOnlineCount();
		}
		log.info("用户退出:" + userCode + ",当前在线人数为:" + getOnlineCount());
	}

	/**
	 * 收到客户端消息后调用的方法
	 *
	 * @param message 客户端发送过来的消息
	 */
	@OnMessage
	public void onMessage(String message, Session session) {
		log.info("用户消息:" + userCode + ",报文:" + message);
		//可以群发消息
		//消息保存到数据库、redis
		if (StringUtils.isNotBlank(message)) {
			try {
				//解析发送的报文
				JSONObject jsonObject = JSON.parseObject(message);
				//追加发送人(防止串改)
				jsonObject.put("fromUserId", this.userCode);
				String toUserCode = jsonObject.getString("toUserId");
				//传送给对应toUserId用户的websocket
				if (StringUtils.isNotBlank(toUserCode) && webSocketMap.containsKey(toUserCode)) {
					webSocketMap.get(toUserCode).sendMessage(jsonObject.toJSONString());
				} else {
					log.error("请求的toUserCode:" + toUserCode + "不在该服务器上");
					//否则不在这个服务器上,发送到mysql或者redis
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * @param session
	 * @param error
	 */
	@OnError
	public void onError(Session session, Throwable error) {
		log.error("用户错误:" + this.userCode + ",原因:" + error.getMessage());
		error.printStackTrace();
	}

	/**
	 * 实现服务器主动推送
	 */
	public void sendMessage(String message) throws IOException {
		this.session.getBasicRemote().sendText(message);
	}

	/**
	 * 发送自定义消息
	 */
	public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException {
		log.info("发送消息到:" + userId + ",报文:" + message);
		if (StringUtils.isNotBlank(userId) && webSocketMap.containsKey(userId)) {
			webSocketMap.get(userId).sendMessage(message);
		} else {
			log.error("用户" + userId + ",不在线!");
		}
	}

	public static synchronized int getOnlineCount() {
		return onlineCount;
	}

	public static synchronized void addOnlineCount() {
		WebSocketServer.onlineCount++;
	}

	public static synchronized void subOnlineCount() {
		WebSocketServer.onlineCount--;
	}

}

4、前端代码

if (typeof (WebSocket) == "undefined") {
					console.log("您的浏览器不支持WebSocket");
				} else {
					const _this = this;
					console.log("您的浏览器支持WebSocket");
					//实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接
					//等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25");
					//var socketUrl="${request.contextPath}/im/"+$("#userId").val();
					var socketUrl = "http://localhost:80/socketServer/" + this.userCode;
					socketUrl = socketUrl.replace("https", "ws").replace("http", "ws");
					console.log(socketUrl);
					if (this.socket != null) {
						this.socket.close();
						this.socket = null;
					}
					this.socket = new WebSocket(socketUrl);
					//打开事件
					this.socket.onopen = function () {
						console.log("websocket已打开");
						//socket.send("这是来自客户端的消息" + location.href + new Date());
					};
					//获得消息事件
					this.socket.onmessage = function (msg) {
						console.log(msg.data);
						//发现消息进入    开始处理前端触发逻辑
						_this.getList();
					};
					//关闭事件
					this.socket.onclose = function () {
						console.log("websocket已关闭");
					};
					//发生了错误事件
					this.socket.onerror = function () {
						console.log("websocket发生了错误");
					}
				}

5、测试是否可以连接
在这里插入图片描述
6、加入redis发布订阅
(1)redis发布消息
topic:topic名称
msg:发送的消息内容

@Component
public class RedisTopicSendTemplate {

	@Autowired
	private RedisTemplate<String, Object> redisTemplate;

	/**
	 * 发送消息
	 * @param msg
	 */
	public void sendMsg(String topic, String msg) {
		redisTemplate.convertAndSend(topic, msg);
	}
}

(2)监听消息

@Component
@Slf4j
public class SysUserListener implements MessageListener {

	/**
	 * 消息监听
	 * @param message
	 * @param pattern
	 */
	@SneakyThrows
	@Override
	public void onMessage(Message message, byte[] pattern) {
		log.info("监听到redis消息:{}", message);
		byte[] body = message.getBody();
		if (body.length > 0){
			String userCode = new String(message.getBody(), StandardCharsets.UTF_8).trim().replaceAll("\"", "");
			// 发送消息
			WebSocketServer.sendInfo(userCode, userCode);
		}
	}
}
@Configuration
public class SubscriptionConfig {

	@Bean
	MessageListenerAdapter messageListener() {
		return new MessageListenerAdapter(new SysUserListener());
	}

	@Bean
	RedisMessageListenerContainer redisContainer(RedisConnectionFactory factory) {
		RedisMessageListenerContainer container = new RedisMessageListenerContainer();
		container.setConnectionFactory(factory);
		// 支持多个topic
		container.addMessageListener(messageListener(), new ChannelTopic(RedisConstant.SYS_USER_TOPIC));
		return container;
	}
}

7、nginx配置
在这里插入图片描述

  • 9
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值