【Netty实战】基于Netty+WebSocket的IM通信后台服务代码详解

一、引言

二、技术选型与前提条件

三、核心代码实现

        服务的启动类 ChatServer

        初始化器类 WSServerInitializer

        心跳Handler类 HeartBeatHandler

        OkHttpUtil工具类

        json实体转换工具类 JsonUtils

        发送消息的类型/动作枚举类 MsgTypeEnum

        消息实体类 ChatMsg

        自定义通信Handler类 ChatHandler

        会话用户id和channel的关联处理类(确保线程安全) UserChannelSession

四、结语

五、其他参考文章


一、引言

在即时通讯(IM)系统架构中,通信层是核心基础设施,负责实现消息的实时、可靠传输。本文将详细介绍如何使用Netty框架结合WebSocket协议构建一个高性能、轻量级且专注通信功能的独立IM后台服务。该服务将作为整个IM系统的消息中枢,专注于解决以下核心问题:

实时消息传输:支持单聊/群聊的全双工通信

连接管理:用户多端连接配对管理

消息路由:实现点对点精准投递

协议适配:基于WebSocket的标准协议交互

设计边界
本文实现的Netty通信服务作为独立服务存在,其他服务的可以通过REST/RPC接口或OKHTTP等方式进行调用,但有些功能的实现会用TODO介绍思路,以下IM系统功能不在本文讨论范围:

用户关系管理(好友系统)、多媒体文件存储、社交功能(朋友圈、点赞等)、消息历史存储等等。

这种设计遵循单一职责原则,使通信服务可独立扩展并适配不同业务场景。

二、技术选型与前提条件

Netty:高性能异步事件驱动的网络应用框架

WebSocket:全双工通信协议,适合实时通讯场景

JDK版本:JDK11+ (在Java生态中,JDK 11和JDK 17作为LTS(Long-Term Support,长期支持)版本,相比JDK 8在NIO性能GC优化整体架构上有显著提升,这些改进对Netty+WebSocket这类高并发网络应用的性能影响尤为明显,具体可以通过AI询问进行了解)

pom.xml文件


    
    
        io.netty
        netty-all
        4.1.25.Final
    

    
    
        com.a3test.component
        idworker-snowflake
        1.0.0
    

三、核心代码实现

服务的启动类 ChatServer

/**
 * WebSocket 聊天服务器主启动类
 * 基于Netty框架实现的WebSocket服务器,用于处理实时聊天通信
 */
public class ChatServer {

