本文用最通俗的语言,最完整的案例,带你从零掌握高并发网络编程核心。每一行代码都有详细注释,每一个概念都用生活化比喻解释。

引言:为什么传统IO会让我们头疼?

想象一下你开了一家只有3个柜台的银行(服务器),采用传统的一对一服务模式(BIO):

// 这就是传统BIO(Blocking IO)的工作方式
public class BioBank {
    public static void main(String[] args) throws IOException {
        // 银行开门(服务器启动)
        ServerSocket bank = new ServerSocket(8888);
        System.out.println("银行开门营业,3个柜台...");
        
        while (true) {
            // 顾客来了(客户端连接)
            Socket customer = bank.accept(); // 这里会阻塞!没有顾客就干等着
            System.out.println("新顾客来了,分配一个柜台...");
            
            // 每个顾客分配一个专门的柜员(线程)
            new Thread(() -> {
                try {
                    // 柜员开始服务(处理业务)
                    serveCustomer(customer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    
    static void serveCustomer(Socket customer) throws IOException {
        // 模拟业务办理(读取请求,处理,返回响应)
        BufferedReader in = new BufferedReader(new InputStreamReader(customer.getInputStream()));
        PrintWriter out = new PrintWriter(customer.getOutputStream(), true);
        
        String request;
        while ((request = in.readLine()) != null) { // 这里也会阻塞!
            System.out.println("办理业务: " + request);
            // 模拟耗时业务
            Thread.sleep(1000); // 假设每笔业务需要1秒
            out.println("业务办理完成: " + request);
        }
        
        customer.close();
        System.out.println("顾客离开");
    }
}

问题来了

  1. 第100个顾客来了,但只有3个柜台怎么办?→ 排队,体验差

  2. 柜员给一个顾客办业务,这个顾客慢慢填表,柜员只能干等→ 资源浪费

  3. 顾客太多,创建太多线程→ 内存耗尽,系统崩溃

这就是现实:一个Tomcat默认200个线程,2000人同时访问就卡死。

一、NIO:银行的叫号系统

1.1 什么是NIO?

NIO(Non-blocking IO,非阻塞IO)就像银行的叫号系统

  • 一个大堂经理(一个线程)管理所有顾客

  • 顾客取号后去旁边等待(非阻塞)

  • 哪个顾客准备好了(数据就绪),经理就叫哪个

// 叫号系统的核心组件
public class NioCoreComponents {
    public static void main(String[] args) {
        System.out.println("NIO三大核心组件:");
        System.out.println("1. Selector(选择器)- 大堂经理");
        System.out.println("2. Channel(通道)- 办理窗口");
        System.out.println("3. Buffer(缓冲区)- 业务申请表");
    }
}

1.2 Buffer(缓冲区)- 数据的"申请表"

为什么需要Buffer?
传统IO是流(水流),数据来了就得马上处理。NIO是块(砖头),数据可以暂存起来。

public class BufferDetailDemo {
    public static void main(String[] args) {
        System.out.println("========= Buffer深度解析 =========");
        
        // 创建一个10字节的缓冲区(就像10个格子的申请表)
        ByteBuffer buffer = ByteBuffer.allocate(10);
        printBuffer("初始状态", buffer);  // 位置=0, 限制=10, 容量=10
        
        System.out.println("\n1. 写入数据(顾客填表):");
        buffer.put("Hello".getBytes());  // 写入5个字节
        printBuffer("写入5字节后", buffer);  // 位置=5, 限制=10, 容量=10
        
        System.out.println("\n2. 切换为读模式(柜员查看申请表):");
        buffer.flip();  // 翻转!这是关键操作
        printBuffer("flip()之后", buffer);  // 位置=0, 限制=5, 容量=10
        
        System.out.println("\n3. 读取数据(柜员读取申请表内容):");
        byte[] data = new byte[buffer.limit()];
        buffer.get(data);  // 读取数据
        System.out.println("读取的内容: " + new String(data));
        printBuffer("读取后", buffer);  // 位置=5, 限制=5, 容量=10
        
        System.out.println("\n4. 清空缓冲区(业务办完,表格回收):");
        buffer.clear();  // 注意:clear不会删除数据,只是重置指针
        printBuffer("clear()之后", buffer);  // 位置=0, 限制=10, 容量=10
        
        System.out.println("\n5. compact压缩缓冲区(部分处理,保留未处理数据):");
        buffer.put("HelloWorld".getBytes());  // 写入10字节
        buffer.flip();
        buffer.get(new byte[5]);  // 读取前5字节
        printBuffer("读取5字节后", buffer);  // 位置=5, 限制=10
        buffer.compact();  // 把未读的"World"移到开头
        printBuffer("compact()之后", buffer);  // 位置=5, 限制=10
        buffer.put("!!!".getBytes());  // 可以继续写入
        printBuffer("再写入3字节", buffer);  // 位置=8, 限制=10
    }
    
    static void printBuffer(String stage, ByteBuffer buffer) {
        System.out.printf("%s: position=%d, limit=%d, capacity=%d%n",
                stage, buffer.position(), buffer.limit(), buffer.capacity());
    }
}

Buffer四个关键属性

  • capacity(容量):表格总格子数,创建后不变

  • limit(限制):当前允许操作的最大位置

  • position(位置):下一个要操作的位置

  • mark(标记):临时标记位置,可通过reset()返回

Buffer状态流转图

      +------[初始]------+
      | position=0      |
      | limit=capacity  |
      +-----------------+
             |
             | put()
             v
      +------[写模式]-----+
      | position=写入的数据量 |
      | limit=capacity   |
      +-----------------+
             |
             | flip()
             v
      +------[读模式]-----+
      | position=0       |
      | limit=写入的数据量  |
      +-----------------+
             |
             | get()
             v
      +------[读取后]-----+
      | position=读取的数据量 |
      | limit=写入的数据量  |
      +-----------------+
             |
        clear()/compact()
             v
      +------[重置]------+
      | position=0       |
      | limit=capacity   |
      +-----------------+

1.3 Channel(通道)- 数据的"办理窗口"

Channel vs Stream

  • Stream是单行道(InputStream只能读,OutputStream只能写)

  • Channel是双行道(可读可写),支持异步

public class ChannelDetailDemo {
    public static void main(String[] args) throws Exception {
        System.out.println("========= Channel类型详解 =========");
        
        System.out.println("\n1. FileChannel - 文件通道:");
        fileChannelDemo();
        
        System.out.println("\n2. SocketChannel - 网络套接字通道:");
        socketChannelDemo();
        
        System.out.println("\n3. ServerSocketChannel - 服务器监听通道:");
        serverSocketChannelDemo();
        
        System.out.println("\n4. DatagramChannel - UDP通道:");
        datagramChannelDemo();
    }
    
    static void fileChannelDemo() throws IOException {
        // 创建测试文件
        String content = "Hello, FileChannel!";
        Files.write(Paths.get("test.txt"), content.getBytes());
        
        // 使用FileChannel读取文件
        try (FileInputStream fis = new FileInputStream("test.txt");
             FileChannel channel = fis.getChannel()) {
            
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int bytesRead = channel.read(buffer);  // 从通道读取到缓冲区
            
            buffer.flip();
            byte[] data = new byte[buffer.limit()];
            buffer.get(data);
            System.out.println("文件内容: " + new String(data));
            
            // 零拷贝演示
            try (FileOutputStream fos = new FileOutputStream("copy.txt");
                 FileChannel destChannel = fos.getChannel()) {
                buffer.flip();  // 重置position
                destChannel.write(buffer);  // 直接从缓冲区写入
            }
        }
        
        Files.deleteIfExists(Paths.get("test.txt"));
        Files.deleteIfExists(Paths.get("copy.txt"));
    }
    
    static void socketChannelDemo() throws IOException {
        System.out.println("创建SocketChannel的两种方式:");
        System.out.println("方式1: SocketChannel.open()");
        SocketChannel clientChannel = SocketChannel.open();
        
        System.out.println("\n配置非阻塞模式(关键!):");
        clientChannel.configureBlocking(false);  // 设为非阻塞
        
        System.out.println("尝试连接到服务器(非阻塞立即返回):");
        boolean connected = clientChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
        System.out.println("连接结果: " + connected);  // 非阻塞模式可能返回false
        
        System.out.println("\n完成连接(在非阻塞模式下需要循环检查):");
        while (!clientChannel.finishConnect()) {
            System.out.println("连接进行中...可以做其他事");
            Thread.sleep(100);
        }
        
        System.out.println("\n读写操作都是非阻塞的:");
        ByteBuffer buffer = ByteBuffer.wrap("Hello".getBytes());
        int bytesWritten = clientChannel.write(buffer);  // 非阻塞,立即返回
        System.out.println("写入字节数: " + bytesWritten);
        
        clientChannel.close();
    }
    
    static void serverSocketChannelDemo() throws IOException {
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);  // 非阻塞
        serverChannel.bind(new InetSocketAddress(8888));
        
        System.out.println("ServerSocketChannel监听中...");
        System.out.println("非阻塞模式下,accept()立即返回:");
        SocketChannel client = serverChannel.accept();  // 非阻塞,没有连接返回null
        System.out.println("accept结果: " + (client == null ? "null" : "有连接"));
        
        serverChannel.close();
    }
    
    static void datagramChannelDemo() throws IOException {
        DatagramChannel channel = DatagramChannel.open();
        channel.configureBlocking(false);
        channel.bind(new InetSocketAddress(9999));
        
        System.out.println("UDP通道已绑定到端口9999");
        channel.close();
    }
}

通道的重要特性

  1. 非阻塞模式configureBlocking(false)是关键

  2. 分散/聚集:一次操作多个Buffer

  3. 文件锁定:FileChannel支持文件锁

  4. 内存映射:FileChannel.map()实现内存映射文件

1.4 Selector(选择器)- 多路复用器

Selector工作原理(最核心最难理解的部分):

public class SelectorDetailDemo {
    public static void main(String[] args) throws IOException {
        System.out.println("========= Selector工作原理 =========");
        
        // 创建Selector(大堂经理)
        Selector selector = Selector.open();
        
        // 创建ServerSocketChannel(银行门口)
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);  // 必须非阻塞
        serverChannel.bind(new InetSocketAddress(8888));
        
        // 注册到Selector,关注ACCEPT事件(新顾客事件)
        // register()返回的SelectionKey就是顾客的"号牌"
        SelectionKey serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("注册ServerSocketChannel,关注ACCEPT事件");
        
        // 为这个Key附加一些额外信息(比如顾客的VIP级别)
        serverKey.attach("VIP通道");
        
        // 开始处理事件
        processEvents(selector);
    }
    
    static void processEvents(Selector selector) throws IOException {
        System.out.println("\n======= 事件处理循环开始 =======");
        
        while (true) {
            System.out.println("\n等待事件发生(select()会阻塞)...");
            // select()方法会阻塞,直到有事件发生
            // 可以设置超时:selector.select(1000) - 最多等1秒
            int readyChannels = selector.select(2000);  // 2秒超时
            
            if (readyChannels == 0) {
                System.out.println("2秒内没有事件,继续等待...");
                continue;
            }
            
            System.out.println("有 " + readyChannels + " 个Channel就绪");
            
            // 获取就绪的SelectionKey集合(拿到需要服务的顾客号牌)
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                
                System.out.println("\n处理Key: " + key);
                System.out.println("附加信息: " + key.attachment());
                
                // 判断事件类型
                if (key.isAcceptable()) {
                    System.out.println("事件类型: 新连接");
                    handleAccept(key, selector);
                }
                
                if (key.isConnectable()) {
                    System.out.println("事件类型: 连接完成");
                    handleConnect(key);
                }
                
                if (key.isReadable()) {
                    System.out.println("事件类型: 可读");
                    handleRead(key);
                }
                
                if (key.isWritable()) {
                    System.out.println("事件类型: 可写");
                    handleWrite(key);
                }
                
                // 非常重要:处理完必须移除,否则下次还会处理
                keyIterator.remove();
                System.out.println("Key已从selectedKeys移除");
            }
        }
    }
    
    static void handleAccept(SelectionKey key, Selector selector) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverChannel.accept();  // 不会阻塞
        clientChannel.configureBlocking(false);
        
        System.out.println("接受新连接: " + clientChannel.getRemoteAddress());
        
        // 为新连接注册读事件
        // 注意:这里只注册了OP_READ,没有注册OP_WRITE
        // 因为写事件通常只在需要写时才注册
        clientChannel.register(selector, SelectionKey.OP_READ);
        
        // 发送欢迎消息
        ByteBuffer welcome = ByteBuffer.wrap("Welcome!\n".getBytes());
        clientChannel.write(welcome);
    }
    
    static void handleConnect(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        if (channel.finishConnect()) {
            System.out.println("连接建立完成");
            // 连接建立后,可以注册读事件
            key.interestOps(SelectionKey.OP_READ);
        }
    }
    
    static void handleRead(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        int bytesRead = channel.read(buffer);
        if (bytesRead > 0) {
            buffer.flip();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            String message = new String(data);
            System.out.println("收到消息: " + message.trim());
            
            // 回显消息
            String response = "Echo: " + message;
            ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
            channel.write(responseBuffer);
            
            // 如果缓冲区还有空间,可以继续监听读事件
            // 如果缓冲区满了,可能需要暂停读事件监听
        } else if (bytesRead == -1) {
            System.out.println("客户端关闭连接");
            channel.close();
            key.cancel();  // 取消这个Key
        }
    }
    
    static void handleWrite(SelectionKey key) throws IOException {
        System.out.println("处理写事件...");
        // 写完后,通常要取消写事件监听,避免CPU空转
        key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
    }
}

Selector关键概念

  1. SelectionKey:事件令牌,包含:

    • Channel:关联的通道

    • Selector:关联的选择器

    • InterestOps:感兴趣的事件集合

    • ReadyOps:就绪的事件集合

    • Attachment:附加对象

  2. 事件类型

    • OP_ACCEPT:新连接,ServerSocketChannel专用

    • OP_CONNECT:连接建立,SocketChannel专用

    • OP_READ:数据可读

    • OP_WRITE:数据可写

  3. 重要方法

    • select():阻塞等待事件

    • select(timeout):带超时的等待

    • selectNow():立即返回,不阻塞

    • wakeup():唤醒阻塞的select()

1.5 完整的NIO服务器示例

public class CompleteNioServer {
    private Selector selector;
    private ServerSocketChannel serverChannel;
    private final ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    private final ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
    
    public void start(int port) throws IOException {
        // 1. 创建Selector
        selector = Selector.open();
        
        // 2. 创建ServerSocketChannel
        serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(port));
        
        // 3. 注册ACCEPT事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO服务器启动在端口 " + port);
        
        // 4. 事件循环
        eventLoop();
    }
    
    private void eventLoop() throws IOException {
        while (true) {
            // 等待事件,最多1秒
            int readyCount = selector.select(1000);
            
            if (readyCount == 0) {
                // 没有事件,可以做一些其他工作
                doOtherWork();
                continue;
            }
            
            // 处理事件
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                keyIterator.remove();  // 必须移除!
                
                try {
                    if (!key.isValid()) {
                        continue;  // Key已失效
                    }
                    
                    if (key.isAcceptable()) {
                        acceptClient(key);
                    } else if (key.isReadable()) {
                        readData(key);
                    } else if (key.isWritable()) {
                        writeData(key);
                    }
                } catch (Exception e) {
                    // 异常处理:关闭通道,取消Key
                    System.err.println("处理事件异常: " + e.getMessage());
                    if (key != null) {
                        key.cancel();
                        try {
                            key.channel().close();
                        } catch (IOException ex) {
                            // 忽略关闭异常
                        }
                    }
                }
            }
        }
    }
    
