Netty IM案例

wen java案例 2

本文目录导读:

Netty IM案例

  1. 系统架构设计
  2. 自定义通信协议设计
  3. 服务端核心代码(im-server)
  4. 客户端实现(im-client)
  5. 测试与运行
  6. 进阶与优化方向
  7. 技术要点总结

下面为你提供一个基于 Netty 的即时通讯(IM)核心案例设计与代码实现,这个案例会涵盖客户端登录、单聊、群聊、心跳检测以及离线消息等核心功能。


系统架构设计

技术栈

  • Netty 4.x — 网络通信框架
  • Protobuf / JSON — 自定义协议(这里使用JSON简单演示,生产建议Protobuf)
  • Redis — 用户路由信息存储(保证分布式集群状态一致性)
  • MySQL + MyBatis — 好友关系与离线消息存储

核心模块划分

├── im-common          // 公共模块:协议、常量、工具类
├── im-server          // IM服务端
├── im-client          // IM客户端(用于测试)
└── im-admin           // 管理后台(可选)

自定义通信协议设计

由于Netty提供TCP传输,我们需要自定义消息边界,这里采用 长度域 + JSON体 方式:

// 消息结构:包体长度(4字节) + 消息类型(1字节) + 消息体(JSON)

协议定义(im-common模块)

public enum MsgType {
    LOGIN_REQ(1),       // 登录请求
    LOGIN_RESP(2),      // 登录响应
    CHAT_MSG(3),        // 聊天消息
    PING(4),            // 心跳
    PONG(5);            // 心跳响应
    public final int code;
    MsgType(int code) { this.code = code; }
    public static MsgType fromCode(int code) {
        for (MsgType type : values()) {
            if (type.code == code) return type;
        }
        return null;
    }
}
public class Message {
    private MsgType type;       // 消息类型
    private String body;        // JSON body
    private long timestamp;     // 客户端消息时间戳
    // getter/setter...
}

服务端核心代码(im-server)

Netty服务端启动器

public class IMServer {
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;
    private Channel serverChannel;
    public void start(int port) throws InterruptedException {
        bossGroup = new NioEventLoopGroup(1);
        workerGroup = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup)
                 .channel(NioServerSocketChannel.class)
                 .option(ChannelOption.SO_BACKLOG, 1024)
                 .childOption(ChannelOption.SO_KEEPALIVE, true)
                 .childOption(ChannelOption.TCP_NODELAY, true)
                 .childHandler(new ChannelInitializer<SocketChannel>() {
                     @Override
                     protected void initChannel(SocketChannel ch) {
                         ChannelPipeline pipeline = ch.pipeline();
                         // 拆包解包(处理半包/粘包)
                         pipeline.addLast(new LengthFieldBasedFrameDecoder(
                                 1024*1024, 0, 4, 1, 0));
                         pipeline.addLast(new JsonDecoder());
                         // 写数据处理
                         pipeline.addLast(new JsonEncoder());
                         // 业务逻辑
                         pipeline.addLast(new IMServerHandler());
                         // **空闲检测:5分钟内无读则关闭**
                         pipeline.addLast(new IdleStateHandler(300, 0, 0));
                         pipeline.addLast(new HeartbeatHandler());
                     }
                 });
        serverChannel = bootstrap.bind(port).sync().channel();
        System.out.println("IM Server started on port " + port);
    }
    public void shutdown() {
        if (serverChannel != null) serverChannel.close();
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
}

自定义编解码器

// JSON编码器
public class JsonEncoder extends MessageToByteEncoder<Message> {
    private static final ObjectMapper mapper = new ObjectMapper();
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) {
        try {
            byte[] data = mapper.writeValueAsBytes(msg);
            out.writeInt(data.length);      // 长度
            out.writeByte(msg.getType().code); // 类型
            out.writeBytes(data);           // 数据
        } catch (JsonProcessingException e) {
            ctx.close();
        }
    }
}
// JSON解码器
public class JsonDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        // 因为前面已经使用LengthFieldBasedFrameDecoder拆包,这里直接解析
        int len = in.readInt();
        byte type = in.readByte();
        byte[] body = new byte[len];
        in.readBytes(body);
        Message msg = new Message();
        msg.setType(MsgType.fromCode(type));
        msg.setBody(new String(body, StandardCharsets.UTF_8));
        out.add(msg);
    }
}

核心业务处理器