    /**
     * 主方法 - 启动WebSocket服务器
     */
    public static void main(String[] args) throws Exception {

        // ==================== 线程组初始化 ====================
        // 主线程组(Boss线程组): 负责接收客户端连接
        // 类比:公司老板,只负责接单不处理具体业务
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        
        // 工作线程组(Worker线程组): 处理已建立连接的数据读写
        // 类比:公司员工,负责处理老板接来的具体业务
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // ==================== 服务器配置 ====================
            // 创建服务器启动引导类
            ServerBootstrap server = new ServerBootstrap();
            
            // 配置线程模型
            server.group(bossGroup, workerGroup)
                    // 指定通道类型为NIO模式
                    .channel(NioServerSocketChannel.class)
                    // 初始化通道处理器链(包含WebSocket协议处理器等)
                    .childHandler(new WSServerInitializer());

            // ==================== 端口绑定 ====================
            // 绑定监听端口875(sync()表示同步等待绑定操作完成)
            // 实际生产环境建议使用配置文件管理端口号
            ChannelFuture channelFuture = server.bind(875).sync();
            
            // WebSocket服务器已启动,监听端口: 875
            // 访问地址: ws://127.0.0.1:875/ws

            // ==================== 等待关闭 ====================
            // 阻塞直到服务器通道关闭(通常是人为关闭或发生异常)
            // 这行代码会让主线程持续运行,保持服务可用状态
            channelFuture.channel().closeFuture().sync();
            
        } finally {
            // ==================== 资源清理 ====================
            // 优雅关闭线程池(先平滑停止接收新请求,再处理完已接收请求)
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

初始化器类 WSServerInitializer

/**
 * WebSocket 通道初始化器
 * 负责配置新建立的SocketChannel的处理器链(Pipeline)
 * 注:每个客户端连接都会创建一个新的Channel和对应的Pipeline
 */
public class WSServerInitializer extends ChannelInitializer {

    /**
     * 初始化通道处理器链
     * @param channel 新建立的Socket通道
     * @throws Exception 可能抛出的异常
     */
    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
        // 获取通道的处理器管道(数据会依次按照addLast的顺序经过管道中的各个处理器)
        ChannelPipeline pipeline = channel.pipeline();

        /* ==================== HTTP协议相关处理器 ==================== */
        
        // HTTP编解码器(组合了HttpRequestDecoder和HttpResponseEncoder)
        // 作用:将字节流解码为HTTP请求/将HTTP响应编码为字节流
        // 注意:WebSocket握手阶段使用HTTP协议,因此需要此处理器
        pipeline.addLast(new HttpServerCodec());

        // 大数据块写入处理器
        // 作用:支持异步写入大型数据流,防止内存溢出
        // 典型应用场景:文件传输、大文件下载等
        pipeline.addLast(new ChunkedWriteHandler());

        // HTTP消息聚合器(最大聚合64KB内容)
        // 作用:将多个HTTP消息片段聚合成完整的FullHttpRequest/FullHttpResponse
        // 注意:设置过大会增加内存消耗,过小可能导致大请求被拒绝
        pipeline.addLast(new HttpObjectAggregator(1024 * 64));

        /* ==================== 心跳检测机制 ==================== */
        
        // 空闲状态检测处理器(参数单位:秒)
        // 参数说明:读空闲(8s), 写空闲(10s), 全部空闲(300*60s=5小时)
        // 工作原理:超过指定时间未对应操作会触发IdleStateEvent事件
        pipeline.addLast(new IdleStateHandler(8, 10, 300 * 60));
        
        // 自定义心跳处理器(需自行实现HeartBeatHandler)
        // 功能:处理IdleStateEvent事件,可能包括:
        // 1. 发送心跳请求(写空闲时)
        // 2. 断开不活跃连接(读空闲超时)
        pipeline.addLast(new HeartBeatHandler());

        /* ==================== WebSocket协议处理器 ==================== */
        
        // WebSocket协议处理器(指定访问路径为/ws)
        // 核心功能:
        // 1. 处理WebSocket握手(Upgrade握手)
        // 2. 处理控制帧(Close/Ping/Pong)
        // 3. 维护WebSocket协议状态
        // 注意:实际业务消息会以二进制帧或文本帧的形式传递
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

        /* *************** 可选增强配置 *************** */
        // 1. 可添加SSL处理器保证安全通信:
        // pipeline.addFirst("ssl", new SslHandler(sslContext.newEngine(channel.alloc())));
        // 2. 可添加 自定义 IP过滤/黑名单处理器
        // 3. 可添加 自定义 流量整形处理器控制速率

       
        // 自定义业务处理器(需自行实现ChatHandler)
        // 功能:处理实际的业务消息,例如:
        // 1. 文本消息解析与转发
        // 2. 二进制消息处理(如图片/语音)
        // 3. 用户上下线管理
        pipeline.addLast(new ChatHandler());
        
        
    }
}

心跳Handler类 HeartBeatHandler

/**
 * 心跳检测处理器
 * 用于处理网络连接的空闲状态检测,自动关闭不活跃的连接以节省资源
 * 
 * 工作原理:
 * 1. 由IdleStateHandler触发空闲事件
 * 2. 根据不同的空闲状态采取相应措施
 * 3. 默认会继续传播事件(除非显式拦截)
 */
@ChannelHandler.Sharable  // 标记为可共享处理器(无状态时可安全复用)
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {

    /**
     * 处理用户事件(由IdleStateHandler触发)
     * @param ctx 通道上下文
     * @param evt 事件对象
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 仅处理空闲状态事件
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            
            switch (event.state()) {
                case READER_IDLE:
                    // 读空闲(客户端长时间未发送数据)
                    // 注意:这里只是示例,实际需要根据业务决定是否处理
                    System.out.println("通道[" + ctx.channel().id() + "]进入读空闲...");
                    break;
                    
                case WRITER_IDLE:
                    // 写空闲(服务器长时间未发送数据)
                    System.out.println("通道[" + ctx.channel().id() + "]进入写空闲...");
                    break;
                    
                case ALL_IDLE:
                    // 读写都空闲(连接完全不活跃)
                    // 记录关闭前的连接数(调试用)
                    System.out.println("channel关闭前,clients数量:" + ChatHandler.clients.size());
                    
                    // 通过Context异步关闭不活跃连接,并添加监听器
                    ctx.channel().close().addListener(future -> {
                        if (future.isSuccess()) {
                            // 可添加关闭成功后的逻辑
                            System.out.println("channel关闭后,clients数量:" + ChatHandler.clients.size());
                        }
                    });
                    break;
            }
        } 
    }

    /**
     * 异常处理
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 记录心跳检测过程中的异常
        cause.printStackTrace();
        ctx.close();
    }
}

OkHttpUtil工具类

/**
 * OkHttp 工具类
 * 提供基于 OkHttp 的 HTTP 请求简化操作
 *
 * 主要功能:
 *   1. 封装 GET 请求基础操作
 *   2. 自动处理响应结果转换
 * 
 */
public class OkHttpUtil {
    
    // 静态常量:单例 OkHttpClient 实例
    private static final OkHttpClient CLIENT = new OkHttpClient();
    
    /**
     * 执行 GET 请求(与原方法完全兼容)
     * @param url 请求地址
     * @return Result 或 null(失败时)
     */
    public static Result get(String url) {
        try {
            // 使用单例 CLIENT 替代新建对象
            Request request = new Request.Builder()
                    .get()
                    .url(url)
                    .build();
            
            Response response = CLIENT.newCall(request).execute();
            String res = response.body().string();
            return JsonUtils.jsonToPojo(res, Result.class);
            
        } catch (Exception e) {
            System.out.println("OkHttp get failed:", e);
            return null;
        }
    }
}

json实体转换工具类 JsonUtils

/**
 * 
 * @Title: JsonUtils.java
 * @Package com.imooc.utils
 * @Description: json转换类
 * Copyright: Copyright (c)
 * Company: www.imooc.com
 * 
 */
public class JsonUtils {

    // 定义jackson对象
    private static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * 将对象转换成json字符串。
     * @param data
     * @return
     */
    public static String objectToJson(Object data) {
    	try {
			String string = MAPPER.writeValueAsString(data);
			return string;
		} catch (JsonProcessingException e) {
			e.printStackTrace();
		}
    	return null;
    }
    
    /**
     * 将json结果集转化为对象
     * 
     * @param jsonData json数据
     * @param beanType 对象中的object类型
     * @return
     */
    public static  T jsonToPojo(String jsonData, Class beanType) {
        try {
            T t = MAPPER.readValue(jsonData, beanType);
            return t;
        } catch (Exception e) {
        	e.printStackTrace();
        }
        return null;
    }
    
    /**
     * 将json数据转换成pojo对象list
     * @param jsonData
     * @param beanType
     * @return
     */
    public static List jsonToList(String jsonData, Class beanType) {
    	JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
    	try {
    		List list = MAPPER.readValue(jsonData, javaType);
    		return list;
		} catch (Exception e) {
			e.printStackTrace();
		}
    	
    	return null;
    }

}

发送消息的类型/动作枚举类 MsgTypeEnum

/**
 * 消息类型枚举
 * 定义系统中不同类型的消息常量
 */
public enum MsgTypeEnum {
	
	// 枚举实例定义
	CONNECT_INIT(0, "第一次(或重连)初始化连接"),  // 连接初始化消息,用于建立或重新建立连接
	WORDS(1, "文字表情消息"),                  // 普通文本或表情消息
	IMAGE(2, "图片"),                         // 图片消息
	VOICE(3, "语音"),                         // 语音消息
	VIDEO(4, "视频"),                         // 视频消息
	
	// 枚举字段
	public final Integer type;       // 消息类型编码(数字形式)
	public final String content;     // 消息类型描述
	
	/**
	 * 枚举构造函数
	 * @param type 消息类型编码
	 * @param content 类型描述
	 */
	MsgTypeEnum(Integer type, String content){
		this.type = type;
		this.content = content;
	}
	
	/**
	 * 获取消息类型编码
	 * @return 类型编码(Integer)
	 */
	public Integer getType() {
		return type;
	}
	
}

消息实体类 ChatMsg

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class ChatMsg {

	private String senderId;				// 发送者的用户id
	private String receiverId;				// 接受者的用户id
	private String msg;						// 聊天内容
	private Integer msgType;				// 消息类型,见枚举 MsgTypeEnum.java
	private String msgId;                   // 消息主键id

    @JsonDeserialize(using = LocalDateTimeDeserializer.class)  // JSON反序列化
    @JsonSerialize(using = LocalDateTimeSerializer.class)      // JSON序列化
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") // 日期格式
	private LocalDateTime chatTime;			// 消息的聊天时间,既是发送者的发送时间、又是接受者的接受时间
	private Integer showMsgDateTimeFlag;	// 标记存储数据库,用于历史展示。每超过1分钟,则显示聊天时间,前端可以控制时间长短

	private String videoPath;				// 视频地址
	private Integer videoWidth;				// 视频宽度
	private Integer videoHeight;			// 视频高度
	private Integer videoTimes;				// 视频时间

	private String voicePath;				// 语音地址
	private Integer speakVoiceDuration;		// 语音时长
	private Boolean isRead;					// 语音消息标记是否已读未读,true: 已读,false: 未读

	private Integer communicationType;		// 聊天类型, 1:单聊,2:群聊

	private Boolean isReceiverOnLine;		// 用于标记当前接受者是否在线
}

自定义通信Handler类 ChatHandler

/**
 * WebSocket消息处理器
 * 功能:处理实时聊天消息,包括文本、图片、语音、视频等多种消息类型
 * 核心职责:
 *   1. 管理所有客户端连接
 *   2. 处理消息路由(单聊/群聊)
 *   3. 维护用户-通道的映射关系
 *   4. 处理连接生命周期事件
 *
 * @Auther yang
 */
// 继承SimpleChannelInboundHandler专门处理TextWebSocketFrame类型消息
public class ChatHandler extends SimpleChannelInboundHandler {

    /**
     * 全局Channel组,管理所有活跃连接
     * 特点:
     *   - 使用Netty提供的DefaultChannelGroup
     *   - 采用全局事件执行器管理生命周期
     */
    public static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    // 在类初始化时创建单例保证线程安全 snowflake主键id生成
    private static final Snowflake ID_GENERATOR = new Snowflake(new IdWorkerConfigBean());

    /**
     * 处理收到的WebSocket文本帧
     * @param ctx 通道上下文
     * @param msg 文本帧对象(包含消息内容)
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        // 原始消息内容输出(调试用)
        String content = msg.text();
        System.out.println("接受到的数据:" + content);

        // 消息解析流程
        DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class);
        ChatMsg chatMsg = dataContent.getChatMsg();

        // 提取消息要素
        String msgText = chatMsg.getMsg();
        String receiverId = chatMsg.getReceiverId();
        String senderId = chatMsg.getSenderId();

        // TODO: 判断双方是否黑名单拉黑 start
        // 如果双方只要有一方是黑名单,则终止发送(通过OKHTTP调用其他服务)
        Result result = OkHttpUtil.get("http://127.0.0.1:8888/isBlack?friendId1st=" + receiverId
                                                                + "&friendId2nd=" + senderId);
        boolean isBlack = (Boolean)result.getData();
        System.out.println("当前的黑名单关系为: " + isBlack);
        if (isBlack) {
            return;
        }
        // 判断双方是否黑名单拉黑 end

        // 统一使用服务器时间
        chatMsg.setChatTime(LocalDateTime.now());
        Integer msgType = chatMsg.getMsgType();

        // 获取当前通道信息
        Channel currentChannel = ctx.channel();
        String currentChannelId = currentChannel.id().asLongText();
        String currentChannelIdShort = currentChannel.id().asShortText();

        // System.out.println("客户端currentChannelId:" + currentChannelId);
        // System.out.println("客户端currentChannelIdShort:" + currentChannelIdShort);

        // 消息类型路由,判断消息类型,根据不同的类型来处理不同的业务
        if (msgType == MsgTypeEnum.CONNECT_INIT.type) {
            /* 连接初始化处理,当websocket初次open的时候,初始化channel,把channel和用户userid管来起来 */
            UserChannelSession.putMultiChannels(senderId, currentChannel);
            
        } else if (msgType == MsgTypeEnum.WORDS.type
                || msgType == MsgTypeEnum.IMAGE.type
                || msgType == MsgTypeEnum.VIDEO.type
                || msgType == MsgTypeEnum.VOICE.type) {
            
            /* 业务消息处理 */
            // 生成消息ID(保证分布式唯一性)
            // TODO: 此处如果用mq异步解耦,保存信息到数据库,数据库无法获得信息的主键id(无法在此处chat-server获得消息主键id)
            // 所以此处可以用snowflake直接生成唯一的主键id传到业务服务直接新增
            String sid = ID_GENERATOR.nextId();
            System.out.println("sid = " + sid);

            chatMsg.setMsgId(sid);

            // 接收者通道查询
            List receiverChannels = UserChannelSession.getMultiChannels(receiverId);
            if (receiverChannels == null || receiverChannels.isEmpty()) {
                // 接收方离线处理
                chatMsg.setIsReceiverOnLine(false);
            } else {
                // 接收方在线处理
                chatMsg.setIsReceiverOnLine(true);
                
                // 多设备消息投递
                for (Channel c : receiverChannels) {
                    Channel findChannel = clients.find(c.id());
                    if (findChannel != null) {
                        // 语音消息特殊标记
                        if (msgType == MsgTypeEnum.VOICE.type) {
                            chatMsg.setIsRead(false);
                        }

                        // 消息封装与发送
                        dataContent.setChatMsg(chatMsg);
                        String chatTimeFormat = LocalDateUtils.format(
                                chatMsg.getChatTime(),
                                LocalDateUtils.DATETIME_PATTERN_2);
                        dataContent.setChatTime(chatTimeFormat);
                        // 发送消息给在线的用户
                        findChannel.writeAndFlush(
                            new TextWebSocketFrame(JsonUtils.objectToJson(dataContent)));
                    }
                }
            }
            
            // TODO: 消息持久化到数据库(通过MQ异步处理或者其他方式)
        }

        /* 发送方多设备同步(如PC与手机双端) */
        List myOtherChannels = UserChannelSession.getMyOtherChannels(
                senderId, currentChannelId);
        for (Channel c : myOtherChannels) {
            Channel findChannel = clients.find(c.id());
            if (findChannel != null) {
                dataContent.setChatMsg(chatMsg);
                String chatTimeFormat = LocalDateUtils.format(
                        chatMsg.getChatTime(),
                        LocalDateUtils.DATETIME_PATTERN_2);
                dataContent.setChatTime(chatTimeFormat);
                
                findChannel.writeAndFlush(
                    new TextWebSocketFrame(JsonUtils.objectToJson(dataContent)));
            }
        }

        // 调试输出当前会话状态
        UserChannelSession.outputMulti();
    }