    private void acceptClient(SelectionKey key) throws IOException {
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        SocketChannel client = server.accept();
        client.configureBlocking(false);
        
        System.out.println("接受新连接: " + client.getRemoteAddress());
        
        // 注册读事件,并附加一个缓冲区给这个连接
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        client.register(selector, SelectionKey.OP_READ, buffer);
        
        // 发送欢迎消息
        String welcome = "Welcome to NIO Server!\n";
        ByteBuffer welcomeBuf = ByteBuffer.wrap(welcome.getBytes());
        client.write(welcomeBuf);
    }
    
    private void readData(SelectionKey key) throws IOException {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        
        buffer.clear();  // 清空缓冲区准备读取
        
        int bytesRead = client.read(buffer);
        if (bytesRead == -1) {
            // 客户端关闭连接
            System.out.println("客户端断开: " + client.getRemoteAddress());
            client.close();
            key.cancel();
            return;
        }
        
        if (bytesRead > 0) {
            buffer.flip();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            String message = new String(data).trim();
            System.out.println("收到: " + message);
            
            // 处理消息
            String response = processMessage(message);
            
            // 切换到写模式
            key.interestOps(SelectionKey.OP_WRITE);
            key.attach(ByteBuffer.wrap(response.getBytes()));
        }
    }
    
