Java NIO(New I/O)是Java高性能网络编程的核心,而Netty是基于NIO的业界最流行的高性能网络应用框架——它解决了原生NIO的空轮询Bug、代码复杂、半包/粘包处理麻烦等痛点,被广泛应用于聊天室、RPC框架、游戏服务器等场景。

本文从零开始,先实现原生NIO版聊天室(理解NIO核心思想),再实现Netty优化版聊天室(解决原生NIO痛点,生产级可用),涵盖:群聊/私聊、用户上线/下线通知、编解码器解决半包/粘包、心跳检测、断线重连


一、原生NIO核心组件回顾

原生NIO的核心是Selector(选择器)、Channel(通道)、Buffer(缓冲区),三者配合实现非阻塞I/O

  1. Selector:一个线程可以监控多个Channel的事件(连接、读、写),避免了多线程的资源开销;
  2. Channel:双向通道,可以读也可以写,非阻塞;
  3. Buffer:缓冲区,数据的载体,Channel读写数据都要经过Buffer。

二、实战1:原生NIO版聊天室(理解核心思想)

我们先实现一个简化版原生NIO聊天室,功能:用户连接、群聊、用户上线/下线通知。

1. 项目结构

nio-chatroom
├── src/main/java/com/example/nio
│   ├── NioChatServer.java  # 服务端
│   └── NioChatClient.java  # 客户端
└── pom.xml

2. 原生NIO服务端(NioChatServer.java)

package com.example.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 原生NIO聊天室服务端
 */
public class NioChatServer {
    private static final int PORT = 8888;
    // 存储所有在线的Channel(ConcurrentHashMap保证线程安全)
    private static final ConcurrentHashMap<SocketChannel, String> onlineUsers = new ConcurrentHashMap<>();
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;

    public static void main(String[] args) {
        new NioChatServer().start();
    }

