Java NIO/AIO 与网络编程

掌握 Java NIO 的 Channel、Buffer 与 Selector 核心组件,理解非阻塞 I/O 与异步 I/O 的演进与选型

Java NIO(New I/O,非阻塞 I/O)在 JDK 1.4 引入,AIO(Asynchronous I/O)在 JDK 1.7 引入。理解这些 I/O 模型是掌握 Netty 等高性能框架的基础。

一、I/O 模型对比

1.1 五种 I/O 模型

模型阻塞点Java 支持特点
阻塞 I/O(BIO)全程阻塞JDK 1.0一个连接一个线程
非阻塞 I/O(NIO)JDK 1.4多路复用,单线程处理多连接
I/O 多路复用select/poll/epollJDK 1.4Selector 机制
信号驱动 I/O无原生支持Linux 信号机制
异步 I/O(AIO)完全无阻塞JDK 1.7内核完成 I/O 后回调

1.2 形象比喻

阻塞 I/O(BIO):
  你去餐厅,点餐后站在柜台等,直到餐做好才离开(阻塞)

非阻塞 I/O(NIO):
  你去餐厅,点餐后拿个号牌,每隔几分钟去问一次好了没(轮询)

I/O 多路复用:
  你去餐厅,点餐后坐在座位上,服务员做好后叫你的号牌(事件通知)

异步 I/O(AIO):
  你打电话点餐,商家做好后送到你家(完全异步, callback)

二、NIO 三大核心组件

2.1 Channel(通道)

// Channel 是双向的,区别于 Stream 的单向
// 主要实现:

// 文件通道
FileChannel fileChannel = new RandomAccessFile("data.txt", "rw").getChannel();

// 网络通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
SocketChannel socketChannel = SocketChannel.open();

// UDP 通道
DatagramChannel datagramChannel = DatagramChannel.open();

2.2 Buffer(缓冲区)

// NIO 数据读写必须通过 Buffer

// 创建 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);        // 堆内存
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);  // 直接内存

// Buffer 状态属性
// position: 当前读写位置
// limit: 可以读写的边界
// capacity: 最大容量

// 写入数据(写模式)
buffer.put("Hello".getBytes());
buffer.putInt(123);

// 切换为读模式
buffer.flip();
// flip():limit = position, position = 0

// 读取数据
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println(new String(data));

// 清空,重新写入
buffer.clear();
// clear():position = 0, limit = capacity

// 或者 compact():将未读数据移到头部
buffer.compact();

2.3 Selector(多路复用器)

// Selector 是 NIO 的核心,允许单线程监控多个 Channel

Selector selector = Selector.open();

// 注册 Channel 到 Selector
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);  // 必须非阻塞
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

// 事件循环
while (true) {
    // 阻塞等待就绪事件(有超时版本)
    int readyCount = selector.select();
    
    if (readyCount == 0) continue;
    
    // 获取就绪的 SelectionKey
    Set<SelectionKey> readyKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = readyKeys.iterator();
    
    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        iterator.remove();  // 必须移除,否则重复处理
        
        if (key.isAcceptable()) {
            // 有新的连接请求
            ServerSocketChannel server = (ServerSocketChannel) key.channel();
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
            System.out.println("Client connected: " + client.getRemoteAddress());
        }
        
        if (key.isReadable()) {
            // 有数据可读
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int read = client.read(buffer);
            
            if (read == -1) {
                // 连接关闭
                key.cancel();
                client.close();
            } else if (read > 0) {
                buffer.flip();
                // 处理数据...
                System.out.println("Received: " + new String(buffer.array(), 0, read));
                
                // 注册写事件(响应客户端)
                key.interestOps(SelectionKey.OP_WRITE);
            }
        }
        
        if (key.isWritable()) {
            // 可以写入数据
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer response = ByteBuffer.wrap("OK\n".getBytes());
            client.write(response);
            
            // 写完后改回读事件
            key.interestOps(SelectionKey.OP_READ);
        }
    }
}

三、完整 NIO Server 示例

public class NioEchoServer {
    
    private Selector selector;
    private ServerSocketChannel serverChannel;
    private ByteBuffer buffer = ByteBuffer.allocate(1024);
    
    public void start(int port) throws IOException {
        selector = Selector.open();
        
        serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(port));
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        System.out.println("NIO Server started on port " + port);
        