    private void writeData(SelectionKey key) throws IOException {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        
        while (buffer.hasRemaining()) {
            int written = client.write(buffer);
            if (written == 0) {
                // TCP缓冲区满,等待下次写事件
                break;
            }
        }
        
        if (!buffer.hasRemaining()) {
            // 数据发送完毕,切换回读模式
            key.interestOps(SelectionKey.OP_READ);
            key.attach(ByteBuffer.allocate(1024));  // 重新分配读缓冲区
        }
    }
    
    private String processMessage(String message) {
        // 简单的业务处理
        if ("time".equalsIgnoreCase(message)) {
            return "当前时间: " + new Date();
        } else if ("quit".equalsIgnoreCase(message)) {
            return "BYE";
        } else {
            return "ECHO: " + message;
        }
    }
    
    private void doOtherWork() {
        // 这里可以执行一些后台任务
        // 比如:清理资源、记录日志、监控状态等
        // System.out.println("执行其他任务...");
    }
    
    public void stop() throws IOException {
        if (selector != null) {
            selector.close();
        }
        if (serverChannel != null) {
            serverChannel.close();
        }
    }
    
    public static void main(String[] args) throws IOException {
        CompleteNioServer server = new CompleteNioServer();
        try {
            server.start(8888);
        } finally {
            server.stop();
        }
    }
}

二、Netty:NIO的"豪华装修版"

2.1 为什么需要Netty?

原生NIO的问题

  1. API太复杂:Selector、Channel、Buffer各种组合,容易出错

  2. 需要处理太多细节:拆包粘包、编码解码、异常处理

  3. 性能优化困难:内存管理、线程模型需要自己实现

  4. Bug多:空轮询Bug、epoll Bug等

Netty的优势

  • 简单:封装了NIO的复杂性

  • 高性能:优化的线程模型,零拷贝

  • 稳定:经过大规模生产验证(阿里、Facebook、Twitter都在用)

  • 功能全:支持HTTP、WebSocket、ProtoBuf等多种协议

2.2 Netty核心组件详解

public class NettyComponentsDetail {
    public static void main(String[] args) {
        System.out.println("========= Netty核心组件 =========");
        
        System.out.println("\n1. Channel - 通信的管道");
        System.out.println("   类似SocketChannel,但功能更强");
        System.out.println("   类型:NioSocketChannel, NioServerSocketChannel");
        
        System.out.println("\n2. EventLoop - 事件循环(核心中的核心)");
        System.out.println("   一个EventLoop = 一个线程 + 一个Selector");
        System.out.println("   负责处理:IO事件、定时任务、普通任务");
        
        System.out.println("\n3. ChannelPipeline - 处理器管道");
        System.out.println("   一系列ChannelHandler的链式处理");
        System.out.println("   数据像流水一样经过每个Handler");
        
        System.out.println("\n4. ChannelHandler - 业务处理器");
        System.out.println("   处理具体的业务逻辑");
        System.out.println("   分两种:Inbound(入站)、Outbound(出站)");
        
        System.out.println("\n5. ByteBuf - 增强的缓冲区");
        System.out.println("   比ByteBuffer更好用,支持引用计数、池化");
    }
}

2.3 Netty线程模型(最核心!)

public class NettyThreadModel {
    public static void main(String[] args) {
        System.out.println("========= Netty线程模型详解 =========");
        
        System.out.println("\n传统模型的问题:");
        System.out.println("  一个连接一个线程 → 线程太多");
        System.out.println("  线程池模型 → 上下文切换开销大");
        
        System.out.println("\nNetty的解决方案:Reactor模式");
        System.out.println("  Boss Group:1个线程,负责接受连接");
        System.out.println("  Worker Group:N个线程,负责处理IO");
        System.out.println("  一个Worker线程管理多个连接");
        
        System.out.println("\n为什么这样设计?");
        System.out.println("  1. 连接建立是低频操作,1个线程足够");
        System.out.println("  2. IO操作是高频操作,需要多个线程");
        System.out.println("  3. 一个连接固定在一个线程,避免并发问题");
        
        System.out.println("\n线程数设置建议:");
        System.out.println("  Boss线程数 = 1(通常足够)");
        System.out.println("  Worker线程数 = CPU核心数 * 2");
    }
}

2.4 完整Netty服务器(带详细注释)

/**
 * 完整的Netty服务器示例
 * 包含:心跳检测、编解码、业务处理、异常处理
 */
public class CompleteNettyServer {
    
