Skip to content
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删除的逻辑是:

  1. 使用filter找到value等于当前channel的key
  2. 再根据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());
        }
    }