    /**
     * 新连接建立时触发
     * 功能:将新通道加入全局管理
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel currentChannel = ctx.channel();
        String currentChannelId = currentChannel.id().asLongText();
        System.out.println("客户端建立连接,channel对应的长id为:" + currentChannelId);

        // 获得客户端的channel,并且存入到ChannelGroup中进行管理(作为一个客户端群组)
        clients.add(currentChannel);
    }

    /**
     * 连接关闭时触发
     * 功能:清理通道相关资源
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel currentChannel = ctx.channel();
        String currentChannelId = currentChannel.id().asLongText();
        System.out.println("客户端关闭连接,channel对应的长id为:" + currentChannelId);

        // 清理用户-通道映射
        String userId = UserChannelSession.getUserIdByChannelId(currentChannelId);
        UserChannelSession.removeUselessChannels(userId, currentChannelId);

        // 从全局组移除
        clients.remove(currentChannel);
    }

    /**
     * 异常处理
     * 功能:安全关闭异常连接
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        Channel currentChannel = ctx.channel();
        String currentChannelId = currentChannel.id().asLongText();
        System.out.println("发生异常捕获,channel对应的长id为:" + currentChannelId);

        // 关闭异常连接(关闭channel)
        ctx.channel().close();
        // 随后从ChannelGroup中移除对应的channel
        clients.remove(currentChannel);

        // 清理用户会话
        String userId = UserChannelSession.getUserIdByChannelId(currentChannelId);
        UserChannelSession.removeUselessChannels(userId, currentChannelId);
    }
}

会话用户id和channel的关联处理类(确保线程安全) UserChannelSession

/**
 * 用户会话管理器(线程安全版)
 * 核心功能:
 *   1. 维护用户ID与Channel的多对多关系(支持多端登录)
 *   2. 提供通道查找和会话管理能力
 *   3. 支持设备级消息路由控制
 * 
 * 线程安全实现:
 *   1. 使用ConcurrentHashMap保证基础结构的线程安全
 *   2. 使用CopyOnWriteArrayList保证Channel列表的线程安全
 *   3. 所有写操作都通过原子方法实现
 */
public class UserChannelSession {

