Java NIO

NIO 是什么?为什么需要它?

NIO(New I/O 或 Non-blocking I/O) 是 Java 1.4 引入的一套高性能 I/O 框架,用于替代传统的 BIO(Blocking I/O)。它的目标是:用更少的线程,处理更多的连接,特别适合高并发场景(如聊天服务器、游戏后端、日志收集系统等)。

而 NIO 的强大,离不开它的 三大核心组件:

✅ Buffer(缓冲区)
✅ Channel(通道)
✅ Selector(选择器)

核心组件讲解:

buffer(缓冲区)

⁉️ 是什么?
Buffer 是一个固定大小的容器,用于临时存储读写的数据。
所有 NIO 操作都必须通过 Buffer,不能直接操作 Channel。
🔑 核心属性:
属性 说明
capacity 容量,最大能存多少数据(创建后不可变)
position 当前读/写的位置(从 0 开始)
limit 最多能读/写到哪里
🪧 关键方法:
flip():写 → 读 切换(把 position 设为 0,limit 设为当前 position)
clear():清空缓冲区(position=0, limit=capacity)
rewind():重置 position=0,保留数据(用于重复读)
💡 举个生活例子:
你往快递箱(Buffer)里塞包裹(写数据),塞满后盖上盖子(flip()),快递员(Channel)来取走(读数据)。下次再用,先清空箱子(clear())。

✅ 代码示例:文件复制(使用 ByteBuffer)

// 文件复制:source.txt → target.txt
try (FileChannel in = FileChannel.open(Paths.get("source.txt"), StandardOpenOption.READ);
     FileChannel out = FileChannel.open(Paths.get("target.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
 
    ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配 1KB 缓冲区
 
    while (in.read(buffer) != -1) { // 读入数据
        buffer.flip();              // 切换为读模式
        out.write(buffer);          // 写出数据
        buffer.clear();             // 清空,准备下一轮
    }
}

channel(通道)

⁉️ 是什么?

  • Channel 类似于传统 IO 的 流(Stream),但它是双向的(既能读也能写)。
    常见实现:
    • FileChannel:文件读写
    • SocketChannel:TCP 客户端
    • ServerSocketChannel:TCP 服务端
    • DatagramChannel:UDP

✨ 特点:
必须配合 Buffer 使用
支持 阻塞 / 非阻塞模式(通过 configureBlocking(false) 设置)
✅ 代码示例:非阻塞 TCP 服务端(原生 NIO)

// 创建 ServerSocketChannel
ServerSocketChannel server = ServerSocketChan nel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false); // ⭐ 设置为非阻塞!
 
// 接受连接(非阻塞:无连接时立即返回 null)
SocketChannel client = server.accept();
if (client != null) {
    client.configureBlocking(false);
    // 后续用 Selector 监听读事件...
}

Selector(选择器)

⁉️ 是什么?
Selector 允许单个线程监听多个 Channel 的 I/O 事件(如可读、可写、连接)。
这就是 I/O 多路复用 的核心,也是 NIO 高并发的关键!
🔑 工作流程:
创建 Selector
将 Channel 注册到 Selector,并指定关注事件(OP_READ, OP_WRITE 等)
调用 select() 阻塞等待事件发生
遍历 selectedKeys() 处理就绪的 Channel
✅ 代码示例:单线程管理多个客户端

Selector selector = Selector.open(); // 打开一个选择器
ServerSocketChannel server = ServerSocketChannel.open(); // 打开一个TCP服务端通道
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false); // 设置“非阻塞”模式运行,如不设置则不可注册到selector中
server.register(selector, SelectionKey.OP_ACCEPT); // 设置关注“新连接”事件
 
while (true) {
    selector.select(); // 阻塞直到有意向的事件发生,这里底层是有操作系统的epoll(windows是select)实现
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> iter = keys.iterator();
 
    while (iter.hasNext()) {
        SelectionKey key = iter.next();
        iter.remove(); // 必须remove
 
        if (key.isAcceptable()) {
            // 处理新连接
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ); // 这里把client也注册到selector中,关注“可读”事件
        } else if (key.isReadable()) {
            // 处理读事件
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buf = ByteBuffer.allocate(1024);
            int len = client.read(buf);
            if (len > 0) {
                buf.flip();
                System.out.println("收到: " + new String(buf.array(), 0, len));
            }
        }
    }
}

✳️Selector可管理的通道