    public static void main(String[] args) throws InterruptedException {
        System.out.println("开始启动Netty服务器...");
        
        // ========== 第一步:创建线程组 ==========
        // BossGroup:负责接受客户端连接
        // 参数1:线程数,通常1个就够了(除非有多个网卡或多个端口)
        EventLoopGroup bossGroup = new NioEventLoopGroup(1, new ThreadFactory() {
            private final AtomicInteger counter = new AtomicInteger(0);
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "BOSS-" + counter.incrementAndGet());
            }
        });
        
        // WorkerGroup:负责处理IO操作
        // 参数0:使用默认线程数(CPU核心数*2)
        EventLoopGroup workerGroup = new NioEventLoopGroup(0, new ThreadFactory() {
            private final AtomicInteger counter = new AtomicInteger(0);
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "WORKER-" + counter.incrementAndGet());
                thread.setDaemon(true);  // 设置为守护线程
                return thread;
            }
        });
        
        try {
            // ========== 第二步:创建服务器引导类 ==========
            ServerBootstrap bootstrap = new ServerBootstrap();
            
            // ========== 第三步:配置参数 ==========
            bootstrap.group(bossGroup, workerGroup)          // 设置线程组
                    .channel(NioServerSocketChannel.class)   // 使用NIO传输
                    
                    // BossGroup配置
                    .option(ChannelOption.SO_BACKLOG, 128)   // 连接队列大小
                    .option(ChannelOption.SO_REUSEADDR, true) // 端口重用
                    
                    // WorkerGroup配置
                    .childOption(ChannelOption.TCP_NODELAY, true)  // 禁用Nagle算法
                    .childOption(ChannelOption.SO_KEEPALIVE, true) // 保持连接
                    .childOption(ChannelOption.SO_RCVBUF, 32 * 1024) // 接收缓冲区
                    .childOption(ChannelOption.SO_SNDBUF, 32 * 1024) // 发送缓冲区
                    .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) // 使用内存池
                    
                    // ========== 第四步:设置处理器 ==========
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            // 获取管道(数据处理的流水线)
                            ChannelPipeline pipeline = ch.pipeline();
                            
                            System.out.println("初始化客户端通道: " + ch.remoteAddress());
                            
                            // ===== 添加处理器(顺序很重要!) =====
                            
                            // 1. 空闲检测(5秒未读触发)
                            pipeline.addLast("idleStateHandler", 
                                    new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
                            
                            // 2. 心跳处理器(处理空闲事件)
                            pipeline.addLast("heartbeatHandler", new HeartbeatHandler());
                            
                            // 3. 解决TCP粘包/拆包问题
                            // 按行分割(\n或\r\n)
                            pipeline.addLast("lineBasedFrameDecoder", 
                                    new LineBasedFrameDecoder(1024));
                            
                            // 4. 字符串解码器(ByteBuf → String)
                            pipeline.addLast("stringDecoder", 
                                    new StringDecoder(CharsetUtil.UTF_8));
                            
                            // 5. 字符串编码器(String → ByteBuf)
                            pipeline.addLast("stringEncoder", 
                                    new StringEncoder(CharsetUtil.UTF_8));
                            
                            // 6. 业务处理器
                            pipeline.addLast("businessHandler", new BusinessHandler());
                            
                            // 7. 异常处理器(放在最后,捕获所有异常)
                            pipeline.addLast("exceptionHandler", new ExceptionHandler());
                        }
                    });
            
            // ========== 第五步:绑定端口 ==========
            System.out.println("绑定端口8888...");
            ChannelFuture future = bootstrap.bind(8888).sync();
            System.out.println("服务器启动成功!");
            
            // ========== 第六步:注册关闭钩子 ==========
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                System.out.println("\n收到关闭信号,优雅关闭服务器...");
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
                System.out.println("服务器已关闭");
            }));
            
            // ========== 第七步:等待服务器关闭 ==========
            future.channel().closeFuture().sync();
            
        } finally {
            // ========== 第八步:优雅关闭 ==========
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
    
    /**
     * 心跳处理器
     */
    static class HeartbeatHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
            if (evt instanceof IdleStateEvent) {
                IdleStateEvent event = (IdleStateEvent) evt;
                if (event.state() == IdleState.READER_IDLE) {
                    System.out.println("5秒未收到心跳,发送心跳包");
                    ctx.writeAndFlush("HEARTBEAT_REQUEST\n");
                }
            } else {
                ctx.fireUserEventTriggered(evt);
            }
        }
    }
    
    /**
     * 业务处理器
     */
    static class BusinessHandler extends SimpleChannelInboundHandler<String> {
        
        // 连接建立时调用
        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            System.out.println("客户端连接: " + ctx.channel().remoteAddress());
            ctx.writeAndFlush("欢迎连接Netty服务器!\n输入'help'查看帮助\n");
        }
        
        // 收到消息时调用
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) {
            System.out.println("收到消息[" + ctx.channel().remoteAddress() + "]: " + msg);
            
            // 处理消息
            String response = processCommand(msg.trim());
            
            // 发送响应
            ctx.writeAndFlush(response + "\n");
        }
        
        // 连接断开时调用
        @Override
        public void channelInactive(ChannelHandlerContext ctx) {
            System.out.println("客户端断开: " + ctx.channel().remoteAddress());
        }
        
        // 处理命令
        private String processCommand(String command) {
            switch (command.toLowerCase()) {
                case "help":
                    return "可用命令:\n" +
                           "  time - 查看服务器时间\n" +
                           "  echo <message> - 回声测试\n" +
                           "  stats - 查看服务器状态\n" +
                           "  quit - 断开连接";
                case "time":
                    return "服务器时间: " + new SimpleDateFormat("HH:mm:ss").format(new Date());
                case "stats":
                    return "服务器状态: 运行正常\n连接数: " + 
                           ChannelManager.getConnectionCount();
                case "quit":
                    return "BYE";
                default:
                    if (command.startsWith("echo ")) {
                        return command.substring(5);
                    }
                    return "未知命令,输入'help'查看帮助";
            }
        }
    }
    
    /**
     * 异常处理器
     */
    static class ExceptionHandler extends ChannelDuplexHandler {
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            if (cause instanceof IOException) {
                System.out.println("客户端异常断开: " + cause.getMessage());
            } else {
                System.err.println("处理异常: " + cause.getMessage());
                cause.printStackTrace();
            }
            ctx.close();
        }
        
        @Override
        public void channelWritabilityChanged(ChannelHandlerContext ctx) {
            // 处理写缓冲区满的情况
            if (!ctx.channel().isWritable()) {
                System.out.println("写缓冲区满,暂停接收数据");
                // 可以暂停读取,避免OOM
                ctx.channel().config().setAutoRead(false);
            } else {
                System.out.println("写缓冲区可用,恢复接收数据");
                ctx.channel().config().setAutoRead(true);
            }
        }
    }
    
    /**
     * 连接管理器
     */
    static class ChannelManager {
        private static final ConcurrentMap<String, Channel> channels = 
                new ConcurrentHashMap<>();
        
        public static void addChannel(Channel channel) {
            channels.put(channel.id().asShortText(), channel);
        }
        
        public static void removeChannel(Channel channel) {
            channels.remove(channel.id().asShortText());
        }
        
        public static int getConnectionCount() {
            return channels.size();
        }
    }
}

2.5 Netty客户端(带重连机制)

public class CompleteNettyClient {
    private EventLoopGroup group;
    private Bootstrap bootstrap;
    private Channel channel;
    private final String host;
    private final int port;
    private volatile boolean reconnect = true;
    private int reconnectAttempts = 0;
    private static final int MAX_RECONNECT_ATTEMPTS = 5;
    
    public CompleteNettyClient(String host, int port) {
        this.host = host;
        this.port = port;
    }
    
    public void start() throws InterruptedException {
        System.out.println("启动Netty客户端...");
        
        group = new NioEventLoopGroup(1, new ThreadFactory() {
            private final AtomicInteger counter = new AtomicInteger(0);
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "CLIENT-" + counter.incrementAndGet());
            }
        });
        
        bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ChannelPipeline pipeline = ch.pipeline();
                        
                        // 添加处理器(顺序要和服务端对应)
                        pipeline.addLast(new LineBasedFrameDecoder(1024));
                        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                        pipeline.addLast(new ClientHandler());
                    }
                });
        
        // 连接服务器
        connect();
        
        // 启动控制台输入
        startConsoleInput();
    }
    
    private void connect() {
        if (!reconnect) {
            return;
        }
        
        System.out.println("连接服务器 " + host + ":" + port + "...");
        
        ChannelFuture future = bootstrap.connect(host, port);
        
        future.addListener((ChannelFutureListener) f -> {
            if (f.isSuccess()) {
                System.out.println("连接服务器成功!");
                channel = f.channel();
                reconnectAttempts = 0;  // 重置重连次数
            } else {
                System.err.println("连接服务器失败: " + f.cause().getMessage());
                
                if (reconnect && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
                    reconnectAttempts++;
                    System.out.println("第" + reconnectAttempts + "次重连,3秒后重试...");
                    
                    // 定时重连
                    f.channel().eventLoop().schedule(this::connect, 3, TimeUnit.SECONDS);
                } else {
                    System.err.println("重连次数超过限制,停止重连");
                    stop();
                }
            }
        });
    }
    
    private void startConsoleInput() {
        new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            while (true) {
                try {
                    if (!scanner.hasNextLine()) {
                        Thread.sleep(100);
                        continue;
                    }
                    
                    String line = scanner.nextLine().trim();
                    
                    if ("quit".equalsIgnoreCase(line)) {
                        System.out.println("退出客户端");
                        stop();
                        break;
                    }
                    
                    if (channel != null && channel.isActive()) {
                        channel.writeAndFlush(line + "\n");
                    } else {
                        System.err.println("未连接到服务器,无法发送消息");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            scanner.close();
        }, "Console-Input").start();
    }
    
    public void sendMessage(String message) {
        if (channel != null && channel.isActive()) {
            channel.writeAndFlush(message + "\n");
        }
    }
    
    public void stop() {
        reconnect = false;
        
        if (channel != null) {
            channel.close();
        }
        
        if (group != null) {
            group.shutdownGracefully();
        }
        
        System.out.println("客户端已停止");
    }
    
    static class ClientHandler extends SimpleChannelInboundHandler<String> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) {
            System.out.println("服务器回复: " + msg);
        }
        
        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            System.out.println("已连接到服务器");
        }
        
        @Override
        public void channelInactive(ChannelHandlerContext ctx) {
            System.out.println("与服务器断开连接");
        }
        
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            System.err.println("客户端异常: " + cause.getMessage());
            ctx.close();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        CompleteNettyClient client = new CompleteNettyClient("127.0.0.1", 8888);
        
        // 注册关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread(client::stop));
        
        client.start();
    }
}