    /**
     * 多设备会话映射表(线程安全实现)
     * Key: 用户ID
     * Value: 该用户所有活跃的Channel列表(CopyOnWriteArrayList保证线程安全)
     */
    private static final ConcurrentMap> multiSession = 
        new ConcurrentHashMap<>();

    /**
     * Channel与用户ID的逆向映射(线程安全实现)
     * Key: Channel长ID(asLongText)
     * Value: 绑定的用户ID
     */
    private static final ConcurrentMap userChannelIdRelation = 
        new ConcurrentHashMap<>();

    /**
     * 添加用户-Channel关联关系(原子操作)
     * @param channelId Channel的长ID(ctx.channel().id().asLongText())
     * @param userId 绑定的用户ID
     */
    public static void putUserChannelIdRelation(String channelId, String userId) {
        userChannelIdRelation.put(channelId, userId);
    }

    /**
     * 通过ChannelID查找用户ID(线程安全)
     * @param channelId Channel的长ID
     * @return 关联的用户ID,未找到返回null
     */
    public static String getUserIdByChannelId(String channelId) {
        return userChannelIdRelation.get(channelId);
    }

    /**
     * 添加用户与设备Channel关系(原子操作)
     * @param userId 用户ID
     * @param channel 新增的Channel对象
     */
    public static void putMultiChannels(String userId, Channel channel) {
        multiSession.compute(userId, (k, v) -> {
            if (v == null) v = new CopyOnWriteArrayList<>();
            v.addIfAbsent(channel); // 避免重复添加
            return v;
        });
        
        // 添加用户-ChannelId关联关系
        putUserChannelIdRelation(channel.id().asLongText(), userId); 
    }