不是所有通道都可以被Selector管理,比如FileChannel,判断是否可以使用Selector管理。判断一个 Channel 能被 Selector 复用,有一个前提:判断他是否继承了一个抽象类 SelectableChannel。如果继承了 SelectableChannel , 则可以被复用,否则不能。

❓为什么 FileChannel 不使用 Selector?

磁盘I/O与网络I/O的区别:磁盘I/O通常是顺序的,并且访问时间是可预测的,尤其是对于本地文件系统。相反,网络I/O往往是非常不确定的,因为网络延时和丢包等因素都会影响数据的到达时间。

文件系统的同步性与网络的异步性:当我们执行磁盘上的文件操作时,例如从文件中读取数据或向文件写入数据,这些操作大部分时间是同步的。操作系统通常会保证在进行下一个操作之前,当前的操作已经完成(一次性做完不用长时间保持连接,网络对端不止是否完成所有操作,具有不确定性)。这种可预测性意味着,文件系统的操作很少需要非阻塞或多路复用的特性。

不过,jdk.17也提供了异步非阻塞的AsynchronousFileChannel,简单来说实现原理是通过传入一个回调函数实现

AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(Paths.get("source.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
asynchronousFileChannel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
	@Override
	public void completed(Integer result, ByteBuffer attachment) {
		// 数据读取完成后,这个方法将被调用(读不完还要嵌套)
		System.out.println("Read done");
	}

	@Override
	public void failed(Throwable exc, ByteBuffer attachment) {
		// 数据读取失败后,这个方法将被调用
		System.out.println("Read failed");
		exc.printStackTrace();
	}
});

总结

网络IO

✡️ NIO 三大件关系图

        ┌─────────────┐
        │   Selector  │ ← 单线程监控多个 Channel
        └──────┬──────┘
               │
    ┌──────────▼──────────┐
    │  ServerSocketChannel│ ← 注册 OP_ACCEPT
    │   SocketChannel     │ ← 注册 OP_CONNECT/OP_READ/OP_WRITE
    │  DatagramChannel    │ ← 注册 OP_READ/OP_WRITE
    └──────────┬──────────┘
               │
        ┌──────▼──────┐
        │   Buffer    │ ← 所有数据读写必须经过 Buffer
        └─────────────┘

↔️ 以下是传统 BIO 多线程模型(ServerSocket+多线程处理IO)与 NIO 多路复用模型(Selector + ServerSocketChannel)的详细对比:

特性 传统 BIO (ServerSocket + Socket + 多线程) NIO 非阻塞多路复用 (Selector + ServerSocketChannel)
I/O 模型 阻塞 I/O 非阻塞 I/O + 多路复用
线程模型 一连接一线程(或线程池) 一线程可管理多个连接(Reactor 模式)
连接处理方式 每个连接由独立线程处理,线程在读写时阻塞 一个线程轮询多个通道的就绪事件,仅就绪后处理
资源开销 线程数随连接数增长,内存和上下文切换开销大 少量线程处理大量连接,内存开销低,上下文切换少
最大并发连接数 受限于系统线程数(通常几百到几千) 可支持数万甚至数十万连接(取决于系统资源)
线程利用率 低,大量线程空闲等待 I/O 高,线程始终处理就绪事件,无空闲等待
数据读写 直接读写,可能阻塞导致线程挂起 就绪后才读写,不会阻塞线程
编程复杂度 低,顺序编程,易于理解 高,需处理事件注册、缓冲区管理、粘包半包等
适用场景 连接数少、长连接、低并发(如小规模内网服务) 高并发、短连接、大量空闲连接(如Web服务器、聊天服务)
扩展性 差,增加连接需增加线程或线程池大小 好,可通过增加 Selector 或线程池进一步扩展
典型应用 早期简单服务器,如单机小应用 Netty、Tomcat NIO 模式、Redis(单线程多路复用)
数据拷贝优化 通常涉及多次内存拷贝 可配合直接缓冲区实现零拷贝(如 FileChannel.transferTo)
异常处理 线程内异常易处理,单个连接故障不影响其他 事件驱动,需小心处理异常,避免影响整个 Selector 循环

核心差异总结

  • BIO 多线程:简单直观,但线程资源随连接数线性增长,适合连接数可控的低并发场景。
  • NIO 多路复用:通过少量线程管理海量连接,适合高并发、长连接或大量空闲连接的场景,但开发复杂度较高。

实际生产中,可结合两者优势:用 NIO 处理 I/O 事件,将业务逻辑交给线程池,实现高性能与可维护性的平衡。

