nginx分布式集群中使用websocket,(redis订阅)

一 前言

1.1 原理

websocket核心是将握手成功的session信息存入到静态变量中,但是分布式中有很多个服务器,tomcat,使用nginx转发到不同的tomcat中,就不能保证此tomcat存储着握手成功的session信息,因此可以使用redis订阅功能,当发送消息时可以使用redis订阅,发送到已经订阅过该频道的服务器,从每个服务器中查找是否有接收人的session信息,有就发送消息。
springboot已经封装的很好,本文使用普通的开发框架进行处理

注意

实现的场景是nginx分布式环境,系统的session共享已经实现的前提下。
session共享本文不作描述。代码不够优雅,但原理是这样的

二 订阅频道,接收消息的config

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.user.MultiServerUserRegistry;
import org.springframework.messaging.simp.user.SimpUserRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.msunsoft.common.utils.Constants;
import com.msunsoft.websocket.oaNotify.hand.MessageReceiver;
import com.msunsoft.websocket.oaNotify.hand.MessageReceiver2;
import com.msunsoft.websocket.oaNotify.interceptor.WebSocketInterceptor;

@Configuration
public class RedisObserverConfig {
	//通知公告订阅的频道
//	public static final String TOPIC_OA_NOTIFY = "websocket:oa_notify";

//    @Autowired
//    private JedisConnectionFactory jedisConnectionFactory;
	
  @Autowired
  private LettuceConnectionFactory jedisConnectionFactory;
    //线程调用
    @Resource(name = "taskExecutor")
    private TaskExecutor          taskExecutor;
 
   /**
    * 添加监听,收到频道消息时执行(默认方式写法)
    * @return
    */
   
//    @Bean
//    MessageListenerAdapter messageListener(Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer) {
//        return new MessageListenerAdapter(oaNotifyListener());
//    }
    
//    public SimpUserRegistry userRegistry() {
//    	SimpUserRegistry userRegistry = new MultiServerUserRegistry(userRegistry);
//    }
    /**
     * 使用Jackson序列化对象
     */
    @Bean
    public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer(){
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(objectMapper);

        return serializer;
    }
    /**
     * RedisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory jedisConnectionFactory, Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(jedisConnectionFactory);

        //字符串方式序列化KEY
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);

        //JSON方式序列化VALUE
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
    /**
     * 消息监听器,收到消息
     */
    @Bean
    MessageListenerAdapter messageListenerAdapter(MessageReceiver messageReceiver, Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer){
        //消息接收者以及对应的默认处理方法
        MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(messageReceiver, "receiveMessage");
        //消息的反序列化方式
        messageListenerAdapter.setSerializer(jackson2JsonRedisSerializer);

        return messageListenerAdapter;
    }
    //订阅频道
    @Bean
    RedisMessageListenerContainer redisContainer( MessageListenerAdapter messageListenerAdapter) {
        final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(jedisConnectionFactory);
        container.addMessageListener(messageListenerAdapter, new PatternTopic(Constants.TOPIC_OA_NOTIFY));
//        container.setTaskExecutor(Executors.newFixedThreadPool(4));
        container.setTaskExecutor(taskExecutor);
        return container;
    }
    //用于发送消息
//    @Bean
//    StringRedisTemplate template(RedisConnectionFactory connectionFactory) {
//    	return new StringRedisTemplate(connectionFactory);
//    }
//    @Bean
//    SimpMessagingTemplate messageTemp(MessageChannel messageChannel) {
//    	return new SimpMessagingTemplate(messageChannel);
//    }
/**
 * 订阅频道
 * @return
 */
//    @Bean
//    ChannelTopic orderFoodTopic() {
//        return new ChannelTopic(Constants.TOPIC_OA_NOTIFY);
//    }

  
}

三 自定义接收消息后执行的方法

import org.springframework.stereotype.Component;

import com.msunsoft.websocket.oaNotify.entity.RedisWebsocketMsg;