    /**
     * 获取用户的所有活跃Channel(线程安全快照)
     * @param userId 用户ID
     * @return Channel列表(可能为null)
     */
    public static List getMultiChannels(String userId) {
        return multiSession.get(userId);
    }

    /**
     * 移除指定用户的无效Channel(原子操作)
     * @param userId 用户ID
     * @param channelId 需要移除的Channel长ID
     */
    public static void removeUselessChannels(String userId, String channelId) {
        multiSession.computeIfPresent(userId, (k, v) -> {
            v.removeIf(c -> c.id().asLongText().equals(channelId));
            return v.isEmpty() ? null : v; // 自动清理空列表
        });
        userChannelIdRelation.remove(channelId);
    }

    /**
     * 获取用户其他设备的Channel(线程安全快照)
     * @param userId 用户ID
     * @param channelId 需要排除的Channel长ID
     * @return 其他设备的Channel列表(可能为null)
     */
    public static List getMyOtherChannels(String userId, String channelId) {
        CopyOnWriteArrayList channels = multiSession.get(userId);
        if (channels == null || channels.isEmpty()) {
            return null;
        }
        return channels.stream()
                .filter(c -> !c.id().asLongText().equals(channelId))
                .collect(Collectors.toList());
    }

    /**
     * 打印当前所有会话状态(线程安全快照)
     * 注意:输出期间可能有数据更新,但不影响输出一致性
     */
    public static void outputMulti() {
        System.out.println("++++++++++++++++++");
        multiSession.forEach((userId, channels) -> {
            System.out.println("----------");
            System.out.println("UserId: " + userId);
            channels.forEach(c -> 
                System.out.println("\t\t ChannelId: " + c.id().asLongText())
            );
            System.out.println("----------");
        });
        System.out.println("++++++++++++++++++");
    }
}

四、结语

本文实现的Netty+WebSocket IM通信服务具有以下特点:

高性能:通过Netty的零拷贝、内存池等技术优化

可扩展:支持水平扩展的集群架构

可观测:完善的监控指标和日志记录

轻量级:专注通信核心功能,代码量<2000行

后续演进方向

添加消息可靠性保障(ACK机制)

实现黑名单处理器、流量限制处理器

SSL处理器保证安全通信

这个实现可作为企业IM系统的基础通信组件,通过REST/RPC接口或OKHTTP与上层或其他业务系统交互,保持架构的清晰边界,如果你有什么其他问题或者改进建议欢迎评论区留言讨论!

五、其他参考文章

【Netty框架全解析】:从基础概念到高阶实践,从零玩转高性能网络框架!-CSDN博客

【Netty+WebSocket详解】WebSocket全双工通信与Netty的高效结合与实战-CSDN博客

【Spring WebSocket详解】Spring WebSocket从入门到实战-CSDN博客

你可能感兴趣的:(【Netty实战】基于Netty+WebSocket的IM通信后台服务代码详解)