文件IO

⏬ 实现

  • FileChannel(jdk1.4)

  • AsynchronousFileChannel(jdk1.7)

↔️ 以下是文件BIO(传统I/O)与NIO(阻塞 NIO、异步NIO)的详细对比:

特性 BIO 流 (FileInputStream/OutputStream) FileChannel (阻塞 NIO) AsynchronousFileChannel (异步 NIO)
所属包 java.io java.nio.channels java.nio.channels
I/O 模型 流式 I/O 通道 + 缓冲区 异步通道 + 缓冲区
阻塞特性 阻塞(线程等待操作完成) 阻塞(线程等待操作完成) 非阻塞(调用立即返回,完成后回调/Future)
线程模型 每个操作独占线程 每个操作独占线程 少量线程可管理大量并发操作
读写方向 单向(读或写需不同流) 双向(同一个通道可读可写) 双向(同一个通道可读可写)
缓冲区类型 byte[](堆内存) ByteBuffer(可堆内或直接内存) ByteBuffer(可堆内或直接内存)
零拷贝传输 不支持 支持 transferTo/transferFrom 不支持(无直接传输方法)
内存映射文件 不支持 支持 map() 获取 MappedByteBuffer 不支持
数据强制落盘 FileDescriptor.sync()(阻塞) force(boolean metaData)(阻塞) force(boolean metaData)(阻塞)
文件锁定 FileChannel 间接实现 lock()/tryLock()(阻塞) lock()(异步,Future 或回调)
与其他 NIO 集成 差(无法直接与 Selector 等配合) 好(可与 ByteBufferCharset 等配合) 好(可与异步机制、ByteBuffer 等配合)
编程复杂度 低(顺序编程) 中(需管理缓冲区、位置) 高(需处理回调或 Future 链)
适用场景 简单文件操作、小文件、顺序读写 大文件、随机访问、需零拷贝或内存映射的场景 高并发文件 I/O、需非阻塞的复杂应用

说明

  • 阻塞特性FileChannel 虽属 NIO,但默认阻塞;AsynchronousFileChannel 实现真正异步非阻塞。
  • 零拷贝FileChanneltransferTo 可直接将文件数据发送到网络通道,减少 CPU 拷贝。
  • 内存映射FileChannel 可将文件映射到内存,像访问数组一样操作文件,适合大文件随机读写。
  • 数据强制落盘:三者均可确保数据写入磁盘,但均为阻塞操作(包括异步通道的 force 方法)。
  • 文件锁定AsynchronousFileChannel 的锁定方法支持异步,避免阻塞调用线程。

实战Demo(主要对比NIO相较于BIO的优势)

要直观地对比 BIO 和 NIO 在“慢客户端”场景下的差异,最简单的方法不是真的去改网卡配置,而是写一个**“懒惰客户端”**。这个客户端连接上服务器后,故意休眠(Sleep)很长一段时间再发送数据。

这样,服务器端的处理逻辑差异就会被无限放大,从而清晰地看到 BIO 被“阻塞”的过程,以及 NIO 如何“绕过”阻塞去处理其他连接。

以下是一个完整的实战方案,包含代码示例和操作步骤。


🎯 场景设计

  • 目标:模拟N个客户端连接服务器。

  • 模拟“慢”

    • 服务端监听连接,拿到client套接字后,先取出【请求消息1】“Hello Server”,然后写入【响应消息】“Hello Client”,再等待读取【请求消息2】“Goodbye Server” 。
    • 客户端连接后,先发送【请求消息1】“Hello Server” ,然后等待读取服务器【响应消息】“Hello Client”,等待 3 秒再发送【请求消息2】“Goodbye Server”,发送后调用close()方法关闭连接 )。
  • 观察点:

    Server端处理逻辑保持相同:读【请求消息1】->写【响应消息】->读【请求消息2】

    • BIO Server:调用accept()接受Client1,顺序进行读写读,因client端【请求消息2】发送前有3秒阻塞,Server端页阻塞读,所以总耗时≈3秒+;Client1读写完成后,线程才循环再调用accept()接受Client2,处理Client2总耗时≈3*2秒+。
    • NIO Server:调用selector.select(),Client1连接触发OP_ACCEPT事件,接受Client1并注册到selector中,继续监听下个事件,Client1发送的数据触发OP_READ事件,则读、写,因client端【请求消息2】发送前有3秒阻塞,没发过来数据不会触发OP_READ,Server端就停留在selelct()方法处可以处理其他事件,可以接受/读/写Client2、Client3…,所以Client的耗时不会对其他Client造成有效的影响。