三、WebSocket:双向通信的终极方案

3.1 HTTP的局限性 vs WebSocket的优势

HTTP轮询的问题(传统方案):

// 客户端每1秒轮询一次
public class HttpPollingClient {
    public void pollServer() {
        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
        executor.scheduleAtFixedRate(() -> {
            try {
                // 1. 创建HTTP连接(每次都要握手)
                URL url = new URL("http://server.com/check");
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                
                // 2. 发送请求(即使没有新消息)
                BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String response = in.readLine();
                
                // 3. 处理响应
                if (!"no new message".equals(response)) {
                    processMessage(response);
                }
                
                in.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, 0, 1, TimeUnit.SECONDS);  // 每秒请求一次
    }
}

问题分析

  1. 浪费带宽:90%的请求可能都是"没有新消息"

  2. 延迟高:最坏情况要等1秒才收到消息

  3. 服务器压力大:每秒都要处理大量请求

WebSocket的优势

  • 一次握手,持久连接:建立连接后一直保持

  • 双向通信:服务器可以主动推送

  • 低延迟:消息实时到达

  • 节省资源:没有重复的HTTP头

3.2 WebSocket协议详解

握手过程(HTTP升级):

客户端请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

服务器响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

数据帧格式

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

帧类型(opcode)

  • 0x0:继续帧(数据分片)

  • 0x1:文本帧

  • 0x2:二进制帧

  • 0x8:关闭帧

  • 0x9:Ping帧

  • 0xA:Pong帧

3.3 完整的WebSocket聊天室

/**
 * 完整的WebSocket聊天室服务器
 * 功能:用户管理、房间管理、消息广播、历史记录
 */
public class CompleteWebSocketChatServer {
    
    public static void main(String[] args) throws InterruptedException {
        System.out.println("启动WebSocket聊天室服务器...");
        
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            
                            // ===== HTTP处理器 =====
                            // WebSocket基于HTTP,首先处理HTTP请求
                            pipeline.addLast("httpCodec", new HttpServerCodec());
                            
                            // 聚合HTTP请求(WebSocket握手请求是一个完整的HTTP请求)
                            pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
                            
                            // ===== WebSocket处理器 =====
                            // 处理WebSocket握手和协议升级
                            pipeline.addLast("webSocketHandler", new WebSocketServerProtocolHandler(
                                    "/ws",      // WebSocket路径
                                    null,       // 子协议
                                    true,       // 允许扩展
                                    65536,      // 最大帧大小
                                    false,      // 不验证掩码(客户端必须掩码)
                                    true,       // 强制关闭超时
                                    10000L      // 握手超时10秒
                            ));
                            
                            // WebSocket数据压缩
                            pipeline.addLast("compression", new WebSocketServerCompressionHandler());
                            
                            // ===== 业务处理器 =====
                            pipeline.addLast("chatHandler", new WebSocketChatHandler());
                            
                            // ===== 异常处理器 =====
                            pipeline.addLast("exceptionHandler", new ExceptionHandler());
                        }
                    });
            
            // 绑定端口
            ChannelFuture future = bootstrap.bind(8080).sync();
            System.out.println("WebSocket服务器启动在 ws://localhost:8080/ws");
            System.out.println("HTTP管理界面: http://localhost:8080");
            
            // 等待关闭
            future.channel().closeFuture().sync();
            
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
    
    /**
     * WebSocket聊天处理器
     */
    static class WebSocketChatHandler extends SimpleChannelInboundHandler<Object> {
        
        // 使用ChannelGroup管理所有连接
        private static final ChannelGroup allChannels = 
                new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
        
        // 用户信息映射
        private static final Map<ChannelId, UserInfo> users = 
                new ConcurrentHashMap<>();
        
        // 聊天室映射
        private static final Map<String, ChatRoom> rooms = 
                new ConcurrentHashMap<>();
        
