結合redis訂閱和發佈消息,解決websocket多節點問題
單節點和多節點下,websocket會出現什麼問題呢?看如下兩個對比圖:
這時候你會發現有部分連接在ws node2節點上用戶收不到消息推送。而且websocket的session是無法共享的,加上session是有序無法存入到redis緩存中。
瞭解大概內容後,直接進入到代碼模塊
1.相關依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>8.5.23</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2.在RedisConfig加入消息監聽容器
@Configuration
public class RedisConfig{
/**
* 使CacheComponent的redisTemplate組件的key使用StringRedisSerializer而非默認的JdkSerializationRedisSerializer
* 避免key出現字節碼的情況
* @param factory redis鏈接
* @return RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
RedisSerializer redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//key使用StringRedisSerializer序列化
redisTemplate.setKeySerializer(redisSerializer);
//value使用jackson2JsonRedisSerializer序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
/**
* 創建Redis消息監聽者容器
* @param factory
* @return
*/
@Bean("redisMessageListenerContainer")
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
return container;
}
}
3.新建消息訂閱監聽類
import com.opp.util.CommonUtil;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
import javax.websocket.Session;
import java.io.IOException;
/**
* @author: huangnenghuan
* @create: 2019/12/03
* @description: 創建消息訂閱監聽者類
**/
@Component
public class RedisMessageListener implements MessageListener {
private org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(RedisMessageListener.class);
/**
* websocket客戶端連接會話對象
*/
private Session session;
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
@Override
public void onMessage(Message message, byte[] bytes) {
String msg = new String(message.getBody()).replace("\"", "");
if(CommonUtil.isNotNull(session) && session.isOpen()){
try {
session.getBasicRemote().sendText(msg);
}catch (IOException e){
logger.error("RedisSubListener消息訂閱監聽異常:" + e.getMessage());
}
}
}
}
4.對websocket進行修改
import com.opp.listener.RedisMessageListener;
import com.opp.util.CommonUtil;
import com.opp.util.SpringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.web.bind.annotation.RestController;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author: huangnenghuan
* @create: 2019/11/25
* @description: websocket
**/
@RestController
@ServerEndpoint("/webSocket/{id}")
public class WebSocketController {
private org.slf4j.Logger logger = LoggerFactory.getLogger(WebSocketController.class);
/**
* 用來記錄當前連接數的變量
*/
private static volatile int onlineCount = 0;
public static ConcurrentHashMap<String, WebSocketController> webSocketSet = new ConcurrentHashMap<>();
/**
* 與某個客戶端的連接會話,需要通過它來與客戶端進行數據收發
*/
private Session session;
/**
* 用來引入剛纔在webcoketConfig注入的類
*/
private RedisMessageListenerContainer container = SpringUtils.getBean("redisMessageListenerContainer");
/**
* 自定義的消息發送器
*/
private RedisMessageListener listener;
/**
* 連接建立成功調用的方法
* @param id
* @param session
* @throws Exception
*/
@OnOpen
public void onOpen(@PathParam("id") String id, Session session) throws Exception {
try {
this.session = session;
webSocketSet.put(id, this);
//在線人數增加1
addOnlineCount();
sendMessage("連接成功");
listener = new RedisMessageListener();
//放入session
listener.setSession(session);
container.addMessageListener(listener, new PatternTopic("liveBroadcast"));
} catch (IOException e) {
logger.error("websocket IO異常");
}
}
/**
* 連接關閉調用方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this);
subOnlineCount();
logger.info("websocket有一連接關閉");
container.removeMessageListener(listener);
}
/**
* 收到客戶端消息後調用方法
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) throws Exception{
if (session.isOpen()) {
session.getBasicRemote().sendText(message);
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
logger.error("websocket 發生異常", error);
}
/**
* 向客戶端發送消息推送
* @param message
* @throws Exception
*/
public void sendMessage(String message) throws Exception {
if (this.session.isOpen()) {
this.session.getBasicRemote().sendText(message);
}
}
/**
* 發送信息給指定ID用戶,如果用戶不在線則返回
* @param message
* @param sendUserId
* @throws IOException
*/
public void sendUserMessage(String message,String sendUserId) throws Exception {
if(CommonUtil.isNotNull(webSocketSet.get(sendUserId))){
webSocketSet.get(sendUserId).sendMessage(message);
}else{
logger.info("當前用戶不在線:" + sendUserId);
}
}
/**
* 推送信息給所有用戶
* @param message
* @throws IOException
*/
public void sendAllMessage(String message) throws IOException {
for(String key : webSocketSet.keySet()){
try {
webSocketSet.get(key).sendMessage(message);
}catch (Exception e){
logger.info("推送信息給所有用戶異常:" + e.getMessage());
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketController.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketController.onlineCount--;
}
}
/**
* @author: huangnenghuan
* @create: 2019/12/03
* @description:
**/
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
//定義爲final類
@Component
public final class SpringUtils implements BeanFactoryPostProcessor {
//靜態
private static ConfigurableListableBeanFactory beanFactory; // Spring應用上下文環境
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
SpringUtils.beanFactory = beanFactory;
}
/**
* 獲取對象
*
* @param name
* @return Object 一個以所給名字註冊的bean的實例
* @throws org.springframework.beans.BeansException
*
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException {
return (T) beanFactory.getBean(name);
}
/**
* 獲取類型爲requiredType的對象
*
* @param clz
* @return
* @throws org.springframework.beans.BeansException
*
*/
public static <T> T getBean(Class<T> clz) throws BeansException {
@SuppressWarnings("unchecked")
T result = (T) beanFactory.getBean(clz);
return result;
}
/**
* 如果BeanFactory包含一個與所給名稱匹配的bean定義,則返回true
*
* @param name
* @return boolean
*/
public static boolean containsBean(String name) {
return beanFactory.containsBean(name);
}
/**
* 判斷以給定名字註冊的bean定義是一個singleton還是一個prototype。
* 如果與給定名字相應的bean定義沒有被找到,將會拋出一個異常(NoSuchBeanDefinitionException)
*
* @param name
* @return boolean
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
return beanFactory.isSingleton(name);
}
/**
* @param name
* @return Class 註冊對象的類型
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
return beanFactory.getType(name);
}
/**
* 如果給定的bean名字在bean定義中有別名,則返回這些別名
*
* @param name
* @return
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
return beanFactory.getAliases(name);
}
}
最後redis發佈通道消息
cacheComponent.sendMessage("liveBroadcast", "Barrage-" + String.valueOf(obj));