@Component
public interface  MessageReceiver {
	public void receiveMessage(RedisWebsocketMsg redisWebsocketMsg);
}

四 websocket握手及实现自定义接收消息的接口

import java.io.IOException;
import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.spi.LoggerFactoryBinder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import com.msunsoft.common.utils.Constants;
import com.msunsoft.common.utils.JsonUtils;
import com.msunsoft.common.utils.SpringUtils;
import com.msunsoft.modules.sys.entity.User;
import com.msunsoft.modules.sys.utils.UserUtils;
import com.msunsoft.websocket.oaNotify.entity.RedisWebsocketMsg;
import com.msunsoft.websocket.oaNotify.hand.MessageReceiver;
import com.msunsoft.websocket.oaNotify.service.RedisService;

import net.sf.json.JSONArray;

//ServerEndpoint它的功能主要是将目前的类定义成一个websocket服务器端。注解的值将被用于监听用户连接的终端访问URL地址。
@ServerEndpoint(value = “/socket/{userId}”)
@Component
//public class WebSocketServer {
public class WebSocketServer implements MessageReceiver{ //implements WebSocketConfigurer
//使用slf4j打日志
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketServer.class);

//用来记录当前在线连接数
private static int onLineCount = 0;
/**
 * 因为@ServerEndpoint不支持注入,所以使用SpringUtils获取IOC实例
 */

// private RedisMessageListenerContainer redisMessageListenerContainer = SpringUtils.getBean(RedisMessageListenerContainer.class);
private RedisService redisService = SpringUtils.getBean(RedisService.class);

private ThreadPoolTaskExecutor threadPool =  SpringUtils.getBean("taskExecutor")  ;
//用来存放每个客户端对应的WebSocketServer对象
private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<String, WebSocketServer>();

//某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;

//客户端的id地址
private String userId;


/**
 * 连接建立成功,调用的方法,与前台页面的onOpen相对应
 * @param ip ip地址
 * @param session 会话
 */
@OnOpen
public void onOpen(@PathParam("userId")String userId,Session session){
    //根据业务,自定义逻辑实现
    //如果在其他tomcat中添加了此用户信息则不再重复添加
    if(null != webSocketMap && null !=webSocketMap.get(userId)) {//如果是本tomcat添加的用户信息则不再向redis中添加信息
    	
    }else if(redisService.isSetMember(Constants.REDIS_WEBSOCKET_USER_SET, userId) &&(null == webSocketMap || null == webSocketMap.get(userId))) {
    	onMessage("close", session);
    	//将session放入此服务器中
    	this.session = session;
        this.userId = userId;
        webSocketMap.put(userId,this);  //将当前对象放入map中
    }else {
    	this.session = session;
        this.userId = userId;
        webSocketMap.put(userId,this);  //将当前对象放入map中
        addOnLineCount();  //在线人数加一
        LOGGER.info("有新的连接加入,ip:{}!当前在线人数:{}",userId,getOnLineCount());
        //将信息保存到redis
        redisService.addToSet(Constants.REDIS_WEBSOCKET_USER_SET, userId);
    }
    
    
}
/**
 * 连接关闭调用的方法,与前台页面的onClose相对应
 * @param ip
 */
@OnClose
public void onClose(@PathParam("userId")String userId){
	//从Redis中移除用户
    webSocketMap.remove(userId);  //根据ip(key)移除WebSocketServer对象
    redisService.removeFromSet(Constants.REDIS_WEBSOCKET_USER_SET, userId);
    subOnLineCount();
    LOGGER.info("WebSocket关闭,ip:{},当前在线人数:{}",userId,getOnLineCount());
}

/**
 * 当服务器接收到客户端发送的消息时所调用的方法,与前台页面的onMessage相对应
 * @param message
 * @param session
 */
