分布式webSocket session无法共享问题
问题:
最近碰到一个麻烦的事情,就是在使用webSocket推送消息时候,发现session存放在各个节点,各节点之间无法获取session。而且经过研究发现:WebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议,调用的WebSocket的send函数在实现中最终都是通过TCP的系统接口进行传输的。WebSocket和Http协议一样都属于应用层的协议,WebSocket在建立握手连接时,数据是通过http协议传输的,但是在建立连接之后,真正的数据传输阶段是不需要参与的
分析
也就是说目前问题:1、wehsocketSession无法序列化,即无法存放在redis里面达到共享
2、就算能在别的节点创建session对象,也无法推送消息,因为webSocket是基于tcp的协议,http,https在传输数据上是不会参与的,也就是说与哪个节点建立的连接必须那个节点推送消息
解决思路
1 使用订阅发布服务工具,各个节点即为订阅方也为发布方,一个节点发布,其余节点收到消息,检测自己是否存在改session,存在通过收到的消息推送数据。
2记录session关键信息(节点ip,端口,用户数据)到缓存里面(推荐redis),通过查找session关键信息,定位到哪个节点,通过http请求改节点,达到推送效果(涉及http请求,可能响应回慢)
ps:本来想使用方法一,通过redis订阅发布服务实现消息推送,但是项目节点很多,如果一个节点发布,其余都要查找,担心增加服务器压力,所以本人选择方法二,但是方法一我会在最下面做简单介绍
websocket服务部署:
redsi 存放关键信息,ip,端口
@ServerEndpoint("/signWebsocket/{mobile}")
@Component
public class SignWebSocket {
private static final Logger LOG = LoggerFactory.getLogger(SignWebSocket.class);
private static SignWebSocket instance = new SignWebSocket();
//保存客户端发起的唯一标识与长连接
private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<String, Session>();
public static final String KEY_SOCKET_SESSION = "socket_session_";
private static int KEY_SOCKET_TIME = 3000;
private RedisCache redisCache;
public SignWebSocket() {
ApplicationContext act = ApplicationContextRegister.getApplicationContext();
this.redisCache = act.getBean(RedisCache.class);
}
public static ConcurrentHashMap<String, Session> getSessionMap() {
return sessionMap;
}
public static SignWebSocket getInstance() {
return instance;
}
/**
* 连接建立成功调用的方法
*
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(@PathParam("mobile") String mobile, Session session) {
String path= AllConstant.getInstance().getLocalAddr()+":"+AllConstant.getInstance().getLocalPort();
try {
if (sessionMap.containsKey(mobile)) {
Session session_old = sessionMap.get(mobile);
session_old.close();
sessionMap.remove(mobile);
}
} catch (IOException e) {
LOG.error(e.getMessage(),e);
}
sessionMap.put(mobile, session);
this.redisCache.set(KEY_SOCKET_SESSION + mobile, path, 3000);
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam("mobile") String mobile, Session session) {
try {
if (sessionMap.containsKey(mobile)) {
// System.out.println("调用onClose:"+mobile);
session = sessionMap.get(mobile);
session.close();
sessionMap.remove(mobile);
}
} catch (Exception ex) {
LOG.error(ex.getMessage(), ex);
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@OnMessage
public void onMessage(@PathParam("mobile") String mobile, String message, Session session) {
try {
} catch (Exception e) {
LOG.error(e.getMessage(),e);
}
}
/**
* 发生错误时调用
*
* @param session
* @param error
*/
@OnError
public void onError(@PathParam("mobile") String mobile, Session session, Throwable error) {
try {
if (sessionMap.containsKey(mobile)) {
sessionMap.remove(mobile);
}
session.close();
} catch (Exception ex) {
LOG.error(ex.getMessage(), ex);
}
}
/**
* mobile ,向对应的客户端推送消息
*/
public void pushMessage(String mobile, int coin, int flow) {
try {
Session session;
System.out.println("sessionMap ========={}"+sessionMap);
if (sessionMap.containsKey(mobile)) {
JSONObject user = new JSONObject();
user.put("coin", coin);
user.put("flow", flow);
session = sessionMap.get(mobile);
if (null != session) {
session.getBasicRemote().sendText(user.toString());
}
}
} catch (Exception ex) {
LOG.error(ex.getMessage(), ex);
}
}
}
http监听请求,推送消息
@RequestMapping( value = {"/socket"},method = {RequestMethod.GET})
@ResponseBody
public JSONObject socket(String phone,Integer coin,Integer flow , HttpServletRequest request){
JSONObject obj =new JSONObject();
logger.error("开始推送");
System.out.println("socket phone:"+phone+" coin:"+coin+" flow:"+flow);
try{
SignWebSocket.getInstance().pushMessage(phone,coin,flow);
}catch (Exception e){
e.printStackTrace();
}
obj.put("phone",phone);
obj.put("coin",coin);
obj.put("flow",flow);
return obj;
}
方法二:
使用redis订阅发布服务,每个节点都是发布方,其他节点都是订阅方,当接收到发布的消息时,检测webSocket sessionMap里面session,推送消息。