        static {
            // 初始化默认聊天室
            rooms.put("general", new ChatRoom("general", "综合聊天室"));
            rooms.put("tech", new ChatRoom("tech", "技术交流"));
            rooms.put("game", new ChatRoom("game", "游戏天地"));
        }
        
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) {
            Channel incoming = ctx.channel();
            allChannels.add(incoming);
            
            System.out.println("新连接: " + incoming.remoteAddress());
            
            // 发送欢迎消息
            sendWelcomeMessage(incoming);
        }
        
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) {
            Channel leaving = ctx.channel();
            allChannels.remove(leaving);
            
            UserInfo user = users.remove(leaving.id());
            if (user != null) {
                System.out.println("用户离开: " + user.getUsername());
                
                // 通知其他用户
                broadcastSystemMessage(user.getUsername() + " 离开了聊天室");
                
                // 从聊天室移除
                if (user.getCurrentRoom() != null) {
                    ChatRoom room = rooms.get(user.getCurrentRoom());
                    if (room != null) {
                        room.removeMember(user.getUsername());
                    }
                }
            }
        }
        
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
            if (msg instanceof FullHttpRequest) {
                // 处理HTTP请求(比如管理界面)
                handleHttpRequest(ctx, (FullHttpRequest) msg);
            } else if (msg instanceof WebSocketFrame) {
                // 处理WebSocket帧
                handleWebSocketFrame(ctx, (WebSocketFrame) msg);
            }
        }
        
        private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
            // 如果是WebSocket握手请求,交给后续处理器
            if (isWebSocketUpgrade(req)) {
                ctx.fireChannelRead(req.retain());
                return;
            }
            
            // 否则提供管理界面
            if (req.uri().equals("/")) {
                sendHtmlPage(ctx, req);
            } else if (req.uri().equals("/admin")) {
                sendAdminPage(ctx, req);
            } else {
                send404(ctx, req);
            }
        }
        
        private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
            // 关闭帧
            if (frame instanceof CloseWebSocketFrame) {
                handleCloseFrame(ctx, (CloseWebSocketFrame) frame);
                return;
            }
            
            // Ping帧(回复Pong)
            if (frame instanceof PingWebSocketFrame) {
                handlePingFrame(ctx, (PingWebSocketFrame) frame);
                return;
            }
            
            // Pong帧(心跳回复)
            if (frame instanceof PongWebSocketFrame) {
                handlePongFrame(ctx, (PongWebSocketFrame) frame);
                return;
            }
            
            // 只处理文本帧和二进制帧
            if (frame instanceof TextWebSocketFrame) {
                handleTextFrame(ctx, (TextWebSocketFrame) frame);
            } else if (frame instanceof BinaryWebSocketFrame) {
                handleBinaryFrame(ctx, (BinaryWebSocketFrame) frame);
            } else {
                throw new UnsupportedOperationException(
                        "不支持帧类型: " + frame.getClass().getName());
            }
        }
        
        private void handleCloseFrame(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
            System.out.println("收到关闭帧,关闭连接");
            ctx.close();
        }
        
        private void handlePingFrame(ChannelHandlerContext ctx, PingWebSocketFrame frame) {
            System.out.println("收到Ping,回复Pong");
            ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
        }
        
        private void handlePongFrame(ChannelHandlerContext ctx, PongWebSocketFrame frame) {
            System.out.println("收到Pong,连接正常");
            // 可以更新最后活跃时间
        }
        
        private void handleTextFrame(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
            String request = frame.text();
            System.out.println("收到文本消息: " + request);
            
            try {
                // 解析JSON消息
                JsonNode json = JsonUtil.parse(request);
                String type = json.get("type").asText();
                
                switch (type) {
                    case "login":
                        handleLogin(ctx, json);
                        break;
                    case "join":
                        handleJoinRoom(ctx, json);
                        break;
                    case "message":
                        handleChatMessage(ctx, json);
                        break;
                    case "leave":
                        handleLeaveRoom(ctx, json);
                        break;
                    case "list_users":
                        handleListUsers(ctx, json);
                        break;
                    case "list_rooms":
                        handleListRooms(ctx);
                        break;
                    default:
                        sendError(ctx, "未知消息类型: " + type);
                }
            } catch (Exception e) {
                sendError(ctx, "消息格式错误: " + e.getMessage());
            }
        }
        
        private void handleBinaryFrame(ChannelHandlerContext ctx, BinaryWebSocketFrame frame) {
            // 处理二进制数据(比如图片、文件)
            System.out.println("收到二进制数据,大小: " + frame.content().readableBytes());
            
            // 可以保存文件或转发给其他用户
            // 这里简单回复确认
            ctx.writeAndFlush(new TextWebSocketFrame("收到文件"));
        }
        
        private void handleLogin(ChannelHandlerContext ctx, JsonNode json) {
            String username = json.get("username").asText();
            Channel channel = ctx.channel();
            
            // 检查用户名是否已存在
            if (isUsernameTaken(username)) {
                sendError(ctx, "用户名已存在");
                return;
            }
            
            // 创建用户信息
            UserInfo user = new UserInfo(
                    channel.id().toString(),
                    username,
                    channel,
                    new Date()
            );
            
            users.put(channel.id(), user);
            
            // 发送登录成功响应
            JsonNode response = JsonUtil.createObject()
                    .put("type", "login_success")
                    .put("username", username)
                    .put("message", "登录成功");
            
            channel.writeAndFlush(new TextWebSocketFrame(response.toString()));
            
            // 广播用户加入
            broadcastSystemMessage(username + " 加入了聊天室");
            
            System.out.println("用户登录: " + username);
        }
        
        private void handleJoinRoom(ChannelHandlerContext ctx, JsonNode json) {
            String roomId = json.get("room").asText();
            Channel channel = ctx.channel();
            UserInfo user = users.get(channel.id());
            
            if (user == null) {
                sendError(ctx, "请先登录");
                return;
            }
            
            // 检查聊天室是否存在
            ChatRoom room = rooms.get(roomId);
            if (room == null) {
                sendError(ctx, "聊天室不存在");
                return;
            }
            
            // 离开之前的房间
            if (user.getCurrentRoom() != null) {
                ChatRoom oldRoom = rooms.get(user.getCurrentRoom());
                if (oldRoom != null) {
                    oldRoom.removeMember(user.getUsername());
                    
                    // 通知原房间用户
                    broadcastToRoom(oldRoom.getId(), 
                            new TextWebSocketFrame(user.getUsername() + " 离开了房间"));
                }
            }
            
            // 加入新房间
            user.setCurrentRoom(roomId);
            room.addMember(user.getUsername());
            
            // 发送加入成功响应
            JsonNode response = JsonUtil.createObject()
                    .put("type", "join_success")
                    .put("room", roomId)
                    .put("roomName", room.getName())
                    .put("message", "已加入 " + room.getName());
            
            channel.writeAndFlush(new TextWebSocketFrame(response.toString()));
            
            // 发送房间历史消息
            sendRoomHistory(channel, room);
            
            // 通知房间其他用户
            broadcastToRoom(roomId, 
                    new TextWebSocketFrame(user.getUsername() + " 加入了房间"));
            
            System.out.println(user.getUsername() + " 加入房间: " + roomId);
        }
        
        private void handleChatMessage(ChannelHandlerContext ctx, JsonNode json) {
            String message = json.get("content").asText();
            Channel channel = ctx.channel();
            UserInfo user = users.get(channel.id());
            
            if (user == null) {
                sendError(ctx, "请先登录");
                return;
            }
            
            if (user.getCurrentRoom() == null) {
                sendError(ctx, "请先加入聊天室");
                return;
            }
            
            // 创建聊天消息
            ChatMessage chatMessage = new ChatMessage(
                    UUID.randomUUID().toString(),
                    user.getCurrentRoom(),
                    user.getUserId(),
                    user.getUsername(),
                    message,
                    new Date(),
                    MessageType.TEXT
            );
            
            // 保存到聊天室历史
            ChatRoom room = rooms.get(user.getCurrentRoom());
            if (room != null) {
                room.addMessage(chatMessage);
                
                // 广播给房间所有用户
                JsonNode broadcastMsg = JsonUtil.createObject()
                        .put("type", "chat_message")
                        .put("sender", user.getUsername())
                        .put("content", message)
                        .put("timestamp", chatMessage.getTimestamp().getTime());
                
                broadcastToRoom(room.getId(), 
                        new TextWebSocketFrame(broadcastMsg.toString()));
            }
            
            System.out.println(user.getUsername() + " 在 " + 
                    user.getCurrentRoom() + " 说: " + message);
        }
        
        private void broadcastSystemMessage(String message) {
            JsonNode msg = JsonUtil.createObject()
                    .put("type", "system_message")
                    .put("content", message)
                    .put("timestamp", System.currentTimeMillis());
            
            TextWebSocketFrame frame = new TextWebSocketFrame(msg.toString());
            allChannels.writeAndFlush(frame);
        }
        
        private void broadcastToRoom(String roomId, TextWebSocketFrame frame) {
            for (Map.Entry<ChannelId, UserInfo> entry : users.entrySet()) {
                UserInfo user = entry.getValue();
                if (roomId.equals(user.getCurrentRoom())) {
                    user.getChannel().writeAndFlush(frame.retain());
                }
            }
            frame.release();
        }
        
        private void sendWelcomeMessage(Channel channel) {
            JsonNode welcome = JsonUtil.createObject()
                    .put("type", "welcome")
                    .put("message", "欢迎来到WebSocket聊天室")
                    .put("timestamp", System.currentTimeMillis());
            
            channel.writeAndFlush(new TextWebSocketFrame(welcome.toString()));
        }
        
        private void sendError(ChannelHandlerContext ctx, String error) {
            JsonNode errorMsg = JsonUtil.createObject()
                    .put("type", "error")
                    .put("message", error);
            
            ctx.writeAndFlush(new TextWebSocketFrame(errorMsg.toString()));
        }
        
        private void sendRoomHistory(Channel channel, ChatRoom room) {
            List<ChatMessage> history = room.getHistory();
            if (history.isEmpty()) {
                return;
            }
            
            JsonNode historyMsg = JsonUtil.createObject()
                    .put("type", "room_history")
                    .put("room", room.getId());
            
            JsonNode messages = JsonUtil.createArray();
            for (ChatMessage msg : history) {
                JsonNode msgNode = JsonUtil.createObject()
                        .put("sender", msg.getSender())
                        .put("content", msg.getContent())
                        .put("timestamp", msg.getTimestamp().getTime());
                messages.add(msgNode);
            }
            
            historyMsg.set("messages", messages);
            channel.writeAndFlush(new TextWebSocketFrame(historyMsg.toString()));
        }
        
        private boolean isUsernameTaken(String username) {
            return users.values().stream()
                    .anyMatch(user -> username.equals(user.getUsername()));
        }
        
        private boolean isWebSocketUpgrade(FullHttpRequest req) {
            return req.headers().contains(HttpHeaderNames.UPGRADE) &&
                   req.headers().contains(HttpHeaderNames.CONNECTION) &&
                   "websocket".equalsIgnoreCase(req.headers().get(HttpHeaderNames.UPGRADE));
        }
        
        private void sendHtmlPage(ChannelHandlerContext ctx, FullHttpRequest req) {
            String html = "<html><body><h1>WebSocket聊天室</h1>" +
                    "<p>请使用WebSocket连接 ws://" + 
                    req.headers().get(HttpHeaderNames.HOST) + "/ws</p>" +
                    "</body></html>";
            
            FullHttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1,
                    HttpResponseStatus.OK,
                    Unpooled.copiedBuffer(html, CharsetUtil.UTF_8));
            
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
            
            ctx.writeAndFlush(response);
        }
        
        private void sendAdminPage(ChannelHandlerContext ctx, FullHttpRequest req) {
            JsonNode stats = JsonUtil.createObject()
                    .put("online_users", users.size())
                    .put("total_rooms", rooms.size())
                    .put("server_time", new Date().toString());
            
            String html = "<html><body><h1>管理后台</h1>" +
                    "<pre>" + stats.toString() + "</pre>" +
                    "</body></html>";
            
            FullHttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1,
                    HttpResponseStatus.OK,
                    Unpooled.copiedBuffer(html, CharsetUtil.UTF_8));
            
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
            
            ctx.writeAndFlush(response);
        }
        
        private void send404(ChannelHandlerContext ctx, FullHttpRequest req) {
            FullHttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1,
                    HttpResponseStatus.NOT_FOUND);
            
            ctx.writeAndFlush(response);
        }
    }
    
    // ========== 辅助类 ==========
    
    static class UserInfo {
        private final String userId;
        private final String username;
        private final Channel channel;
        private String currentRoom;
        private final Date joinTime;
        
        // 构造方法、getter、setter省略
    }
    
    static class ChatRoom {
        private final String id;
        private final String name;
        private final Set<String> members = ConcurrentHashMap.newKeySet();
        private final List<ChatMessage> history = new ArrayList<>();
        
        // 构造方法、getter、setter省略
    }
    
    static class ChatMessage {
        private final String id;
        private final String roomId;
        private final String senderId;
        private final String sender;
        private final String content;
        private final Date timestamp;
        private final MessageType type;
        
        // 构造方法、getter省略
    }
    
    enum MessageType {
        TEXT, IMAGE, FILE, SYSTEM
    }
    
    static class JsonUtil {
        private static final ObjectMapper mapper = new ObjectMapper();
        
        static JsonNode parse(String json) throws IOException {
            return mapper.readTree(json);
        }
        
        static ObjectNode createObject() {
            return mapper.createObjectNode();
        }
        
        static ArrayNode createArray() {
            return mapper.createArrayNode();
        }
    }
}