@OnMessage
public void onMessage(String message,Session session){
    //根据业务,自定义逻辑实现
    LOGGER.info("收到客户端的消息:{}",message);
    sendMessage(message,session);//保留
}

/**
 * 发生错误时调用,与前台页面的onError相对应
 * @param session
 * @param error
 */
@OnError
public void onError(Session session,Throwable error){
    LOGGER.error("WebSocket发生错误");
    error.printStackTrace();
}


/**
 * 给当前用户发送消息
 * @param message
 */
public void sendMessage(String message,Session session){
    try{
        //getBasicRemote()是同步发送消息,,推荐使用getAsyncRemote()异步
        session.getBasicRemote().sendText(message);
    }catch (IOException e){
        e.printStackTrace();
        LOGGER.info("发送数据错误:,ip:{},message:{}",userId,message);
    }
}
/**
     *    给指定用户发送消息
 * @param message
 */
public void sendMessageToId(String message,String id){
	Session session = webSocketMap.get(id).session;
    sendMessage(message,session);
}
/**
 * 给所有用户发消息
 * @param message
 */
public static void sendMessageAll(final String message){
    //使用entrySet而不是用keySet的原因是,entrySet体现了map的映射关系,遍历获取数据更快。
    Set<Map.Entry<String, WebSocketServer>> entries = webSocketMap.entrySet();
    for (Map.Entry<String, WebSocketServer> entry : entries) {
        final WebSocketServer webSocketServer = entry.getValue();
        //这里使用线程来控制消息的发送,这样效率更高。
        new Thread(new Runnable() {
            public void run() {
                webSocketServer.sendMessage(message,webSocketServer.session);
            }
        }).start();
    }
}

/**
 * 获取当前的连接数
 * @return
 */
public static synchronized int getOnLineCount(){
    return WebSocketServer.onLineCount;
}

/**
 * 有新的用户连接时,连接数自加1
 */
public static synchronized void addOnLineCount(){
    WebSocketServer.onLineCount++;
}

/**
 * 断开连接时,连接数自减1
 */
public static synchronized void subOnLineCount(){
    WebSocketServer.onLineCount--;
}

public Session getSession(){
    return session;
}
public void setSession(Session session){
    this.session = session;
}

/**
 * 接收订阅信息
 */
public void receiveMessage(RedisWebsocketMsg rdisWebsocketMsg) {
	// TODO Auto-generated method stub
	Map<String, String> recieveUserMap = rdisWebsocketMsg.getReceiver();
	
	 for (Map.Entry<String, String> m : recieveUserMap.entrySet()) {
		 ThreadSendMsage threadSendMsage = new ThreadSendMsage(m.getKey(),m.getValue(),rdisWebsocketMsg);
		 threadPool.execute(threadSendMsage);
	}
	LOGGER.info("发送消息成功了!");
}
//使用线程发送消息
public class ThreadSendMsage implements Runnable  {
    private String recieveUserId;
    private String msgId;
    private RedisWebsocketMsg rdisWebsocketMsg;
    
	public ThreadSendMsage(String recieveUserId, String msgId, RedisWebsocketMsg rdisWebsocketMsg) {
		super();
		this.recieveUserId = recieveUserId;
		this.msgId = msgId;
		this.rdisWebsocketMsg = rdisWebsocketMsg;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		rdisWebsocketMsg.setMsgId(msgId);
		sendMessageToUser(recieveUserId,  rdisWebsocketMsg);
	}
	
}
public boolean sendMessageToUser(String recieveUserId, RedisWebsocketMsg rdisWebsocketMsg) {
	WebSocketServer webSocketServer = webSocketMap.get(recieveUserId);
	if(webSocketServer==null) {
		return false;
	}
	LOGGER.info("进入发送消息");
	if (!webSocketServer.session.isOpen()) {//通道处于关闭状态,删除redis中的记录信息
		LOGGER.info("websocket通道关闭---------------");
		return false;
    }
	try {
		LOGGER.info("正在发送消息");
		String msg = JsonUtils.objectToJson(rdisWebsocketMsg);
		webSocketServer.sendMessage(msg,webSocketServer.session);
	} catch (Exception e) {
		e.printStackTrace();
	}
    return true;
}

}

