一 前言
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();
}
}