Appearance
java
public class Constants {
/** channel绑定的userId Key */
public static final String UserId = "userId";
/** channel绑定的appId Key */
public static final String AppId = "appId";
/** channel绑定的ClientType Key */
public static final String ClientType = "clientType";
public static class RedisConstants {
/** 用户SessionKey:appId+userSession+用户id */
public static final String UserSessionConstants = "%s:userSession:%s";
}
}统一管理Socket连接工具类
在上一文登录我们写了获取和新增方法,现在需要删除方法,删除可根据key或value进行删除;其中根据value删除的逻辑是:
- 使用filter找到value等于当前channel的key
- 再根据key删除
java
public class SessionSocketHolder {
private static final Map<UserClientDto, NioSocketChannel> CHANNELS = new ConcurrentHashMap<>();
public static void put(Integer appId, String userId, Integer clientType, NioSocketChannel channel) {
UserClientDto dto = new UserClientDto(appId, userId, clientType);
CHANNELS.put(dto, channel);
}
public static NioSocketChannel get(Integer appId, String userId, Integer clientType) {
UserClientDto dto = new UserClientDto(appId, userId, clientType);
return CHANNELS.get(dto);
}
/** 根据Key删除 */
public static void remove(Integer appId, String userId, Integer clientType) {
UserClientDto dto = new UserClientDto(appId, userId, clientType);
CHANNELS.remove(dto);
}
/** 根据value删除 */
public static void remove(NioSocketChannel channel) {
CHANNELS.entrySet().stream().filter(entry -> entry.getValue().equals(channel)).forEach(entry -> CHANNELS.remove(entry.getKey()));
}
}退出登录
java
public class NettyServerHandler extends SimpleChannelInboundHandler<Message> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception {
Integer command = msg.getMessageHeader().getCommand();
if (command == SystemCommand.LOGIN.getCommand()) {
login(ctx, msg);
} else if (command == SystemCommand.LOGOUT.getCommand()) {
logout(ctx, msg);
}
}
/**
* 退出登录
*/
private void logout(ChannelHandlerContext ctx, Message msg) {
// 1.删除Session
String userId = (String) ctx.channel().attr(AttributeKey.valueOf(Constants.UserId)).get();
Integer appId = (Integer) ctx.channel().attr(AttributeKey.valueOf(Constants.AppId)).get();
Integer clientType = (Integer ) ctx.channel().attr(AttributeKey.valueOf(Constants.ClientType)).get();
SessionSocketHolder.remove(appId, userId, clientType);
// 2.redis删除
RedissonClient redissonClient = RedisManager.getRedissonClient();
RMap<String, String> map = redissonClient.getMap(String.format(Constants.RedisConstants.UserSessionConstants, appId, userId));
map.remove(clientType);
ctx.channel().close();
}
}退出登录与用户离线
- 离线状态:心跳检测到客户端不活跃,用户并没有点击退出登录;此时只清除channel连接,即channel中存放的信息,并不清除redis中的数据,但需要修改Session状态为“离线”。
- 退出登录:用户主动点击退出登录,清空channel连接信息以及redis存储的session;
- 这两种在系统中会有多处使用,因此封装到
SessionSocketHolder工具类中。
java
package com.xk857.im.tcp.utils;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.xk857.im.common.constant.Constants;
import com.xk857.im.common.enums.ImConnectStatusEnum;
import com.xk857.im.common.model.UserClientDto;
import com.xk857.im.common.model.UserSession;
import com.xk857.im.tcp.redis.RedisManager;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.AttributeKey;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author cv大魔王
* @version 1.0
* @description 统一管理Socket链接的channel对象
* @date 2023/8/20
*/
public class SessionSocketHolder {
private static final Map<UserClientDto, NioSocketChannel> CHANNELS = new ConcurrentHashMap<>();
// ……
/** 用户主动退出,清空用户登录Session信息 */
public static void removeUserSession(NioSocketChannel channel) {
Integer clientType = (Integer) channel.attr(AttributeKey.valueOf(Constants.ClientType)).get();
// 1.删除channel连接对象中的Session信息
RMap<String, String> map = deleteUserSession(channel);
// 2.redis中删除session
map.remove(clientType);
channel.close();
}
/** 离线,清除channel连接信息,redis信息状态改为离线但不删除 */
public static void offlineUserSession(NioSocketChannel channel) {
Integer clientType = (Integer) channel.attr(AttributeKey.valueOf(Constants.ClientType)).get();
// 1.删除channel连接对象中的Session信息
RMap<String, String> map = deleteUserSession(channel);
// 2.从redis获取用户session信息,设置登录状态为离线
String sessionStr = map.get(clientType.toString());
if (StrUtil.isNotBlank(sessionStr)) {
UserSession userSession = JSONUtil.toBean(sessionStr, UserSession.class);
userSession.setConnectState(ImConnectStatusEnum.OFFLINE_STATUS.getCode());
map.put(clientType.toString(), JSONUtil.toJsonStr(userSession));
}
channel.close();
}
/** 仅删除channel中的用户信息,并返回redisson的RMap对象 */
private static RMap<String, String> deleteUserSession(NioSocketChannel channel) {
// 1.删除Session
String userId = (String) channel.attr(AttributeKey.valueOf(Constants.UserId)).get();
Integer appId = (Integer) channel.attr(AttributeKey.valueOf(Constants.AppId)).get();
Integer clientType = (Integer) channel.attr(AttributeKey.valueOf(Constants.ClientType)).get();
remove(appId, userId, clientType);
// 2.获取到该连接redis中的Session信息
RedissonClient redissonClient = RedisManager.getRedissonClient();
return redissonClient.getMap(String.format(Constants.RedisConstants.UserSessionConstants, appId, userId));
}
}那么NettyServerHandler退出逻辑可简写成直接调用工具类方法:
java
public class NettyServerHandler extends SimpleChannelInboundHandler<Message> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception {
Integer command = msg.getMessageHeader().getCommand();
if (command == SystemCommand.LOGIN.getCommand()) {
login(ctx, msg);
} else if (command == SystemCommand.LOGOUT.getCommand()) {
// 用户主动退出,清空用户登录Session信息
SessionSocketHolder.removeUserSession((NioSocketChannel) ctx.channel());
}
}