五 接收消息的实体类po

public class RedisWebsocketMsg {
/**
* 消息接收者的userId
/
private Map<String, String> receiver;//key为userID,value为消息id
/
*
* 消息对应的订阅频道的CODE,参考{@link cn.zifangsky.mqwebsocket.enums.WebSocketChannelEnum}的code字段
*/
private String channelCode;

private String title;//消息标题
private String sender;//发送者名称
private String msgId;//消息id
private String receiverId;//接收者的id
/**
 * 消息正文
 */
private String content;

public RedisWebsocketMsg() {

}

public RedisWebsocketMsg(Map<String, String> receiver, String channelCode, String content) {
    this.receiver = receiver;
    this.channelCode = channelCode;
    this.content = content;
}

public RedisWebsocketMsg(Map<String, String> receiver, String channelCode, String title, String sender, String content) {
	this.receiver = receiver;
	this.channelCode = channelCode;
	this.title = title;
	this.sender = sender;
	this.content = content;
}

public String getReceiverId() {
	return receiverId;
}

public void setReceiverId(String receiverId) {
	this.receiverId = receiverId;
}

public String getMsgId() {
	return msgId;
}

public void setMsgId(String msgId) {
	this.msgId = msgId;
}

public String getTitle() {
	return title;
}

public void setTitle(String title) {
	this.title = title;
}

public String getSender() {
	return sender;
}

public void setSender(String sender) {
	this.sender = sender;
}

public Map<String, String> getReceiver() {
    return receiver;
}

public void setReceiver(Map<String, String> receiver) {
    this.receiver = receiver;
}

public String getContent() {
    return content;
}

public void setContent(String content) {
    this.content = content;
}

public String getChannelCode() {
    return channelCode;
}

public void setChannelCode(String channelCode) {
    this.channelCode = channelCode;
}

@Override
public String toString() {
    return "RedisWebsocketMsg{" +
            "receiver='" + receiver + '\'' +
            ", channelCode='" + channelCode + '\'' +
            ", content=" + content +
            '}';
}

}

六 redis 服务(用于存储和查询,删除用户握手信息)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 *
 * @author zifangsky
 * @date 2018/7/30
 * @since 1.0.0
 */
@Service("redisService")
public class RedisService  {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void set(String key, Object value) {
        ValueOperations<String, Object> valueOperation = redisTemplate.opsForValue();
        valueOperation.set(key, value);
    }

    public void setWithExpire(String key, Object value, long time, TimeUnit timeUnit) {
        BoundValueOperations<String, Object> boundValueOperations = redisTemplate.boundValueOps(key);
        boundValueOperations.set(value);
        boundValueOperations.expire(time,timeUnit);
    }

    public <K> K get(String key) {
        ValueOperations<String, Object> valueOperation = redisTemplate.opsForValue();

        return (K) valueOperation.get(key);
    }

    public void delete(String key) {
         redisTemplate.delete(key);
    }

//    public void addToListLeft(String listKey, ExpireEnum expireEnum, Object... values) {
//        //绑定操作
//        BoundListOperations<String, Object> boundValueOperations = redisTemplate.boundListOps(listKey);
//        //插入数据
//        boundValueOperations.leftPushAll(values);
//        //设置过期时间
//        boundValueOperations.expire(expireEnum.getTime(),expireEnum.getTimeUnit());
//    }

//    public void addToListRight(String listKey, ExpireEnum expireEnum, Object... values) {
//        //绑定操作
//        BoundListOperations<String, Object> boundValueOperations = redisTemplate.boundListOps(listKey);
//        //插入数据
//        boundValueOperations.rightPushAll(values);
//        //设置过期时间
//        boundValueOperations.expire(expireEnum.getTime(),expireEnum.getTimeUnit());
//    }