        while (true) {
            selector.select();
            
            Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
            while (keys.hasNext()) {
                SelectionKey key = keys.next();
                keys.remove();
                
                if (key.isAcceptable()) {
                    handleAccept(key);
                } else if (key.isReadable()) {
                    handleRead(key);
                } else if (key.isWritable()) {
                    handleWrite(key);
                }
            }
        }
    }
    
    private void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        SocketChannel client = server.accept();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ);
        System.out.println("Accepted connection from " + client.getRemoteAddress());
    }
    
    private void handleRead(SelectionKey key) throws IOException {
        SocketChannel client = (SocketChannel) key.channel();
        buffer.clear();
        
        int read = client.read(buffer);
        if (read == -1) {
            System.out.println("Client disconnected: " + client.getRemoteAddress());
            key.cancel();
            client.close();
            return;
        }
        
        buffer.flip();
        byte[] data = new byte[buffer.remaining()];
        buffer.get(data);
        String message = new String(data);
        System.out.println("Received from " + client.getRemoteAddress() + ": " + message.trim());
        
        // 准备响应
        key.attach("Echo: " + message);
        key.interestOps(SelectionKey.OP_WRITE);
    }
    
    private void handleWrite(SelectionKey key) throws IOException {
        SocketChannel client = (SocketChannel) key.channel();
        String response = (String) key.attachment();
        
        ByteBuffer buf = ByteBuffer.wrap(response.getBytes());
        client.write(buf);
        
        // 写完后继续监听读取
        key.interestOps(SelectionKey.OP_READ);
        key.attach(null);
    }
    
    public static void main(String[] args) throws IOException {
        new NioEchoServer().start(8080);
    }
}

四、AIO(NIO.2)

4.1 异步通道

public class AioEchoServer {
    
    public static void main(String[] args) throws IOException, InterruptedException {
        AsynchronousServerSocketChannel server = 
            AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));
        
        System.out.println("AIO Server started on port 8080");
        
        // 异步接受连接(回调方式)
        server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel client, Void attachment) {
                // 继续接受下一个连接
                server.accept(null, this);
                
                // 处理当前连接
                handleClient(client);
            }
            
            @Override
            public void failed(Throwable exc, Void attachment) {
                exc.printStackTrace();
            }
        });
        
        // 主线程阻塞,等待异步操作
        Thread.sleep(Long.MAX_VALUE);
    }
    
    private static void handleClient(AsynchronousSocketChannel client) {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        // 异步读取
        client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer buffer) {
                if (result == -1) {
                    try {
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    return;
                }
                
                buffer.flip();
                byte[] data = new byte[buffer.remaining()];
                buffer.get(data);
                String message = new String(data);
                System.out.println("Received: " + message.trim());
                
                // 异步写入响应
                ByteBuffer response = ByteBuffer.wrap(("Echo: " + message).getBytes());
                client.write(response, null, new CompletionHandler<Integer, Void>() {
                    @Override
                    public void completed(Integer result, Void attachment) {
                        // 继续读取
                        ByteBuffer newBuffer = ByteBuffer.allocate(1024);
                        client.read(newBuffer, newBuffer, this);
                    }
                    
                    @Override
                    public void failed(Throwable exc, Void attachment) {
                        exc.printStackTrace();
                    }
                });
            }
            
            @Override
            public void failed(Throwable exc, ByteBuffer buffer) {
                exc.printStackTrace();
            }
        });
    }
}

4.2 Future 方式

// Future 方式(阻塞获取结果,不推荐)
Future<AsynchronousSocketChannel> future = server.accept();
AsynchronousSocketChannel client = future.get();  // 阻塞

// 更好的方式:使用 CompletionHandler(回调)
// 或使用 CompletableFuture 包装

五、NIO vs AIO 对比

特性NIOAIO
模型多路复用(Reactor)真正异步(Proactor)
阻塞非阻塞(需轮询 Selector)完全非阻塞
线程应用处理 I/O内核处理 I/O,应用接收回调
实现复杂度较复杂较简单
Linux 实现epollio_uring(新)/ aio(旧,有限制)
成熟度高(Netty 基于此)
适用场景高并发网络服务文件 I/O 为主

六、Channel 类型速查

// 文件通道(NIO)
FileChannel fc = FileChannel.open(Path.of("file.txt"), StandardOpenOption.READ);
MappedByteBuffer mapped = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());  // 内存映射

// Socket 通道
SocketChannel sc = SocketChannel.open(new InetSocketAddress("localhost", 80));
sc.configureBlocking(false);

// ServerSocket 通道
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));

// 文件传输(零拷贝)
FileChannel src = new FileInputStream("src.txt").getChannel();
FileChannel dst = new FileOutputStream("dst.txt").getChannel();
src.transferTo(0, src.size(), dst);  // 零拷贝传输

七、总结

组件职责关键 API
ChannelI/O 通道,双向传输open(), configureBlocking(), register()
Buffer数据容器allocate(), flip(), clear(), compact()
Selector多路复用器open(), select(), selectedKeys()
SelectionKey注册关系与事件interestOps(), readyOps(), attach()

NIO 是 Java 高性能网络编程的基石。Selector 的多路复用机制让单个线程可以高效管理数千个连接,Buffer 的引入减少了数据拷贝,而 Channel 的双向特性简化了 I/O 操作。Netty 正是在这些基础之上,通过更高效的内存管理和线程模型,实现了百万级并发。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「network」更多文章

  1. 网络安全:TLS/SSL、证书与加密通信
  2. 负载均衡算法与高可用架构
  3. DNS 系统与智能解析