springboot+websocket+redis遇到的websocket集群加Redis发布订阅的问题

wzmlove1121 2019-03-04 04:05:48
求助背景:
现在项目要做一个登陆控制的功能,就是常见的APP多客户端登陆退出问题,当A用户在C1客户端登陆后,C2客户端要实时退出(该用户在C2客户端处于活动状态),做到类似支付宝哪有的效果.
现在情况:
上周完成了部分代码,实现了当用户打开APP后,前端通过websocket连接发送消息到后端,我后端代码如下(代码有删减,重要代码未删减)

@OnOpen
public void onOpen(Session session, EndpointConfig config)
{
SessionSet.add(session);
//打算通过session实现,但是没缕清思路
httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
String token = session.getPathParameters().get("token");// token是前端打开websocket连接时带的参数信息,如果没有表示是打开的APP没有登陆过
// 客户没有在APP端操作退出操作,直接kill程序之后再次打开程序的场景
// 或者客户将程序挂在后台,系统将APP kill之后,用户再次打开,需要重新连接websocket
// 以上情况下,客户端是带有token信息的 token要不要带Bearer 还需要跟前端确认
if (!Stringutils.isEmptyString(token))
{
String userId = manager.getUserByToken(token);
String redisToken = redisUtils.get(userId);
if (ValidateHelper.isEmptyString(redisToken))
{
// 如果Redis里面的token是空的,说明登录已经失效了
sendObject(session, new Response<>(LOGIN_DISABLED, LOGIN_DISABLED.getErrMsg());

}
if (!Stringutils.isEmptyString(redisToken) && !redisToken.equals(token))
{
// 如果Redis里面存在该用户的token,并且该token跟传入的token不一致,那么就说明该用户在其他地方登陆过,踢出现有用户
sendObject(session, new Response<>(LOGIN_REPEAT.getErrCode(), LOGIN_REPEAT.getErrMsg()));
}
// 如果密码没有失效,并且没有被替换,说明当前登陆的用户的唯一登陆的了
uidSessionMap.put(userId, session);
}
}

另外一种就是打开APP但是用户没有登陆,登陆的时候会通过websocket连接发送消息到后端,代码如下:

@OnMessage
public void onMessage(String message, Session session)
{
log.info("来自客户端的消息:{}", message);
if (Boolean.parseBoolean(message)) // 此处是用来测试的代码
{
httpSession.setAttribute(session.getId(), message);
loginOfflinePublisher.send(session.getId() + ":" + message);
sendMessage(session, message);
}
// 如果客户端传入的是token信息
if (message.startsWith("Bearer "))
{
// 获取到当前的token是属于哪个用户的
String userId = manager.getUserByToken(message.substring(7));
Session session1 = uidSessionMap.put(userId, session);
// 如果当前服务器没有登录用户的信息,就发布消息让其他服务器去检查自己是否有该用户的信息,有就踢出该用户
// 注意要把当前连接的session的信息传递过去,防止当前服务器把当前登录用户也给踢出去了(此处有问题)
if (Objects.isNull(session1))
{
loginOfflinePublisher.send(session.getId() + ":" + userId);
} else
{
// 如果在本服务器,直接踢出该用户
sendObject(session1, new Response<>(String.valueOf(LOGIN_REPEAT.getErrCode()), LOGIN_REPEAT.getErrMsg()));
}
}
}


现在遇到的问题是:
因为websocket是搭的集群,各个集群之间是通过Redis的发布订阅功能来实现消息的传递的,loginOfflinePublisher.send方法就是调用Redis的发布消息接口,通过发布需要踢出的用户ID,然后其他websocket节点接收消息后,然后扫描当前容器中的uidSessionMap集合,找出对应的session连接,然后给改session连接发送退出消息,让前端做退出操作.

有个情况就是:Redis的发布消息后,所有的websocket节点都收到了消息,包括触发redis发布消息的这个节点,因此就会导致我当前登录的机器也把该用户给踢出来了;

我尝试过用httpsession来保存当前登录的用户ID及websocket连接的session id信息,然后在Redis发布消息的时候,做判断把当前机器排除了,如下代码:

@Slf4j
public class LoginOfflineListener implements MessageListener
{

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Autowired // 一直无法注入,提示No thread-bound request found
private HttpSession httpSession;

@Override
public void onMessage(Message message, byte[] pattern)
{
doBusiness(message);
}

/**
* 打印 message body 内容
*
* @param message
*/
public void doBusiness(Message message)
{
log.info("收到Redis发布的消息...");
String value = (String) stringRedisTemplate.getValueSerializer().deserialize(message.getBody());
// 收到发布的消息后,Redis向其它服务器发布订阅的消息
String[] sessionIdAndUserId = new String[0];
if (value != null)
{
sessionIdAndUserId = value.split(":");
}
String attribute = (String) WebSocketServer.httpSession.getAttribute(sessionIdAndUserId[0]);
// 在这里排除当前机器
if (!ValidateHelper.isEmptyString(attribute) && sessionIdAndUserId.length > 1 && !attribute.equals(sessionIdAndUserId[1]))
{
WebSocketServer.sendMessage(sessionIdAndUserId);
}
}
}


但是感觉session id可能会有重复,我测试过,不同机器的sessionID都是从0递增的,因此不敢用这种方式,同时对于httpsession,我一直不能直接注入进去.

主要是想解决,如何在Redis发布消息后,本机可以排除在外

现在感觉钻空子里面去了,所以想问下各位,有没有遇到过这样的业务场景或者有什么好的实现方式,谢谢了!!!




...全文
1258 1 打赏 收藏 转发到动态 举报
写回复
用AI写文章
1 条回复
切换为时间正序
请发表友善的回复…
发表回复
wzmlove1121 2019-03-06
  • 打赏
  • 举报
回复
已解决,谢谢!

81,092

社区成员

发帖
与我相关
我的任务
社区描述
Java Web 开发
社区管理员
  • Web 开发社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