    public void start() {
        try {
            // 1. 打开Selector
            selector = Selector.open();
            // 2. 打开ServerSocketChannel
            serverSocketChannel = ServerSocketChannel.open();
            // 3. 绑定端口
            serverSocketChannel.bind(new InetSocketAddress(PORT));
            // 4. 设置为非阻塞模式
            serverSocketChannel.configureBlocking(false);
            // 5. 将ServerSocketChannel注册到Selector,监听ACCEPT事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("[NIO服务端] 启动成功,监听端口:" + PORT);

            // 6. 循环处理Selector上的事件
            while (true) {
                // 阻塞等待事件发生(原生NIO有空轮询Bug,Netty解决了这个问题)
                int readyChannels = selector.select();
                if (readyChannels == 0) continue;

                // 获取所有就绪的SelectionKey
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectedKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 处理完后必须移除,否则会重复处理
                    iterator.remove();

                    try {
                        // 处理ACCEPT事件(新连接)
                        if (key.isAcceptable()) {
                            handleAccept(key);
                        }
                        // 处理READ事件(读取消息)
                        else if (key.isReadable()) {
                            handleRead(key);
                        }
                    } catch (IOException e) {
                        // 客户端异常断开
                        handleDisconnect(key);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            try {
                if (selector != null) selector.close();
                if (serverSocketChannel != null) serverSocketChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 处理ACCEPT事件:接受新连接
     */
    private void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverChannel.accept();
        clientChannel.configureBlocking(false);
        // 注册到Selector,监听READ事件
        clientChannel.register(selector, SelectionKey.OP_READ);
        // 给新用户分配一个用户名(简化版:User+时间戳)
        String username = "User" + System.currentTimeMillis();
        onlineUsers.put(clientChannel, username);
        System.out.println("[新连接] 用户 " + username + " 上线了");
        // 广播用户上线通知
        broadcastMessage("[系统] 用户 " + username + " 上线了", null);
    }

    /**
     * 处理READ事件:读取客户端消息并广播
     */
    private void handleRead(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = clientChannel.read(buffer);
        if (bytesRead == -1) {
            // 客户端正常断开
            handleDisconnect(key);
            return;
        }
        // 切换Buffer为读模式
        buffer.flip();
        String message = StandardCharsets.UTF_8.decode(buffer).toString().trim();
        String username = onlineUsers.get(clientChannel);
        System.out.println("[收到消息] " + username + ": " + message);
        // 广播消息
        broadcastMessage(username + ": " + message, clientChannel);
    }

    /**
     * 处理客户端断开
     */
    private void handleDisconnect(SelectionKey key) {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        String username = onlineUsers.remove(clientChannel);
        if (username != null) {
            System.out.println("[断开连接] 用户 " + username + " 下线了");
            // 广播用户下线通知
            broadcastMessage("[系统] 用户 " + username + " 下线了", null);
        }
        // 取消SelectionKey
        key.cancel();
        try {
            clientChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 广播消息给所有在线用户
     * @param excludeChannel 排除的Channel(比如发送者自己)
     */
    private void broadcastMessage(String message, SocketChannel excludeChannel) {
        ByteBuffer buffer = StandardCharsets.UTF_8.encode(message + "\n");
        for (SocketChannel channel : onlineUsers.keySet()) {
            if (channel != excludeChannel && channel.isOpen()) {
                try {
                    channel.write(buffer);
                    // 重置Buffer的position,否则下一个Channel读不到数据
                    buffer.rewind();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3. 原生NIO客户端(NioChatClient.java)

package com.example.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * 原生NIO聊天室客户端
 */
public class NioChatClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 8888;
    private SocketChannel socketChannel;

    public static void main(String[] args) {
        new NioChatClient().start();
    }

    public void start() {
        try {
            // 1. 打开SocketChannel
            socketChannel = SocketChannel.open();
            // 2. 连接服务器
            socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
            socketChannel.configureBlocking(false);
            System.out.println("[NIO客户端] 已连接到服务器");

            // 3. 启动一个线程读取服务器消息
            new Thread(this::readServerMessages).start();

            // 4. 主线程读取用户输入并发送
            Scanner scanner = new Scanner(System.in);
            while (true) {
                String message = scanner.nextLine();
                if ("exit".equalsIgnoreCase(message)) {
                    break;
                }
                sendMessage(message);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (socketChannel != null) socketChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 读取服务器消息
     */
    private void readServerMessages() {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        try {
            while (socketChannel.isOpen()) {
                int bytesRead = socketChannel.read(buffer);
                if (bytesRead == -1) {
                    System.out.println("[客户端] 服务器断开连接");
                    break;
                }
                if (bytesRead > 0) {
                    buffer.flip();
                    String message = StandardCharsets.UTF_8.decode(buffer).toString().trim();
                    System.out.println(message);
                    buffer.clear();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 发送消息到服务器
     */
    private void sendMessage(String message) {
        try {
            ByteBuffer buffer = StandardCharsets.UTF_8.encode(message);
            socketChannel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4. 原生NIO的痛点(为什么要用Netty?)

  1. 空轮询Bug:Selector的select()方法在某些情况下会立即返回0,导致CPU 100%;
  2. 代码复杂:需要自己处理SelectionKey、Buffer的读写切换、异常断开;
  3. 半包/粘包问题:原生NIO没有自带编解码器,需要自己处理(比如消息长度前缀、分隔符);
  4. 线程模型复杂:需要自己设计线程模型,主从Reactor多线程模型实现起来很麻烦;
  5. Buffer难用:NIO的Buffer是固定大小的,读写切换麻烦,没有Netty的ByteBuf好用。

三、实战2:Netty优化版聊天室(生产级可用)

Netty是基于NIO的高性能网络应用框架,完美解决了原生NIO的痛点,我们用Netty实现一个功能完整的聊天室:群聊/私聊、用户上线/下线通知、编解码器解决半包/粘包、心跳检测、断线重连。

1. 引入依赖(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>netty-chatroom</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <netty.version>4.1.104.Final</netty.version>
    </properties>

    <dependencies>
        <!-- Netty核心 -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>${netty.version}</version>
        </dependency>
    </dependencies>
</project>

2. 项目结构

netty-chatroom
├── src/main/java/com/example/netty
│   ├── protocol          # 协议(消息类型)
│   ├── codec             # 编解码器
│   ├── handler           # ChannelHandler
│   ├── NettyChatServer.java  # 服务端
│   └── NettyChatClient.java  # 客户端
└── pom.xml

3. 协议设计(消息类型)

我们用简单的文本协议,消息格式:[消息类型]|[内容],比如:

  • LOGIN|User123:用户登录;
  • GROUP|大家好:群聊;
  • PRIVATE|User456|你好:私聊;
  • SYSTEM|用户User123上线了:系统通知;
  • HEARTBEAT|:心跳包。
package com.example.netty.protocol;

/**
 * 消息类型枚举
 */
public enum MessageType {
    LOGIN,      // 登录
    GROUP,      // 群聊
    PRIVATE,    // 私聊
    SYSTEM,     // 系统通知
    HEARTBEAT   // 心跳
}

/**
 * 消息实体
 */
public class ChatMessage {
    private MessageType type;
    private String from;    // 发送者
    private String to;      // 接收者(私聊用)
    private String content; // 内容

    public ChatMessage() {}

    public ChatMessage(MessageType type, String from, String to, String content) {
        this.type = type;
        this.from = from;
        this.to = to;
        this.content = content;
    }

    // Getter和Setter
    public MessageType getType() { return type; }
    public void setType(MessageType type) { this.type = type; }
    public String getFrom() { return from; }
    public void setFrom(String from) { this.from = from; }
    public String getTo() { return to; }
    public void setTo(String to) { this.to = to; }
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }

    // 序列化为字符串:[类型]|[发送者]|[接收者]|[内容]
    public String serialize() {
        return type.name() + "|" + (from == null ? "" : from) + "|" + (to == null ? "" : to) + "|" + (content == null ? "" : content);
    }

    // 反序列化
    public static ChatMessage deserialize(String str) {
        String[] parts = str.split("\\|", 4);
        ChatMessage message = new ChatMessage();
        message.setType(MessageType.valueOf(parts[0]));
        message.setFrom(parts[1].isEmpty() ? null : parts[1]);
        message.setTo(parts[2].isEmpty() ? null : parts[2]);
        message.setContent(parts[3].isEmpty() ? null : parts[3]);
        return message;
    }
}

4. 编解码器(解决半包/粘包)

Netty自带LineBasedFrameDecoder(基于换行符的帧解码器)和StringEncoder/StringDecoder,我们用换行符作为消息分隔符,解决半包/粘包问题。

5. Netty服务端(NettyChatServer.java)

package com.example.netty;

import com.example.netty.codec.ChatMessageDecoder;
import com.example.netty.codec.ChatMessageEncoder;
import com.example.netty.handler.ChatServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

/**
 * Netty聊天室服务端
 * 主从Reactor多线程模型:
 * - bossGroup:负责接受新连接
 * - workerGroup:负责处理连接的I/O事件
 */
public class NettyChatServer {
    private static final int PORT = 8888;

    public static void main(String[] args) {
        // 1. 创建bossGroup和workerGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup(1); // bossGroup只有1个线程
        EventLoopGroup workerGroup = new NioEventLoopGroup(); // workerGroup默认CPU核心数*2
        try {
            // 2. 创建ServerBootstrap
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // 指定Channel类型
                    .option(ChannelOption.SO_BACKLOG, 1024) // 服务端接受连接的队列大小
                    .childOption(ChannelOption.SO_KEEPALIVE, true) // 开启TCP Keep-Alive
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 1. 空闲状态检测:60秒没有读事件,触发IdleStateEvent
                            pipeline.addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS));
                            // 2. 基于换行符的帧解码器:解决半包/粘包
                            pipeline.addLast(new LineBasedFrameDecoder(1024 * 10));
                            // 3. String编解码器
                            pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
                            pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));
                            // 4. 自定义业务Handler
                            pipeline.addLast(new ChatServerHandler());
                        }
                    });

            // 3. 绑定端口,启动服务
            ChannelFuture future = bootstrap.bind(PORT).sync();
            System.out.println("[Netty服务端] 启动成功,监听端口:" + PORT);
            // 4. 等待服务端Channel关闭
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 5. 优雅关闭EventLoopGroup
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

6. 服务端业务Handler(ChatServerHandler.java)

package com.example.netty.handler;

import com.example.netty.protocol.ChatMessage;
import com.example.netty.protocol.MessageType;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 服务端业务Handler
 */
public class ChatServerHandler extends SimpleChannelInboundHandler<String> {
    // ChannelGroup:管理所有在线的Channel,方便广播
    private static final ChannelGroup onlineChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    // 存储用户名和Channel的映射
    private static final Map<String, Channel> userChannelMap = new ConcurrentHashMap<>();
    private static final Map<Channel, String> channelUserMap = new ConcurrentHashMap<>();

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        ChatMessage message = ChatMessage.deserialize(msg);
        switch (message.getType()) {
            case LOGIN:
                handleLogin(ctx, message);
                break;
            case GROUP:
                handleGroup(ctx, message);
                break;
            case PRIVATE:
                handlePrivate(ctx, message);
                break;
            case HEARTBEAT:
                // 心跳包,不做处理
                break;
        }
    }

    /**
     * 处理登录
     */
    private void handleLogin(ChannelHandlerContext ctx, ChatMessage message) {
        String username = message.getContent();
        if (userChannelMap.containsKey(username)) {
            // 用户名已存在
            ChatMessage errorMsg = new ChatMessage(MessageType.SYSTEM, "系统", null, "用户名已存在");
            ctx.writeAndFlush(errorMsg.serialize() + "\n");
            return;
        }
        // 登录成功
        Channel channel = ctx.channel();
        onlineChannels.add(channel);
        userChannelMap.put(username, channel);
        channelUserMap.put(channel, username);
        System.out.println("[登录] 用户 " + username + " 上线了");
        // 广播用户上线通知
        ChatMessage systemMsg = new ChatMessage(MessageType.SYSTEM, "系统", null, "用户 " + username + " 上线了");
        broadcastMessage(systemMsg, null);
    }

    /**
     * 处理群聊
     */
    private void handleGroup(ChannelHandlerContext ctx, ChatMessage message) {
        String username = channelUserMap.get(ctx.channel());
        if (username == null) return;
        message.setFrom(username);
        System.out.println("[群聊] " + username + ": " + message.getContent());
        broadcastMessage(message, ctx.channel());
    }

    /**
     * 处理私聊
     */
    private void handlePrivate(ChannelHandlerContext ctx, ChatMessage message) {
        String username = channelUserMap.get(ctx.channel());
        if (username == null) return;
        message.setFrom(username);
        Channel toChannel = userChannelMap.get(message.getTo());
        if (toChannel != null && toChannel.isActive()) {
            System.out.println("[私聊] " + username + " -> " + message.getTo() + ": " + message.getContent());
            toChannel.writeAndFlush(message.serialize() + "\n");
        } else {
            // 对方不在线
            ChatMessage errorMsg = new ChatMessage(MessageType.SYSTEM, "系统", null, "用户 " + message.getTo() + " 不在线");
            ctx.writeAndFlush(errorMsg.serialize() + "\n");
        }
    }

    /**
     * 广播消息
     */
    private void broadcastMessage(ChatMessage message, Channel excludeChannel) {
        String msgStr = message.serialize() + "\n";
        for (Channel channel : onlineChannels) {
            if (channel != excludeChannel && channel.isActive()) {
                channel.writeAndFlush(msgStr);
            }
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        // 客户端断开连接
        String username = channelUserMap.remove(ctx.channel());
        if (username != null) {
            userChannelMap.remove(username);
            onlineChannels.remove(ctx.channel());
            System.out.println("[断开] 用户 " + username + " 下线了");
            // 广播用户下线通知
            ChatMessage systemMsg = new ChatMessage(MessageType.SYSTEM, "系统", null, "用户 " + username + " 下线了");
            broadcastMessage(systemMsg, null);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

7. Netty客户端(NettyChatClient.java)

package com.example.netty;

import com.example.netty.handler.ChatClientHandler;
import com.example.netty.protocol.ChatMessage;
import com.example.netty.protocol.MessageType;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;

import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

/**
 * Netty聊天室客户端
 */
public class NettyChatClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 8888;
    private static Channel channel;
    private static String username;

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入用户名:");
        username = scanner.nextLine().trim();

        // 启动客户端
        new NettyChatClient().start(username);

        // 读取用户输入并发送
        while (true) {
            String input = scanner.nextLine().trim();
            if ("exit".equalsIgnoreCase(input)) {
                channel.close();
                break;
            }
            if (input.startsWith("@")) {
                // 私聊:@User456 你好
                int spaceIndex = input.indexOf(" ");
                if (spaceIndex != -1) {
                    String toUser = input.substring(1, spaceIndex);
                    String content = input.substring(spaceIndex + 1);
                    ChatMessage message = new ChatMessage(MessageType.PRIVATE, username, toUser, content);
                    sendMessage(message);
                }
            } else {
                // 群聊
                ChatMessage message = new ChatMessage(MessageType.GROUP, username, null, input);
                sendMessage(message);
            }
        }
    }

    public void start(String username) {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 1. 空闲状态检测:30秒没有写事件,发送心跳包
                            pipeline.addLast(new IdleStateHandler(0, 30, 0, TimeUnit.SECONDS));
                            // 2. 基于换行符的帧解码器
                            pipeline.addLast(new LineBasedFrameDecoder(1024 * 10));
                            // 3. String编解码器
                            pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
                            pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));
                            // 4. 自定义业务Handler
                            pipeline.addLast(new ChatClientHandler(username));
                        }
                    });

            // 连接服务器
            ChannelFuture future = bootstrap.connect(SERVER_HOST, SERVER_PORT).sync();
            channel = future.channel();
            System.out.println("[Netty客户端] 已连接到服务器");

            // 发送登录消息
            ChatMessage loginMsg = new ChatMessage(MessageType.LOGIN, null, null, username);
            sendMessage(loginMsg);

            // 等待客户端Channel关闭
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void sendMessage(ChatMessage message) {
        if (channel != null && channel.isActive()) {
            channel.writeAndFlush(message.serialize() + "\n");
        }
    }
}

8. 客户端业务Handler(ChatClientHandler.java)

package com.example.netty.handler;

import com.example.netty.NettyChatClient;
import com.example.netty.protocol.ChatMessage;
import com.example.netty.protocol.MessageType;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;

/**
 * 客户端业务Handler
 */
public class ChatClientHandler extends SimpleChannelInboundHandler<String> {
    private final String username;

    public ChatClientHandler(String username) {
        this.username = username;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        ChatMessage message = ChatMessage.deserialize(msg);
        switch (message.getType()) {
            case SYSTEM:
                System.out.println("[系统] " + message.getContent());
                break;
            case GROUP:
                System.out.println("[群聊] " + message.getFrom() + ": " + message.getContent());
                break;
            case PRIVATE:
                System.out.println("[私聊] " + message.getFrom() + ": " + message.getContent());
                break;
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.WRITER_IDLE) {
                // 30秒没有写事件,发送心跳包
                ChatMessage heartbeatMsg = new ChatMessage(MessageType.HEARTBEAT, null, null, null);
                ctx.writeAndFlush(heartbeatMsg.serialize() + "\n");
            }
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        System.out.println("[客户端] 与服务器断开连接,尝试重连...");
        // 断线重连:5秒后重连
        ctx.channel().eventLoop().schedule(() -> {
            new NettyChatClient().start(username);
        }, 5, TimeUnit.SECONDS);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

四、Netty的核心优化点

  1. 主从Reactor多线程模型:bossGroup负责接受新连接,workerGroup负责处理I/O事件,性能最优;
  2. ByteBuf:比NIO的Buffer好用,可读写、可扩容、引用计数复用;
  3. 编解码器:自带LineBasedFrameDecoderLengthFieldBasedFrameDecoder,解决半包/粘包问题;
  4. 心跳检测IdleStateHandler实现心跳检测,防止连接假死;
  5. 断线重连:客户端断线后自动重连;
  6. ChannelGroup:管理所有在线Channel,方便广播;
  7. 优雅关闭shutdownGracefully()优雅关闭EventLoopGroup,不丢失请求。

五、总结

本文从零开始,先实现了原生NIO版聊天室(理解NIO核心思想:Selector、Channel、Buffer),再实现了Netty优化版聊天室(解决原生NIO痛点,生产级可用),涵盖了:

  • 群聊/私聊;
  • 用户上线/下线通知;
  • 编解码器解决半包/粘包;
  • 心跳检测防止连接假死;
  • 断线重连;
  • 主从Reactor多线程模型。

Netty是Java高性能网络编程的事实标准,希望这篇文章能帮你深入理解Netty的核心思想和实战应用,有问题可以在评论区交流!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