    public List<Object> rangeList(String listKey, long start, long end) {
        //绑定操作
        BoundListOperations<String, Object> boundValueOperations = redisTemplate.boundListOps(listKey);
        //查询数据
        return boundValueOperations.range(start, end);
    }

    public void addToSet(String setKey, Object... values) {
        SetOperations<String, Object> opsForSet = redisTemplate.opsForSet();
        opsForSet.add(setKey, values);
    }

    public Boolean isSetMember(String setKey, Object value) {
        SetOperations<String, Object> opsForSet = redisTemplate.opsForSet();

        return opsForSet.isMember(setKey, value);
    }

    public void removeFromSet(String setKey, Object... values) {
        SetOperations<String, Object> opsForSet = redisTemplate.opsForSet();
        opsForSet.remove(setKey, values);
    }

    public void convertAndSend(String channel, Object message) {
        redisTemplate.convertAndSend(channel, message);
    }
}

七 xml中redis配置信息

<?xml version="1.0" encoding="UTF-8"?>

<description>Jedis Configuration</description>

<!-- 加载配置属性文件 -->
<context:property-placeholder ignore-unresolvable="true" location="classpath:jeesite.properties" />

<bean id="redisHttpSessionConfiguration"
class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="1800" />
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
	<property name="maxIdle" value="300" /> <!-- 最大能够保持idel状态的对象数  -->
	<property name="maxTotal" value="60000" /> <!-- 最大分配的对象数 -->
	<property name="testOnBorrow" value="true" /> <!-- 当调用borrow Object方法时,是否进行有效性检查 -->
</bean>

<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
	<constructor-arg index="0" ref="jedisPoolConfig" />
	<constructor-arg index="1" value="${redis.host}" type="java.lang.String"/>
	<constructor-arg index="2" value="${redis.port}" type="int" />
	<!-- <constructor-arg index="3" value="${redis.timeout}" type="int" />
	<constructor-arg index="4" value="${redis.password}"/>
	<constructor-arg index="5" value="${redis.database}" type="int" />
	<constructor-arg index="6" value="${redis.clientName}"/> -->
</bean>
</beans>

八 前端握手

var host = window.location.host;
var hostArray = host.split(":");
host = hostArray[0];
 var port = '<%=port%>';
 var url = window.location.pathname;
 var webApp = url.split('/')[1];
 var url = host+":"+port  +"/"+ webApp +"/socket";
var userId = '${userId}';
function initWebsocket(){
	var webSocket = null;
	var url = document.location.host; 
	if ('WebSocket' in window) {
	   webSocket = new WebSocket("ws://" + url + "/"+userId);
	}
	else if ('MozWebSocket' in window) {
	     webSocket = new MozWebSocket("ws://" + url + "/"+userId);
	}
	else {
	    alert('Not support webSocket');
	}
	// 打开连接时
	webSocket.onopen = function(evnt) {
	  console.log("  websocket.onopen  ");
	  webSocket.send("定时");
	};
	// 收到消息时
	webSocket.onmessage = function(evnt) {
		if(open != null){
			dialog.close(open);
			if(evnt.data != ""){
				open = dialog.open({
					width:350,
					height:250,
					shade:false,
					title:"医保消息提醒",
					position:[400,10,"auto","auto"],
					move:true,
					url:"${path}/jsp/drugAudit/messageReminder.jsp?result="+encodeURIComponent(evnt.data),
					content:""
				});
			}
		}
	};
	webSocket.onerror = function(evnt) {
	  console.log("  websocket.onerror  ");
	};
	webSocket.onclose = function(evnt) {
	  console.log("  websocket.onclose  ");
	};
	//监听窗口关闭
	window.onbeforeunload = function (event) {
	    webSocket.close();
	}
} 
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值