public class IMServerHandler extends SimpleChannelInboundHandler<Message> {
    // 模拟在线用户表:userId -> Channel
    public static ConcurrentHashMap<Long, Channel> onlineUsers = new ConcurrentHashMap<>();
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        // 连接建立(尚未登录,此时不绑定用户)
        System.out.println("新连接: " + ctx.channel().remoteAddress());
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Message msg) {
        switch (msg.getType()) {
            case LOGIN_REQ:
                handleLogin(ctx, msg);
                break;
            case CHAT_MSG:
                handleChat(ctx, msg);
                break;
            case PING:
                // 响应PONG(心跳处理)
                Message pong = new Message();
                pong.setType(MsgType.PONG);
                ctx.writeAndFlush(pong);
                break;
            default:
                ctx.close();
        }
    }
    private void handleLogin(ChannelHandlerContext ctx, Message msg) {
        // 解析登录请求
        LoginRequest req = JSON.parseObject(msg.getBody(), LoginRequest.class);
        // 模拟认证成功
        // 1. 绑定用户ID与Channel
        Long userId = req.getUserId();
        onlineUsers.put(userId, ctx.channel());
        // 2. 返回登录响应
        Message resp = new Message();
        resp.setType(MsgType.LOGIN_RESP);
        resp.setBody("{\"code\":0,\"msg\":\"登录成功\"}");
        ctx.writeAndFlush(resp);
        System.out.println("用户 " + userId + " 登录成功");
    }
    private void handleChat(ChannelHandlerContext ctx, Message msg) {
        ChatRequest req = JSON.parseObject(msg.getBody(), ChatRequest.class);
        Long targetUserId = req.getTargetUserId();
        // 单聊
        if (req.getType() == 1) {
            Channel targetChannel = onlineUsers.get(targetUserId);
            if (targetChannel != null) {
                // 构建发给目标的消息(添加发送者信息)
                Message forward = new Message();
                forward.setType(MsgType.CHAT_MSG);
                forward.setBody(JSON.toJSONString(new ChatRequest(
                        req.getFromUserId(), req.getTargetUserId(), req.getContent())));
                targetChannel.writeAndFlush(forward);
            } else {
                // 用户不在线,存离线消息(可存入Redis/MySQL)
                saveOfflineMessage(req);
            }
        }
        // 群聊逻辑类似:遍历群成员,逐个发送
    }
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        // 移除在线用户
        onlineUsers.entrySet().removeIf(entry -> entry.getValue() == ctx.channel());
        System.out.println("连接关闭");
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

心跳与超时处理器

public class HeartbeatHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IddStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                // 5分钟没收到消息,判定死亡连接
                System.out.println("心跳超时,关闭连接: " + ctx.channel().remoteAddress());
                ctx.close();
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
}

客户端实现(im-client)

public class IMClient {
    private Channel channel;
    private Long userId;
    public void connect(String host, int port) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                 .channel(NioSocketChannel.class)
                 .handler(new ChannelInitializer<SocketChannel>() {
                     @Override
                     protected void initChannel(SocketChannel ch) {
                         ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024*1024, 0, 4, 1, 0));
                         ch.pipeline().addLast(new JsonDecoder());
                         ch.pipeline().addLast(new JsonEncoder());
                         ch.pipeline().addLast(new SimpleChannelInboundHandler<Message>() {
                             @Override
                             protected void channelRead0(ChannelHandlerContext, Message msg) {
                                 // 处理服务端推送的消息
                                 if (msg.getType() == MsgType.CHAT_MSG) {
                                     ChatRequest req = JSON.parseObject(msg.getBody(), ChatRequest.class);
                                     System.out.println("收到来自用户[" + req.getFromUserId() + "]: " + req.getContent());
                                 }
                             }
                         });
                     }
                 });
        channel = bootstrap.connect(host, port).sync().channel();
    }
    public void sendLogin(Long userId) {
        this.userId = userId;
        Message msg = new Message();
        msg.setType(MsgType.LOGIN_REQ);
        msg.setBody("{\"userId\":" + userId + "}");
        channel.writeAndFlush(msg);
    }
    public void sendChat(Long targetUserId, String content) {
        Message msg = new Message();
        msg.setType(MsgType.CHAT_MSG);
        msg.setBody(JSON.toJSONString(new ChatRequest(userId, targetUserId, content)));
        channel.writeAndFlush(msg);
    }
}

测试与运行

主测试类(两个客户端模拟单聊)

public class TestMain {
    public static void main(String[] args) throws Exception {
        // 启动服务端
        new Thread(() -> {
            try {
                new IMServer().start(8080);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        Thread.sleep(1000);
        // 客户端A登录
        IMClient clientA = new IMClient();
        clientA.connect("localhost", 8080);
        clientA.sendLogin(1001L);
        // 客户端B登录
        IMClient clientB = new IMClient();
        clientB.connect("localhost", 8080);
        clientB.sendLogin(1002L);
        Thread.sleep(500);
        // A给B发送消息
        clientA.sendChat(1002L, "你好,B!");
        Thread.sleep(2000);
    }
}

进阶与优化方向

  1. 协议优化:使用 Protobuf 代替JSON,性能提升数倍。
  2. 集群部署:接入 ZooKeeper 管理服务节点,通过 Redis Pub/Sub 实现跨节点消息转发。
  3. 消息可靠性:增加ACK确认机制,失败后自动重发。
  4. 离线消息:用户登录后拉取Redis/MySQL中的离线消息。
  5. 流量控制:结合Netty的高水位(WriteBufferWaterMark)实现背压。
  6. 安全:TLS/SSL加密,防止抓包。

技术要点总结

功能 实现方案
粘包/拆包 LengthFieldBasedFrameDecoder 长度域拆包
心跳检测 IdleStateHandler 定期检测读空闲
在线管理 ConcurrentHashMap<userId, Channel>
多端登录 使用Redis记录最新设备,踢出旧设备
消息有序 引入消息序号,配合Redis队列
性能调优 使用 NioEventLoopGroup,合理配置线程数

这个案例涵盖了Netty在IM系统中的核心流程常见解决方案,你可以基于此框架扩展到更复杂的业务场景,群管理、朋友圈、文件传输等功能,核心在于处理好协议定义、连接管理和消息路由三大部分。

抱歉,评论功能暂时关闭!