🧪 准备工作:慢客户端 (SlowClient.java)

这个客户端是关键。它连接服务器后,需要能拖住Server的IO让其套接字的生命周期被Thread.sleep(3000)影响到。

package com.zadaya.io.test;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SlowClient {

    // 定义请求并发量
    public static final int CONCURRENT_COUNT = 2;

    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();

        ExecutorService executor = Executors.newFixedThreadPool(CONCURRENT_COUNT);
        for (int i = 0; i < CONCURRENT_COUNT; i++) {
            executor.submit(() -> {
                try {
                    new SlowClient().start();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

        executor.shutdown();
        while (!executor.isTerminated()) {
            Thread.sleep(10);
        }

        System.out.println("\n\n所有客户端全部处理完毕,总耗时:" + (System.currentTimeMillis() - startTime) + "ms");
    }

    public void start() {
        String uuid = UUID.randomUUID().toString().replace("-", "");
        try (
                Socket socket = new Socket("127.0.0.1", 9001);
                InputStream in = socket.getInputStream();
                OutputStream out = socket.getOutputStream();
        ) {

            System.out.println("连接建立-" + uuid + "-" + java.time.LocalDateTime.now().toString().substring(0, 26));

            // 发送数据1
            String request = "<html><body><h1>Hello Server!</h1><p>来自客户端的请求1</p><reqId>" + uuid + "</reqId></body></html>";
            out.write(request.getBytes(StandardCharsets.UTF_8));
            out.flush();

            // 读取数据
            byte[] buffer = new byte[1024];
            int read;
            while ((read = in.read(buffer)) != -1) {
                System.out.println("接到数据-" + uuid + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "-" + new String(buffer, 0, read, StandardCharsets.UTF_8));
            }
            System.out.println("接收完成-" + uuid + "-" + java.time.LocalDateTime.now().toString().substring(0, 26));

            // 等待3000ms
            Thread.sleep(3000);

            // 发送数据2
            String request2 = "<html><body><h1>Goodbye Server!</h1><p>来自客户端的请求2</p><reqId>" + uuid + "</reqId></body></html>";
            out.write(request2.getBytes(StandardCharsets.UTF_8));
            out.flush();

            System.out.println("发送完成-" + uuid + "-" + java.time.LocalDateTime.now().toString().substring(0, 26));

            socket.close();
            System.out.println("连接完成-" + uuid + "-" + java.time.LocalDateTime.now().toString().substring(0, 26));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

⚔️ 对比实验 A:BIO 服务器 (BIOServer.java)

这个服务器使用单线程处理所有连接(这是 BIO 最典型的瓶颈场景,当然也可适当调高线程比如10)。

package com.zadaya.io.test;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;

public class BIOServer {

    // 定义监听的地址
    private static final String ADDRESS = "127.0.0.1";
    private static final int PORT = 9001;

    public static void main(String[] args) {
        new BIOServer().start(ADDRESS, PORT);
    }

    public void start(String address, int port) {
        try {
            // 创建固定大小线程池
            ExecutorService executor = Executors.newFixedThreadPool(1);

            try (ServerSocket serverSocket = new ServerSocket()) {
                serverSocket.bind(new InetSocketAddress(address, port)); // 绑定监听的 地址:端口
                while (true) {
                    Socket socket = serverSocket.accept();
                    executor.submit(new BIOServerHandler(socket));
                    // new BIOServerHandler(socket).run();
                    System.out.println();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

class BIOServerHandler implements Runnable {

    private Socket socket;

    public BIOServerHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        String oriReqId = null;
        try (
                OutputStream out = this.socket.getOutputStream();
                InputStream in = this.socket.getInputStream();
        ) {
            // 1. 先完整读取客户端请求
            StringBuilder requestStringBuilder = new StringBuilder();
            byte[] buffer = new byte[1024];
            int bytesRead;

            // 设置超时避免无限等待
            socket.setSoTimeout(0);

            // 读取1次请求数据
            while ((bytesRead = in.read(buffer)) != -1) {
                requestStringBuilder.append(new String(buffer, 0, bytesRead, StandardCharsets.UTF_8));
                // 检查是否读取完整(简单判断是否有</html>)
                if (requestStringBuilder.toString().contains("</html>")) {
                    break;
                }
            }
            String request = requestStringBuilder.toString();
            oriReqId = request.substring(request.indexOf("<reqId>") + 7, request.indexOf("</reqId>"));
            requestStringBuilder.setLength(0); // requestStringBuilder清空
            System.out.println("接收到【请求消息1】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + request);

            // 组装发送响应消息
            // 2. 构造正确的HTTP响应
            String response = "<html><body><h1>Hello Client!</h1><p>来自服务器的响应</p><reqId>" + oriReqId + "</reqId></body></html>";
            out.write(response.getBytes(StandardCharsets.UTF_8));
            out.flush();
            socket.shutdownOutput(); // 关闭输出流让客户端不再阻塞等待读取
            System.out.println("已发送【响应消息S】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + response);

            // 读取2次请求数据
            while ((bytesRead = in.read(buffer)) != -1) {
                requestStringBuilder.append(new String(buffer, 0, bytesRead, StandardCharsets.UTF_8));
            }
            String request2 = requestStringBuilder.toString();
            System.out.println("接收到【请求消息2】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + request2);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

预期输出现象:

  1. 启动 Server(单线程)。
  2. 使用线程池同时启动 2个Client:Server 控制台在打印 接收到【请求消息1】已发送【响应消息S】,会卡住 3秒
  3. 3 秒后,Server 打印 接收到【请求消息2】
  4. 此时Server 现在才开始处理 Client2 的 accept,控制台在打印 接收到【请求消息1】已发送【响应消息S】,依旧会卡住 3秒
  5. 总耗时极长,Client2 最后打印 接收到【请求消息2】 时距离启动Client已过去 6秒

⚔️ 对比实验 B:NIO 服务器 (NIOServer.java)

这个服务器使用单线程 + Selector,可以同时管理多个连接。

package com.zadaya.io.test;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {

    // 定义监听的地址
    private static final String ADDRESS = "127.0.0.1";
    private static final int PORT = 9001;

    public static void main(String[] args) {
        new NIOServer().start(ADDRESS, PORT);
    }

    public void start(String address, int port) {
        try (
                Selector selector = Selector.open();
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ) {

            serverSocketChannel.bind(new InetSocketAddress(address, port));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                int selected = selector.select();
                if (selected <= 0) {
                    // 0 个就绪的通道
                    continue;
                }
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (key.isValid()) {
                        // 继续处理
                    }
                    if (key.isAcceptable()) {
                        handleAccept(key, selector);
                    } else if (key.isReadable()) {
                        handleRead(key);
                    } else if (key.isWritable()) {
                        handleWrite(key);
                    } else {
                        // 读完写完可以删除
                        key.cancel();
                    }
                }
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void handleAccept(SelectionKey key, Selector selector) {
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        try {
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
        } catch (IOException e) {
            System.err.println("接受连接异常");
            key.cancel();
            e.printStackTrace();
        }
    }

    private void handleRead(SelectionKey key) {
        try {
            SocketChannel socketChannel = (SocketChannel) key.channel();
            // 创建缓冲区用于读取数据
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

            // 读取数据的StringBuffer
            StringBuilder requestStringBuilder = new StringBuilder();

            // 开始读取数据
            int bytesRead;
            while (true) {
                bytesRead = socketChannel.read(byteBuffer);
                if (bytesRead <= 0) {
                    break;
                }
                byteBuffer.flip(); // 缓冲区转换为读模式
                requestStringBuilder.append(new String(byteBuffer.array(), 0, bytesRead, StandardCharsets.UTF_8));
                byteBuffer.clear(); // 空缓冲区转换为写模式
            }
            String request = requestStringBuilder.toString();
            String oriReqId = (String) key.attachment();
            if (oriReqId == null) {
                // 记录首次请求的id
                oriReqId = request.substring(request.indexOf("<reqId>") + 7, request.indexOf("</reqId>"));
                key.attach(oriReqId);
                System.out.println("接收到【请求消息1】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + request);
                key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); // 增加写意向
            } else {
                System.out.println("接收到【请求消息2】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + request);
            }

        } catch (IOException e) {
            key.cancel();
            e.printStackTrace();
        }
    }

    private void handleWrite(SelectionKey key) {
        try {
            SocketChannel socketChannel = (SocketChannel) key.channel();

            // 组装发送响应消息
            // 构造正确的HTTP响应
            String oriReqId = (String) key.attachment();
            String response = "<html><body><h1>Hello Client!</h1><p>来自服务器的响应</p><reqId>" + oriReqId + "</reqId></body></html>";
            // 创建缓冲区用于写入数据 (直接用response数据创建)
            ByteBuffer byteBuffer = ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8));

            // 开始写入数据
            // 判断buffer中数据是否全部写完
            while (byteBuffer.hasRemaining()) {
                int written = socketChannel.write(byteBuffer);// 写入数据
                if (written <= 0) {
                    throw new RuntimeException("没有数据写入");
                }
            }

            System.out.println("已发送【响应消息S】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + response);
            key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); // 取消写意向
            socketChannel.shutdownOutput(); // 关闭输出流让客户端不再阻塞等待读取

        } catch (IOException e) {
            key.cancel();
            e.printStackTrace();
        }
    }
}

预期输出现象:

  1. 启动 Server。
  2. 使用线程池同时启动 2个Client:Server 控制台在打印 接收到【请求消息1】已发送【响应消息S】接收到【请求消息1】已发送【响应消息S】,会卡住 3秒
  3. Server线程不会卡在IO操作处,而在select()方法处等待事件通知。
  4. 3 秒后,Server 被OP_READ事件唤醒,控制台打印 接收到【请求消息2】接收到【请求消息2】
  5. 两个Client线程关闭连接,全部结束,此时距离启动Client大约也就1个 3秒

谨慎使用!!!

☣️生产实际应用,不建议使用JDK的NIO包手搓WEB服务,可使用成熟的Netty框架进行二次开发,原因如下:

1. 为什么推荐使用 Netty 而非直接使用 JDK NIO?

① 开发效率和复杂性

  • JDK NIO 虽然提供了底层能力,但直接使用它构建高性能 Web 服务需要处理大量细节:
    • 线程模型设计(如 Reactor 模式实现)
    • 缓冲区管理(ByteBuffer 的分配、释放、扩容)
    • 粘包/半包处理
    • 连接生命周期管理
    • 异常处理和断线重连
    • 零拷贝、内存池等优化
  • Netty 将这些通用问题抽象为组件(EventLoop、ChannelPipeline、ByteBuf、Codec 等),开发者只需关注业务逻辑,大幅提高开发效率。

② 稳定性和性能

  • Netty 经过全球无数生产项目验证,修复了大量 NIO 本身的陷阱(如 Selector 空轮询 Bug、Epoll Bug 等)。
  • 它提供了高性能的线程模型(主从 Reactor)、内存池(PooledByteBuf)、零拷贝(FileRegion)、高效的协议编解码,性能通常优于手写代码。
  • 内置多种传输实现(NIO、Epoll、KQueue、IOCP),能自动选择最佳方式。

③ 功能丰富

  • 支持 HTTP、WebSocket、SSL/TLS、HTTP/2 等数十种协议编解码器,可直接复用。
  • 提供流量整形、超时控制、心跳机制、IP 过滤等高级功能。
  • 良好的扩展性,通过 ChannelHandler 链式处理,易于定制。

④ 维护成本

  • 手写服务需要自己持续修复 Bug、适配新 JDK 版本、处理安全漏洞,而 Netty 由社区维护,升级方便。
  • 团队成员熟悉 Netty 后,开发维护成本远低于手写底层 NIO。

2. 是否绝对不能用 JDK NIO 手写?

在极少数场景下,直接使用 JDK NIO 也可能合理:

  • 极端定制化需求:如果对协议栈或性能有特殊要求,且无法通过 Netty 扩展实现,可能需要自己从底层做起。
  • 资源受限环境:如某些嵌入式系统,Netty 依赖包较大,可能不适合。
  • 学习研究目的:为了深入理解 NIO 和 Reactor 模型,手写 Demo 很有价值。
  • 极简服务:如果只是一个非常简单的 UDP 接收器,可能几十行 NIO 代码就能搞定,引入 Netty 显得“重”。

但即使在这些场景,通常也可以基于 Netty 裁剪(如使用核心模块),或借鉴其设计。

3. 结论

对于绝大多数生产级 Web 服务,强烈建议使用 Netty(或其他成熟框架如 Vert.x、Akka HTTP)进行开发,而不是从零手写 JDK NIO。 这可以避免重复造轮子,减少 Bug,提高性能和可维护性。不过,如果项目规模极小、需求特殊或仅为学习,直接使用 NIO 也未尝不可。

Logo

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

更多推荐