3.4 HTML客户端页面

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket聊天室</title>
    <meta charset="UTF-8">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Microsoft YaHei', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            border-radius: 10px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.2);
            overflow: hidden;
        }
        header {
            background: #4a5568;
            color: white;
            padding: 20px;
            text-align: center;
        }
        .main {
            display: flex;
            min-height: 600px;
        }
        .sidebar {
            width: 300px;
            background: #f7fafc;
            border-right: 1px solid #e2e8f0;
            padding: 20px;
        }
        .content {
            flex: 1;
            padding: 20px;
            display: flex;
            flex-direction: column;
        }
        .login-form {
            background: #edf2f7;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 20px;
        }
        .room-list {
            margin-top: 20px;
        }
        .room-item {
            padding: 10px;
            margin: 5px 0;
            background: white;
            border-radius: 5px;
            cursor: pointer;
            transition: background 0.3s;
        }
        .room-item:hover {
            background: #e2e8f0;
        }
        .room-item.active {
            background: #4299e1;
            color: white;
        }
        .user-list {
            margin-top: 20px;
        }
        .user-item {
            padding: 8px;
            display: flex;
            align-items: center;
        }
        .user-status {
            width: 10px;
            height: 10px;
            border-radius: 50%;
            margin-right: 10px;
        }
        .user-status.online {
            background: #48bb78;
        }
        .message-area {
            flex: 1;
            border: 1px solid #e2e8f0;
            border-radius: 8px;
            padding: 20px;
            overflow-y: auto;
            margin-bottom: 20px;
            background: #f8f9fa;
        }
        .message {
            margin-bottom: 15px;
            padding: 10px 15px;
            border-radius: 8px;
            max-width: 80%;
        }
        .message.system {
            background: #e2e8f0;
            margin: 10px auto;
            text-align: center;
            max-width: 60%;
            color: #718096;
        }
        .message.incoming {
            background: #ebf8ff;
            align-self: flex-start;
            border-left: 4px solid #4299e1;
        }
        .message.outgoing {
            background: #f0fff4;
            align-self: flex-end;
            margin-left: auto;
            border-right: 4px solid #48bb78;
        }
        .message-header {
            font-size: 12px;
            color: #718096;
            margin-bottom: 5px;
            display: flex;
            justify-content: space-between;
        }
        .input-area {
            display: flex;
            gap: 10px;
        }
        input, button {
            padding: 12px 16px;
            border: 1px solid #e2e8f0;
            border-radius: 8px;
            font-size: 16px;
        }
        input {
            flex: 1;
            outline: none;
            transition: border 0.3s;
        }
        input:focus {
            border-color: #4299e1;
            box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
        }
        button {
            background: #4299e1;
            color: white;
            border: none;
            cursor: pointer;
            transition: background 0.3s;
        }
        button:hover {
            background: #3182ce;
        }
        button:disabled {
            background: #cbd5e0;
            cursor: not-allowed;
        }
        .status {
            padding: 10px;
            text-align: center;
            background: #f7fafc;
            border-top: 1px solid #e2e8f0;
            font-size: 14px;
            color: #718096;
        }
        .status.connected {
            color: #48bb78;
        }
        .status.disconnected {
            color: #f56565;
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>WebSocket聊天室</h1>
            <p>实时聊天,畅所欲言</p>
        </header>
        
        <div class="main">
            <!-- 侧边栏 -->
            <div class="sidebar">
                <!-- 登录表单 -->
                <div class="login-form" id="loginForm">
                    <h3>登录聊天室</h3>
                    <input type="text" id="username" placeholder="请输入用户名" style="width: 100%; margin: 10px 0;">
                    <button onclick="login()" style="width: 100%;">登录</button>
                </div>
                
                <!-- 房间列表 -->
                <div class="room-list" id="roomList" style="display: none;">
                    <h3>聊天室列表</h3>
                    <div class="room-item" onclick="joinRoom('general')">
                        🏠 综合聊天室
                    </div>
                    <div class="room-item" onclick="joinRoom('tech')">
                        💻 技术交流
                    </div>
                    <div class="room-item" onclick="joinRoom('game')">
                        🎮 游戏天地
                    </div>
                </div>
                
                <!-- 用户列表 -->
                <div class="user-list" id="userList" style="display: none;">
                    <h3>在线用户 (<span id="userCount">0</span>)</h3>
                    <div id="users"></div>
                </div>
            </div>
            
            <!-- 主内容区 -->
            <div class="content">
                <!-- 消息区域 -->
                <div class="message-area" id="messageArea">
                    <div class="message system">
                        欢迎来到WebSocket聊天室!
                    </div>
                </div>
                
                <!-- 输入区域 -->
                <div class="input-area">
                    <input type="text" id="messageInput" placeholder="输入消息..." disabled>
                    <button onclick="sendMessage()" id="sendBtn" disabled>发送</button>
                </div>
            </div>
        </div>
        
        <!-- 状态栏 -->
        <div class="status disconnected" id="status">
            未连接
        </div>
    </div>

    <script>
        // WebSocket连接
        let socket = null;
        let username = '';
        let currentRoom = '';
        let users = new Set();
        
        // 连接WebSocket服务器
        function connect() {
            const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
            const wsUrl = protocol + '//' + window.location.host + '/ws';
            
            socket = new WebSocket(wsUrl);
            
            socket.onopen = function(event) {
                console.log('WebSocket连接成功');
                updateStatus(true);
                
                // 如果已有用户名,自动重新登录
                if (username) {
                    login();
                }
            };
            
            socket.onmessage = function(event) {
                console.log('收到消息:', event.data);
                handleMessage(JSON.parse(event.data));
            };
            
            socket.onclose = function(event) {
                console.log('WebSocket连接关闭');
                updateStatus(false);
                
                // 3秒后重连
                setTimeout(connect, 3000);
            };
            
            socket.onerror = function(error) {
                console.error('WebSocket错误:', error);
            };
        }
        
        // 处理服务器消息
        function handleMessage(data) {
            switch (data.type) {
                case 'welcome':
                    addSystemMessage(data.message);
                    break;
                    
                case 'login_success':
                    handleLoginSuccess(data);
                    break;
                    
                case 'join_success':
                    handleJoinSuccess(data);
                    break;
                    
                case 'chat_message':
                    addChatMessage(data.sender, data.content, data.timestamp, false);
                    break;
                    
                case 'system_message':
                    addSystemMessage(data.content);
                    break;
                    
                case 'room_history':
                    handleRoomHistory(data);
                    break;
                    
                case 'user_list':
                    updateUserList(data.users);
                    break;
                    
                case 'error':
                    alert('错误: ' + data.message);
                    break;
            }
        }
        
        // 登录
        function login() {
            const input = document.getElementById('username');
            username = input.value.trim();
            
            if (!username) {
                alert('请输入用户名');
                return;
            }
            
            if (!socket || socket.readyState !== WebSocket.OPEN) {
                alert('连接未就绪');
                return;
            }
            
            const msg = {
                type: 'login',
                username: username
            };
            
            socket.send(JSON.stringify(msg));
        }
        
        // 处理登录成功
        function handleLoginSuccess(data) {
            document.getElementById('loginForm').style.display = 'none';
            document.getElementById('roomList').style.display = 'block';
            document.getElementById('userList').style.display = 'block';
            document.getElementById('messageInput').disabled = false;
            document.getElementById('sendBtn').disabled = false;
            
            addSystemMessage('登录成功: ' + data.username);
        }
        
        // 加入房间
        function joinRoom(roomId) {
            if (!socket || socket.readyState !== WebSocket.OPEN) {
                alert('请先登录');
                return;
            }
            
            const msg = {
                type: 'join',
                room: roomId
            };
            
            socket.send(JSON.stringify(msg));
            currentRoom = roomId;
            
            // 更新房间选中状态
            document.querySelectorAll('.room-item').forEach(item => {
                item.classList.remove('active');
                if (item.textContent.includes(getRoomName(roomId))) {
                    item.classList.add('active');
                }
            });
        }
        
        // 处理加入成功
        function handleJoinSuccess(data) {
            clearMessages();
            addSystemMessage(data.message);
            
            // 请求用户列表
            const msg = {
                type: 'list_users',
                room: data.room
            };
            socket.send(JSON.stringify(msg));
        }
        
        // 处理房间历史
        function handleRoomHistory(data) {
            data.messages.forEach(msg => {
                addChatMessage(msg.sender, msg.content, msg.timestamp, true);
            });
        }
        
        // 发送消息
        function sendMessage() {
            const input = document.getElementById('messageInput');
            const message = input.value.trim();
            
            if (!message) {
                return;
            }
            
            if (!currentRoom) {
                alert('请先加入聊天室');
                return;
            }
            
            const msg = {
                type: 'message',
                content: message
            };
            
            socket.send(JSON.stringify(msg));
            
            // 添加到自己的消息列表
            addChatMessage(username, message, Date.now(), true);
            
            // 清空输入框
            input.value = '';
        }
        
        // 添加系统消息
        function addSystemMessage(message) {
            const messageArea = document.getElementById('messageArea');
            const msgDiv = document.createElement('div');
            msgDiv.className = 'message system';
            msgDiv.textContent = message;
            messageArea.appendChild(msgDiv);
            scrollToBottom();
        }
        
        // 添加聊天消息
        function addChatMessage(sender, content, timestamp, isOutgoing) {
            const messageArea = document.getElementById('messageArea');
            const msgDiv = document.createElement('div');
            msgDiv.className = `message ${isOutgoing ? 'outgoing' : 'incoming'}`;
            
            const time = new Date(timestamp).toLocaleTimeString();
            
            msgDiv.innerHTML = `
                <div class="message-header">
                    <span>${sender}</span>
                    <span>${time}</span>
                </div>
                <div>${content}</div>
            `;
            
            messageArea.appendChild(msgDiv);
            scrollToBottom();
        }
        
        // 清空消息
        function clearMessages() {
            const messageArea = document.getElementById('messageArea');
            messageArea.innerHTML = '';
        }
        
        // 更新用户列表
        function updateUserList(userList) {
            const usersDiv = document.getElementById('users');
            usersDiv.innerHTML = '';
            
            userList.forEach(user => {
                const userDiv = document.createElement('div');
                userDiv.className = 'user-item';
                userDiv.innerHTML = `
                    <div class="user-status online"></div>
                    <span>${user}</span>
                `;
                usersDiv.appendChild(userDiv);
            });
            
            document.getElementById('userCount').textContent = userList.length;
        }
        
        // 更新连接状态
        function updateStatus(connected) {
            const statusDiv = document.getElementById('status');
            if (connected) {
                statusDiv.textContent = '已连接';
                statusDiv.className = 'status connected';
            } else {
                statusDiv.textContent = '连接断开,正在重连...';
                statusDiv.className = 'status disconnected';
            }
        }
        
        // 滚动到底部
        function scrollToBottom() {
            const messageArea = document.getElementById('messageArea');
            messageArea.scrollTop = messageArea.scrollHeight;
        }
        
        // 获取房间名称
        function getRoomName(roomId) {
            const roomNames = {
                'general': '综合聊天室',
                'tech': '技术交流',
                'game': '游戏天地'
            };
            return roomNames[roomId] || roomId;
        }
        
        // 回车发送消息
        document.getElementById('messageInput').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });
        
        // 页面加载时连接
        window.onload = connect;
        
        // 页面关闭时断开连接
        window.onbeforeunload = function() {
            if (socket) {
                socket.close();
            }
        };
    </script>
</body>
</html>

四、技术选型深度分析

4.1 什么情况下用NIO?什么情况下用Netty?

使用原生NIO的场景(极少):

  1. 对性能有极致要求:需要手动控制每一个细节

  2. 特殊需求:需要绕过Netty的某些限制

  3. 学习研究:理解底层原理

  4. 依赖限制:不能引入第三方库

使用Netty的场景(绝大多数):

  1. 快速开发:需要快速构建网络应用

  2. 生产环境:需要稳定性、可维护性

  3. 复杂协议:需要HTTP、WebSocket、gRPC等支持

  4. 团队协作:标准化的代码,容易维护

性能对比表格

指标 原生NIO Netty 说明
开发效率 Netty封装了复杂细节
性能 理论上限高 优化后接近上限 Netty做了大量优化
内存管理 手动管理 自动池化 Netty的ByteBuf更好用
线程模型 需要自己实现 Reactor模式内置 Netty的线程模型成熟
协议支持 需要自己实现 内置多种协议 HTTP/WebSocket/Protobuf等
社区生态 强大 大量开源项目使用
学习成本 Netty需要学习,但比NIO简单

结语

通过这篇超详细的教程,你应该已经掌握了:

✅ NIO的核心原理:Selector多路复用、非阻塞IO
✅ Netty的完整使用:线程模型、编解码、性能优化
✅ WebSocket的实现:协议细节、完整聊天室

关键要点回顾

  1. NIO是基础,理解Selector才能理解Netty

  2. Netty线程模型是核心,一定要彻底理解

  3. ByteBuf管理很重要,生产环境一定要用池化

  4. WebSocket不是魔法,本质是TCP长链接

Logo

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

更多推荐