Java NIO详解:新手完全指南

文章目录

1. NIO简介

NIO(New I/O或Non-blocking I/O)是Java 1.4引入的一套全新的I/O API,为所有的原始类型提供缓冲区支持,使用它可以提供非阻塞式的高伸缩性网络和文件I/O操作。NIO被设计用来代替标准的Java I/O API(java.io包),提供了更高效的I/O操作方式。

1.1 NIO的核心优势

  1. 非阻塞I/O: 允许单个线程管理多个输入和输出通道,而不需要为每个通道创建单独的线程。
  2. 缓冲区操作: 所有数据都通过显式缓冲区处理,提供了更直接的数据访问控制。
  3. 选择器功能: 提供选择器机制,允许单个线程监视多个通道的I/O状态。
  4. 内存映射文件: 允许将文件直接映射到内存,提供更高效的大文件操作。
  5. 零拷贝技术: 减少了数据复制和上下文切换,提高了数据传输效率。

1.2 NIO的适用场景

  1. 高并发网络服务: 需要同时处理多个连接,如聊天服务器、游戏服务器等。
  2. 大文件处理: 处理大型文件或需要随机访问文件数据时。
  3. 数据密集型处理: 需要高吞吐量的场景,如数据流处理、日志处理等。
  4. 实时系统: 需要快速响应和低延迟的系统。

2. NIO与IO的对比

传统IO与NIO在设计理念和使用方式上有显著差异,下面是它们的主要区别:

特性传统IO (BIO)NIO
处理方式面向流(Stream Oriented)面向缓冲区(Buffer Oriented)
阻塞特性阻塞式I/O支持非阻塞式I/O
I/O模型一个线程处理一个连接一个线程可处理多个连接
缓冲区无显式缓冲区使用显式缓冲区
API复杂度简单易用较为复杂
适用场景连接数少,逻辑简单高并发,大量连接
数据处理逐字节处理块处理(批量读写)

2.1 代码对比示例

2.1.1 传统IO读取文件
import java.io.FileInputStream;
import java.io.IOException;

public class TraditionalIOExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("example.txt")) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            
            // 读取数据直到文件结束
            while ((bytesRead = fis.read(buffer)) != -1) {
                // 处理读取的数据
                System.out.println(new String(buffer, 0, bytesRead));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
2.1.2 NIO读取文件
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class NIOExample {
    public static void main(String[] args) {
        Path path = Paths.get("example.txt");
        
        try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ)) {
            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            
            // 读取数据到缓冲区
            while (fileChannel.read(buffer) != -1) {
                // 切换到读模式
                buffer.flip();
                
                // 读取缓冲区中的数据
                while (buffer.hasRemaining()) {
                    System.out.print((char) buffer.get());
                }
                
                // 清空缓冲区,准备下一次读取
                buffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3. NIO核心组件

Java NIO由三个核心组件组成,它们共同提供了一个完整的非阻塞I/O解决方案:

3.1 Buffer(缓冲区)

Buffer是NIO中的一个抽象概念,它代表一个特定的原始类型的线性固定长度的数据块。在NIO中,所有数据的读写都必须通过缓冲区进行处理。

缓冲区的特点:

  • 是一个特定基本类型的容器
  • 有固定的大小限制
  • 具有读写状态的概念
  • 支持链式操作
  • 提供了对数据的随机存取

Java NIO提供了以下几种类型的Buffer:

  • ByteBuffer:最常用的缓冲区,操作字节数据
  • CharBuffer:操作字符数据
  • ShortBuffer:操作短整型数据
  • IntBuffer:操作整型数据
  • LongBuffer:操作长整型数据
  • FloatBuffer:操作单精度浮点型数据
  • DoubleBuffer:操作双精度浮点型数据
  • MappedByteBuffer:内存映射文件专用缓冲区

3.2 Channel(通道)

Channel(通道)是NIO中用于读取和写入数据的媒介,类似于传统IO中的流,但有一些重要的区别:

  • 通道可以同时进行读写操作,而流通常是单向的
  • 通道总是从缓冲区读取数据或写入数据到缓冲区
  • 通道可以异步读写数据

主要的Channel实现包括:

  • FileChannel:用于文件读写
  • SocketChannel:用于TCP网络连接读写
  • ServerSocketChannel:用于监听TCP连接请求
  • DatagramChannel:用于UDP网络读写

3.3 Selector(选择器)

Selector是NIO中的一个关键组件,允许单个线程监控多个Channel的状态,从而管理多个网络连接。当Channel上发生读、写或连接事件时,Selector会收到通知,使用单个线程就能处理多个通道的数据。

Selector的主要优点:

  • 使用单个线程处理多个Channel,减少线程创建和上下文切换的开销
  • 有效解决了传统阻塞IO模型中的1:1线程-连接模型的伸缩性问题
  • 特别适合需要处理多个低带宽连接的情况,例如聊天服务器

4. Buffer详解

Buffer是NIO中的核心抽象,所有的数据读写都要通过Buffer完成。深入理解Buffer的工作原理和使用方法是掌握NIO的关键。

4.1 Buffer的基本属性

Buffer类有三个重要的属性,用于跟踪缓冲区的状态:

  1. 容量(capacity): 表示Buffer能够容纳的最大数据量,创建后不能更改。
  2. 限制(limit): 表示Buffer当前能够操作的数据量的限制,不能超过capacity。
  3. 位置(position): 表示Buffer中下一个可读/写的位置索引,不能超过limit。
  4. 标记(mark): 可以临时保存position的值,便于后续回退。

这些属性满足以下条件:

0 <= mark <= position <= limit <= capacity

4.2 Buffer的基本操作

  1. 创建(Allocate): 分配空间创建Buffer对象

    // 创建一个容量为1024字节的ByteBuffer
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    
    // 创建直接缓冲区
    ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
    
    // 通过包装现有数组创建缓冲区
    byte[] array = new byte[1024];
    ByteBuffer wrappedBuffer = ByteBuffer.wrap(array);
    
  2. 放入(Put): 写入数据到Buffer

    // 写入单个字节
    buffer.put((byte) 127);
    
    // 写入字节数组
    byte[] data = "Hello NIO".getBytes();
    buffer.put(data);
    
    // 写入字节数组的一部分
    buffer.put(data, 0, 5);
    
    // 在指定位置写入
    buffer.put(10, (byte) 65); // 在索引10处写入字节值65
    
  3. 翻转(Flip): 从写模式切换到读模式

    // 将limit设置为当前position,并将position设置为0
    buffer.flip();
    
  4. 获取(Get): 从Buffer中读取数据

    // 读取单个字节
    byte b = buffer.get();
    
    // 读取到字节数组
    byte[] array = new byte[buffer.remaining()];
    buffer.get(array);
    
    // 读取一部分到字节数组
    byte[] partial = new byte[10];
    buffer.get(partial, 0, 10);
    
    // 从指定位置读取
    byte value = buffer.get(5); // 读取索引5处的字节
    
  5. 重绕(Rewind): 将position重置为0,不改变limit

    // 准备重新读取Buffer中的数据
    buffer.rewind();
    
  6. 清空(Clear): 准备重新写入数据

    // 将position设置为0,limit设置为capacity
    buffer.clear();
    
  7. 压缩(Compact): 将未读数据移动到Buffer起始位置

    // 将未读数据复制到Buffer开头,position设置为剩余数据长度
    buffer.compact();
    
  8. 标记和重置: 保存position并回退

    // 标记当前position
    buffer.mark();
    
    // 读取一些数据...
    buffer.get();
    buffer.get();
    
    // 重置到先前标记的位置
    buffer.reset();
    

4.3 Buffer状态转换图

Buffer在读写操作中会经历不同的状态转换:

  1. 初始状态: 创建后,position=0,limit=capacity,适合写入数据
  2. 写入数据: 每次put()操作后,position增加
  3. 翻转为读模式: 调用flip()后,limit=position,position=0
  4. 读取数据: 每次get()操作后,position增加
  5. 重新写入:
    • 调用clear():position=0,limit=capacity,丢弃所有内容
    • 调用compact():未读数据移到开头,position设为未读数据长度

4.4 ByteBuffer详细示例

下面是一个综合的ByteBuffer使用示例:

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

public class BufferExample {
    public static void main(String[] args) {
        // 创建一个容量为16字节的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(16);
        
        // 显示初始状态
        printBufferStatus("初始状态", buffer);
        
        // 写入数据
        buffer.put("Hello".getBytes(StandardCharsets.UTF_8));
        printBufferStatus("写入5个字节后", buffer);
        
        // 翻转缓冲区
        buffer.flip();
        printBufferStatus("翻转后(准备读取)", buffer);
        
        // 读取2个字节
        byte[] twoBytes = new byte[2];
        buffer.get(twoBytes);
        System.out.println("读取的两个字节: " + new String(twoBytes, StandardCharsets.UTF_8));
        printBufferStatus("读取2个字节后", buffer);
        
        // 标记当前位置
        buffer.mark();
        printBufferStatus("标记当前位置", buffer);
        
        // 继续读取2个字节
        buffer.get(twoBytes);
        System.out.println("又读取的两个字节: " + new String(twoBytes, StandardCharsets.UTF_8));
        printBufferStatus("再读取2个字节后", buffer);
        
        // 重置到标记位置
        buffer.reset();
        printBufferStatus("重置到之前的标记", buffer);
        
        // 使用remaining()方法确认剩余可读字节数
        byte[] remaining = new byte[buffer.remaining()];
        buffer.get(remaining);
        System.out.println("剩余字节: " + new String(remaining, StandardCharsets.UTF_8));
        printBufferStatus("读取所有剩余字节后", buffer);
        
        // 清空缓冲区,准备重新写入
        buffer.clear();
        printBufferStatus("清空后", buffer);
        
        // 写入更多数据
        buffer.put("NIO Buffer".getBytes(StandardCharsets.UTF_8));
        printBufferStatus("写入新数据后", buffer);
        
        // 翻转并读取部分数据
        buffer.flip();
        byte[] firstFive = new byte[5];
        buffer.get(firstFive);
        System.out.println("读取前5个字节: " + new String(firstFive, StandardCharsets.UTF_8));
        printBufferStatus("读取5个字节后", buffer);
        
        // 使用compact()保留未读部分
        buffer.compact();
        printBufferStatus("压缩后", buffer);
        
        // 继续写入数据
        buffer.put(" Rocks!".getBytes(StandardCharsets.UTF_8));
        printBufferStatus("写入更多数据后", buffer);
        
        // 准备最终读取
        buffer.flip();
        byte[] allData = new byte[buffer.remaining()];
        buffer.get(allData);
        System.out.println("最终数据: " + new String(allData, StandardCharsets.UTF_8));
    }
    
    private static void printBufferStatus(String stage, ByteBuffer buffer) {
        System.out.println("\n===== " + stage + " =====");
        System.out.println("position: " + buffer.position());
        System.out.println("limit: " + buffer.limit());
        System.out.println("capacity: " + buffer.capacity());
        System.out.println("剩余空间: " + buffer.remaining());
    }
}

4.5 直接缓冲区与非直接缓冲区

NIO提供了两种类型的ByteBuffer:

  1. 非直接缓冲区(HeapByteBuffer)

    • 创建方式:ByteBuffer.allocate(capacity)
    • 分配在JVM堆内存中
    • 受GC管理
    • 在进行I/O操作时,可能需要将数据复制到直接缓冲区
    • 优点:分配和回收较快
    • 缺点:需要额外的复制操作
  2. 直接缓冲区(DirectByteBuffer)

    • 创建方式:ByteBuffer.allocateDirect(capacity)
    • 分配在操作系统物理内存中
    • 不受GC直接管理
    • I/O操作更高效,无需复制
    • 优点:更高的I/O性能
    • 缺点:分配和回收成本高

示例:

import java.nio.ByteBuffer;

public class DirectBufferExample {
    public static void main(String[] args) {
        // 创建直接缓冲区
        ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
        System.out.println("Direct buffer: " + directBuffer.isDirect());
        
        // 创建非直接缓冲区
        ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
        System.out.println("Heap buffer: " + heapBuffer.isDirect());
        
        // 使用直接缓冲区
        directBuffer.put((byte) 'A');
        directBuffer.put((byte) 'B');
        directBuffer.put((byte) 'C');
        
        directBuffer.flip();
        
        while(directBuffer.hasRemaining()) {
            System.out.print((char) directBuffer.get());
        }
    }
}

4.6 其他类型的缓冲区

除了ByteBuffer外,还有其他原始类型的缓冲区:

import java.nio.*;

public class DifferentBufferTypes {
    public static void main(String[] args) {
        // IntBuffer示例
        IntBuffer intBuffer = IntBuffer.allocate(10);
        for (int i = 0; i < 5; i++) {
            intBuffer.put(i * 100);
        }
        
        intBuffer.flip();
        while (intBuffer.hasRemaining()) {
            System.out.println("IntBuffer: " + intBuffer.get());
        }
        
        // CharBuffer示例
        CharBuffer charBuffer = CharBuffer.allocate(10);
        charBuffer.put("Hello");
        
        charBuffer.flip();
        while (charBuffer.hasRemaining()) {
            System.out.print(charBuffer.get());
        }
        System.out.println();
        
        // FloatBuffer示例
        FloatBuffer floatBuffer = FloatBuffer.allocate(5);
        for (float f = 0.0f; f < 1.0f; f += 0.2f) {
            floatBuffer.put(f);
        }
        
        floatBuffer.flip();
        while (floatBuffer.hasRemaining()) {
            System.out.println("FloatBuffer: " + floatBuffer.get());
        }
    }
}

4.7 视图缓冲区(View Buffers)

ByteBuffer可以创建其他类型的视图缓冲区,允许以不同类型访问相同的数据:

import java.nio.*;

public class ViewBufferExample {
    public static void main(String[] args) {
        // 创建一个ByteBuffer,容量为8个字节(足够存储一个long或double值)
        ByteBuffer byteBuffer = ByteBuffer.allocate(8);
        
        // 写入一些数据
        for (byte i = 0; i < 8; i++) {
            byteBuffer.put(i);
        }
        
        // 翻转准备读取
        byteBuffer.flip();
        
        // 创建不同类型的视图缓冲区
        IntBuffer intView = byteBuffer.asIntBuffer();
        System.out.println("IntBuffer容量: " + intView.capacity()); // 应该是2
        
        // 读取视图中的整数值
        System.out.println("第一个int值: " + intView.get(0)); // 读取前4个字节组成的int
        System.out.println("第二个int值: " + intView.get(1)); // 读取后4个字节组成的int
        
        // 重置ByteBuffer位置
        byteBuffer.rewind();
        
        // 创建LongBuffer视图
        LongBuffer longView = byteBuffer.asLongBuffer();
        System.out.println("LongBuffer容量: " + longView.capacity()); // 应该是1
        System.out.println("Long值: " + longView.get(0)); // 读取所有8个字节组成的long
        
        // 通过视图修改原始数据
        byteBuffer.rewind();
        ShortBuffer shortView = byteBuffer.asShortBuffer();
        System.out.println("修改前的第一个short值: " + shortView.get(0));
        
        shortView.put(0, (short) 9999);
        System.out.println("修改后的第一个short值: " + shortView.get(0));
        
        // 查看修改是否影响了原始字节
        byteBuffer.rewind();
        System.out.print("修改后的字节: ");
        while (byteBuffer.hasRemaining()) {
            System.out.print(byteBuffer.get() + " ");
        }
    }
}

5. Channel详解

Channel(通道)是NIO中的另一个核心组件,它代表了与硬件设备、文件、网络连接等I/O源的连接。不同于传统IO中的流,Channel是双向的,可以同时进行读写操作。

5.1 Channel的主要特性

  1. 双向性:Channel支持读和写操作
  2. 异步性:支持非阻塞I/O操作
  3. 直接缓冲区访问:可以直接使用底层操作系统的I/O操作
  4. 可中断性:支持中断I/O操作

5.2 Channel的主要实现类

Java NIO提供了多种Channel实现,适用于不同类型的I/O操作:

  1. FileChannel:用于文件读写操作
  2. SocketChannel:用于TCP网络连接的客户端
  3. ServerSocketChannel:用于TCP网络连接的服务器端
  4. DatagramChannel:用于UDP网络连接
  5. Pipe.SinkChannelPipe.SourceChannel:用于线程间通信

5.3 FileChannel详解

FileChannel是用于文件操作的通道,它提供了文件的读取、写入、映射和操作文件属性等功能。

5.3.1 FileChannel的创建方式
// 方式1:通过FileInputStream、FileOutputStream或RandomAccessFile获取
FileInputStream fis = new FileInputStream("input.txt");
FileChannel inChannel = fis.getChannel();

FileOutputStream fos = new FileOutputStream("output.txt");
FileChannel outChannel = fos.getChannel();

RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");
FileChannel channel = raf.getChannel();

// 方式2:使用Files工具类(Java 7+)
Path path = Paths.get("example.txt");
FileChannel channel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE);
5.3.2 FileChannel基本读写
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class FileChannelExample {
    public static void main(String[] args) {
        // 写文件示例
        writeExample();
        
        // 读文件示例
        readExample();
        
        // 文件复制示例
        copyExample();
    }
    
    private static void writeExample() {
        String data = "Hello, FileChannel!";
        ByteBuffer buffer = ByteBuffer.wrap(data.getBytes());
        
        try (FileChannel channel = FileChannel.open(
                Paths.get("output.txt"),
                StandardOpenOption.CREATE,
                StandardOpenOption.WRITE)) {
            
            // 写入数据
            int bytesWritten = channel.write(buffer);
            System.out.println("写入了 " + bytesWritten + " 字节");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private static void readExample() {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        try (FileChannel channel = FileChannel.open(
                Paths.get("output.txt"),
                StandardOpenOption.READ)) {
            
            // 读取数据到缓冲区
            int bytesRead = channel.read(buffer);
            System.out.println("读取了 " + bytesRead + " 字节");
            
            // 转换为读模式
            buffer.flip();
            
            // 读取缓冲区数据并输出
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            System.out.println("读取的内容: " + new String(data));
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private static void copyExample() {
        try (FileChannel srcChannel = FileChannel.open(
                Paths.get("output.txt"), StandardOpenOption.READ);
             FileChannel destChannel = FileChannel.open(
                Paths.get("copy.txt"),
                StandardOpenOption.CREATE,
                StandardOpenOption.WRITE)) {
            
            // 方法1:使用transferTo
            long bytesTransferred = srcChannel.transferTo(
                    0, srcChannel.size(), destChannel);
            
            // 方法2:使用transferFrom(替代方案)
            // long bytesTransferred = destChannel.transferFrom(
            //        srcChannel, 0, srcChannel.size());
            
            System.out.println("复制了 " + bytesTransferred + " 字节");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
5.3.3 FileChannel的高级特性
  1. 文件锁定:允许在文件的特定区域上获取共享或独占锁
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class FileLockExample {
    public static void main(String[] args) {
        Path path = Paths.get("lockFile.txt");
        
        try (FileChannel channel = FileChannel.open(
                path, StandardOpenOption.CREATE, 
                StandardOpenOption.WRITE, 
                StandardOpenOption.READ)) {
            
            // 获取独占锁(true表示共享锁,false表示独占锁)
            System.out.println("尝试获取文件锁...");
            try (FileLock lock = channel.lock(0, Long.MAX_VALUE, false)) {
                System.out.println("获取到文件锁: " + lock);
                System.out.println("锁是否共享: " + lock.isShared());
                
                // 在此处安全地修改文件
                System.out.println("模拟文件操作...");
                Thread.sleep(5000); // 模拟处理时间
                
                // 锁自动释放(通过try-with-resources)
            }
            System.out.println("文件锁已释放");
            
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  1. 内存映射文件:将文件区域直接映射到内存中,提供更高效的I/O
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class MemoryMappedFileExample {
    public static void main(String[] args) {
        try {
            // 创建一个10MB的文件
            long fileSize = 10 * 1024 * 1024; // 10MB
            
            // 创建或打开文件
            FileChannel channel = FileChannel.open(
                    Paths.get("mappedFile.dat"),
                    StandardOpenOption.CREATE,
                    StandardOpenOption.READ,
                    StandardOpenOption.WRITE);
            
            // 将文件映射到内存
            MappedByteBuffer buffer = channel.map(MapMode.READ_WRITE, 0, fileSize);
            
            // 写入一些数据
            System.out.println("写入数据到内存映射文件...");
            for (int i = 0; i < 100; i++) {
                buffer.putInt(i);
            }
            
            // 确保数据写入
            buffer.force();
            
            // 重置位置以便读取
            buffer.flip();
            
            // 读取前10个整数
            System.out.println("从内存映射文件读取数据...");
            for (int i = 0; i < 10; i++) {
                System.out.println("读取值: " + buffer.getInt());
            }
            
            // 关闭通道
            channel.close();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  1. 随机访问:可以通过position方法在文件中自由移动
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class RandomAccessExample {
    public static void main(String[] args) {
        try (FileChannel channel = FileChannel.open(
                Paths.get("random.txt"),
                StandardOpenOption.CREATE,
                StandardOpenOption.READ,
                StandardOpenOption.WRITE)) {
            
            // 写入一些数据
            ByteBuffer buffer = ByteBuffer.allocate(4);
            
            for (int i = 0; i < 10; i++) {
                buffer.clear();
                buffer.putInt(i);
                buffer.flip();
                channel.write(buffer);
            }
            
            // 随机访问读取
            // 移动到第5个整数的位置 (每个int占4字节)
            channel.position(5 * 4);
            
            // 读取第5个整数
            buffer.clear();
            channel.read(buffer);
            buffer.flip();
            
            System.out.println("第5个整数值为: " + buffer.getInt());
            
            // 移动到第2个整数的位置
            channel.position(2 * 4);
            
            // 写入新值覆盖第2个整数
            buffer.clear();
            buffer.putInt(99);
            buffer.flip();
            channel.write(buffer);
            
            // 重新读取确认修改
            channel.position(2 * 4);
            buffer.clear();
            channel.read(buffer);
            buffer.flip();
            
            System.out.println("修改后的第2个整数值为: " + buffer.getInt());
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5.4 SocketChannel与ServerSocketChannel详解

SocketChannel和ServerSocketChannel用于实现TCP网络通信,与传统Socket和ServerSocket类似,但支持非阻塞模式。

5.4.1 阻塞式Socket通信

首先,让我们看看阻塞式Socket通信的实现:

服务器端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class BlockingServerExample {
    public static void main(String[] args) {
        try {
            // 创建ServerSocketChannel
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            
            // 绑定到特定端口
            serverChannel.socket().bind(new InetSocketAddress(8080));
            System.out.println("服务器已启动,等待连接...");
            
            while (true) {
                // 接受客户端连接(阻塞操作)
                SocketChannel clientChannel = serverChannel.accept();
                System.out.println("客户端已连接: " + clientChannel.getRemoteAddress());
                
                // 处理客户端请求
                handleClient(clientChannel);
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private static void handleClient(SocketChannel clientChannel) throws IOException {
        // 创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        try {
            // 读取客户端数据
            int bytesRead = clientChannel.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);
                
                // 回复客户端
                String response = "服务器已收到消息: " + message;
                buffer.clear();
                buffer.put(response.getBytes());
                buffer.flip();
                clientChannel.write(buffer);
            }
            
            // 关闭连接
            clientChannel.close();
            
        } catch (IOException e) {
            clientChannel.close();
            e.printStackTrace();
        }
    }
}

客户端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class BlockingClientExample {
    public static void main(String[] args) {
        try {
            // 创建SocketChannel
            SocketChannel socketChannel = SocketChannel.open();
            
            // 连接到服务器
            socketChannel.connect(new InetSocketAddress("localhost", 8080));
            System.out.println("已连接到服务器");
            
            // 发送消息
            String message = "Hello from NIO Client!";
            ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
            socketChannel.write(buffer);
            System.out.println("消息已发送");
            
            // 接收响应
            buffer.clear();
            int bytesRead = socketChannel.read(buffer);
            
            if (bytesRead > 0) {
                buffer.flip();
                byte[] data = new byte[buffer.remaining()];
                buffer.get(data);
                System.out.println("服务器响应: " + new String(data));
            }
            
            // 关闭连接
            socketChannel.close();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
5.4.2 非阻塞式Socket通信

下面是非阻塞式Socket通信的实现(不使用Selector):

服务器端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class NonBlockingServerExample {
    public static void main(String[] args) {
        try {
            // 创建ServerSocketChannel
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            
            // 绑定到特定端口
            serverChannel.socket().bind(new InetSocketAddress(8080));
            
            // 设置为非阻塞模式
            serverChannel.configureBlocking(false);
            
            System.out.println("非阻塞服务器已启动,等待连接...");
            
            // 保存客户端连接的列表
            List<SocketChannel> clientChannels = new ArrayList<>();
            
            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            
            while (true) {
                // 非阻塞式接受连接
                SocketChannel clientChannel = serverChannel.accept();
                
                if (clientChannel != null) {
                    // 新的客户端连接
                    System.out.println("新客户端已连接: " + clientChannel.getRemoteAddress());
                    
                    // 将客户端通道设置为非阻塞
                    clientChannel.configureBlocking(false);
                    
                    // 添加到客户端列表
                    clientChannels.add(clientChannel);
                }
                
                // 处理已有客户端的数据
                Iterator<SocketChannel> iterator = clientChannels.iterator();
                while (iterator.hasNext()) {
                    SocketChannel channel = iterator.next();
                    
                    buffer.clear();
                    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 + " 来自 " + channel.getRemoteAddress());
                        
                        // 回复客户端
                        String response = "服务器已收到消息: " + message;
                        buffer.clear();
                        buffer.put(response.getBytes());
                        buffer.flip();
                        channel.write(buffer);
                    } else if (bytesRead < 0) {
                        // 客户端断开连接
                        System.out.println("客户端断开连接: " + channel.getRemoteAddress());
                        channel.close();
                        iterator.remove();
                    }
                    // 如果bytesRead为0,表示暂时没有数据可读
                }
                
                // 添加短暂休眠,减少CPU使用率
                Thread.sleep(100);
            }
            
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

客户端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NonBlockingClientExample {
    public static void main(String[] args) {
        try {
            // 创建SocketChannel
            SocketChannel socketChannel = SocketChannel.open();
            
            // 设置为非阻塞模式
            socketChannel.configureBlocking(false);
            
            // 连接到服务器
            socketChannel.connect(new InetSocketAddress("localhost", 8080));
            
            // 等待连接完成
            while (!socketChannel.finishConnect()) {
                System.out.println("正在连接服务器...");
                Thread.sleep(100);
            }
            
            System.out.println("已连接到服务器");
            
            // 发送消息
            String message = "Hello from Non-blocking NIO Client!";
            ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
            
            // 确保所有数据都发送出去
            while (buffer.hasRemaining()) {
                socketChannel.write(buffer);
            }
            
            System.out.println("消息已发送");
            
            // 接收响应
            buffer = ByteBuffer.allocate(1024);
            boolean received = false;
            
            while (!received) {
                buffer.clear();
                int bytesRead = socketChannel.read(buffer);
                
                if (bytesRead > 0) {
                    buffer.flip();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    System.out.println("服务器响应: " + new String(data));
                    received = true;
                } else if (bytesRead < 0) {
                    // 服务器关闭连接
                    System.out.println("服务器关闭了连接");
                    break;
                }
                
                // 短暂休眠
                Thread.sleep(100);
            }
            
            // 关闭连接
            socketChannel.close();
            
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

5.5 DatagramChannel详解

DatagramChannel用于UDP网络通信,以下是其基本用法示例:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;

public class DatagramChannelExample {
    public static void main(String[] args) throws IOException, InterruptedException {
        // 启动接收者线程
        new Thread(DatagramChannelExample::runReceiver).start();
        
        // 等待接收者启动
        Thread.sleep(1000);
        
        // 启动发送者
        runSender();
    }
    
    private static void runSender() {
        try {
            // 创建DatagramChannel
            DatagramChannel channel = DatagramChannel.open();
            
            // 准备发送的数据
            String message = "Hello DatagramChannel!";
            ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
            
            // 发送数据包
            InetSocketAddress receiverAddress = new InetSocketAddress("localhost", 9999);
            channel.send(buffer, receiverAddress);
            System.out.println("发送方: 数据已发送");
            
            // 关闭通道
            channel.close();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private static void runReceiver() {
        try {
            // 创建DatagramChannel
            DatagramChannel channel = DatagramChannel.open();
            
            // 绑定到特定端口
            channel.bind(new InetSocketAddress(9999));
            
            // 创建接收缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            
            System.out.println("接收方: 等待数据...");
            
            // 接收数据
            InetSocketAddress senderAddress = (InetSocketAddress) channel.receive(buffer);
            
            // 处理接收到的数据
            buffer.flip();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            String message = new String(data);
            
            System.out.println("接收方: 收到来自 " + senderAddress + " 的消息: " + message);
            
            // 关闭通道
            channel.close();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5.6 使用Pipe进行线程间通信

Pipe是两个线程之间的单向数据连接,它有一个source通道和一个sink通道。数据会被写入sink通道,然后从source通道读取。

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Pipe;

public class PipeExample {
    public static void main(String[] args) throws IOException {
        // 创建管道
        Pipe pipe = Pipe.open();
        
        // 启动写入线程
        new Thread(() -> writeData(pipe)).start();
        
        // 从管道读取数据
        readData(pipe);
    }
    
    private static void writeData(Pipe pipe) {
        try {
            // 获取sink通道
            Pipe.SinkChannel sinkChannel = pipe.sink();
            
            // 准备写入的数据
            String data = "Hello through the pipe!";
            ByteBuffer buffer = ByteBuffer.wrap(data.getBytes());
            
            // 写入数据
            sinkChannel.write(buffer);
            System.out.println("写入线程: 数据已写入");
            
            // 关闭sink通道
            sinkChannel.close();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private static void readData(Pipe pipe) {
        try {
            // 获取source通道
            Pipe.SourceChannel sourceChannel = pipe.source();
            
            // 准备读取数据的缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            
            // 读取数据
            int bytesRead = sourceChannel.read(buffer);
            
            // 处理读取的数据
            buffer.flip();
            byte[] data = new byte[bytesRead];
            buffer.get(data);
            
            System.out.println("读取线程: 收到数据: " + new String(data));
            
            // 关闭source通道
            sourceChannel.close();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5.7 Channel间数据传输

有时需要将数据从一个Channel传输到另一个Channel,NIO提供了高效的传输方法:

import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class ChannelTransferExample {
    public static void main(String[] args) {
        Path sourcePath = Paths.get("source.txt");
        Path targetPath = Paths.get("target.txt");
        
        try (FileChannel sourceChannel = FileChannel.open(sourcePath, StandardOpenOption.READ);
             FileChannel targetChannel = FileChannel.open(targetPath, 
                                                         StandardOpenOption.CREATE, 
                                                         StandardOpenOption.WRITE)) {
            
            // 获取源文件大小
            long size = sourceChannel.size();
            
            // 使用transferTo方法
            long transferred1 = sourceChannel.transferTo(0, size, targetChannel);
            System.out.println("使用transferTo传输了 " + transferred1 + " 字节");
            
            // 如果文件很大,可能需要分多次传输
            long position = 0;
            long count = size;
            while (position < size) {
                long transferred = sourceChannel.transferTo(position, count, targetChannel);
                if (transferred > 0) {
                    position += transferred;
                    count -= transferred;
                }
            }
            
            // 或者使用transferFrom方法
            // 重置目标文件大小
            targetChannel.truncate(0);
            
            long transferred2 = targetChannel.transferFrom(sourceChannel, 0, size);
            System.out.println("使用transferFrom传输了 " + transferred2 + " 字节");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5.8 注意事项和最佳实践

使用Channel时应注意以下几点:

  1. 关闭Channel:Channel使用完毕后必须关闭,最好使用try-with-resources语句自动关闭。

  2. 异常处理:IO操作容易产生异常,确保对异常进行适当处理。

  3. 缓冲区管理:适当选择缓冲区大小,过大的缓冲区会浪费内存,过小的缓冲区会导致频繁IO操作。

  4. 直接缓冲区vs堆缓冲区:根据场景选择合适的缓冲区类型。

  5. 非阻塞模式:非阻塞模式可以提高吞吐量,但编程复杂度也会增加,通常应与Selector结合使用。

  6. 传输数据:尽量使用transferTo和transferFrom方法直接在Channel间传输数据,这比手动读写效率更高。

  7. 内存映射文件:处理大文件时,考虑使用内存映射文件提高效率。

6. Selector详解

Selector(选择器)是Java NIO中一个关键组件,它使得单个线程能够监控多个Channel的I/O状态。当Channel上发生读、写或连接事件时,Selector会通知程序进行相应处理。使用Selector可以构建高效的多路复用I/O程序,尤其适合需要管理多个连接但不想为每个连接创建线程的情况。

6.1 Selector的工作原理

Selector通过不断轮询已注册的Channel来判断是否有I/O事件发生。当有事件发生时,会返回相关的SelectionKey集合,程序可以通过这些SelectionKey来获取Channel并进行操作。

以下是Selector的核心工作原理:

  1. 创建Selector对象
  2. 将Channel注册到Selector上,并指定关注的事件类型
  3. 调用Selector的select()方法等待事件发生
  4. 获取发生事件的SelectionKey集合,并处理
  5. 根据需要更新SelectionKey的事件关注集
  6. 重复步骤3-5进行事件循环

6.2 Selector的事件类型

Channel注册到Selector时需指定关注的事件类型,Java NIO定义了四种事件类型:

  1. OP_READ (SelectionKey.OP_READ): 通道中有数据可读
  2. OP_WRITE (SelectionKey.OP_WRITE): 通道可写数据
  3. OP_CONNECT (SelectionKey.OP_CONNECT): 通道成功建立连接
  4. OP_ACCEPT (SelectionKey.OP_ACCEPT): 接受新的连接

这些事件可以通过位操作符组合使用,例如:SelectionKey.OP_READ | SelectionKey.OP_WRITE

6.3 Selector的基本使用流程

import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class SelectorBasics {
    public static void main(String[] args) throws IOException {
        // 1. 创建Selector
        Selector selector = Selector.open();
        
        // 2. 创建并配置通道(例如ServerSocketChannel)
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false); // 通道必须设置为非阻塞模式
        
        // 3. 将通道注册到Selector上,并指定关注的事件
        SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        // 4. 开始事件循环
        while (true) {
            // 等待事件发生,返回发生事件的通道数量
            int readyChannels = selector.select();
            
            if (readyChannels == 0) {
                continue; // 没有事件发生,继续等待
            }
            
            // 获取发生事件的SelectionKey集合
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            
            // 处理每个发生事件的通道
            while (keyIterator.hasNext()) {
                SelectionKey selectedKey = keyIterator.next();
                
                // 处理各种事件
                if (selectedKey.isAcceptable()) {
                    // 处理接受连接事件
                    // ...
                } else if (selectedKey.isConnectable()) {
                    // 处理连接建立事件
                    // ...
                } else if (selectedKey.isReadable()) {
                    // 处理读事件
                    // ...
                } else if (selectedKey.isWritable()) {
                    // 处理写事件
                    // ...
                }
                
                // 从集合中移除已处理的SelectionKey
                keyIterator.remove();
            }
        }
    }
}

6.4 使用Selector实现多路复用服务器

下面是一个完整的使用Selector实现多客户端通信的TCP服务器示例:

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.util.Iterator;
import java.util.Set;

public class NIOMultiplexingServer {
    private static final int BUFFER_SIZE = 1024;
    private static final int PORT = 8080;
    
    public static void main(String[] args) {
        try {
            // 创建选择器
            Selector selector = Selector.open();
            
            // 创建服务器通道
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            serverChannel.configureBlocking(false);
            serverChannel.socket().bind(new InetSocketAddress(PORT));
            
            // 将服务器通道注册到选择器,关注接受连接事件
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            
            System.out.println("服务器已启动,监听端口: " + PORT);
            
            // 事件循环
            while (true) {
                // 阻塞等待事件发生
                selector.select();
                
                // 获取发生事件的SelectionKey集合
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
                
                // 处理每个事件
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    
                    try {
                        // 处理接受连接事件
                        if (key.isAcceptable()) {
                            handleAccept(key, selector);
                        }
                        
                        // 处理读事件
                        if (key.isReadable()) {
                            handleRead(key);
                        }
                        
                        // 处理写事件
                        if (key.isWritable()) {
                            handleWrite(key);
                        }
                    } catch (IOException e) {
                        // 处理客户端断开连接等异常
                        System.out.println("连接异常: " + e.getMessage());
                        key.cancel();
                        try {
                            key.channel().close();
                        } catch (IOException ex) {
                            ex.printStackTrace();
                        }
                    }
                    
                    // 从集合中移除已处理的事件
                    keyIterator.remove();
                }
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // 处理接受连接事件
    private 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());
        
        // 创建用于读写的缓冲区,并附加到SelectionKey上
        ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE);
        ByteBuffer writeBuffer = ByteBuffer.allocate(BUFFER_SIZE);
        
        // 注册到选择器,并关注读事件
        SelectionKey clientKey = clientChannel.register(selector, SelectionKey.OP_READ);
        
        // 将缓冲区附加到SelectionKey上,方便后续使用
        clientKey.attach(new Buffers(readBuffer, writeBuffer));
    }
    
    // 处理读事件
    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        Buffers buffers = (Buffers) key.attachment();
        ByteBuffer readBuffer = buffers.getReadBuffer();
        ByteBuffer writeBuffer = buffers.getWriteBuffer();
        
        // 读取数据
        int bytesRead = channel.read(readBuffer);
        
        if (bytesRead == -1) {
            // 客户端断开连接
            System.out.println("客户端断开连接: " + channel.getRemoteAddress());
            channel.close();
            key.cancel();
            return;
        }
        
        // 处理接收到的数据
        if (bytesRead > 0) {
            // 回显数据
            readBuffer.flip();
            
            byte[] data = new byte[readBuffer.remaining()];
            readBuffer.get(data);
            String message = new String(data);
            
            System.out.println("收到消息: " + message + " 来自 " + channel.getRemoteAddress());
            
            // 准备回复数据
            writeBuffer.clear();
            writeBuffer.put(("回声: " + message).getBytes());
            writeBuffer.flip();
            
            // 写入数据(可能无法一次写完)
            channel.write(writeBuffer);
            
            // 如果写缓冲区中还有数据,注册写事件
            if (writeBuffer.hasRemaining()) {
                key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
            }
            
            // 清空读缓冲区,准备下一次读取
            readBuffer.clear();
        }
    }
    
    // 处理写事件
    private static void handleWrite(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        Buffers buffers = (Buffers) key.attachment();
        ByteBuffer writeBuffer = buffers.getWriteBuffer();
        
        // 继续写入之前未完成的数据
        channel.write(writeBuffer);
        
        // 如果数据已全部写入,则不再关注写事件
        if (!writeBuffer.hasRemaining()) {
            key.interestOps(SelectionKey.OP_READ);
        }
    }
    
    // 用于存储读写缓冲区的辅助类
    static class Buffers {
        private ByteBuffer readBuffer;
        private ByteBuffer writeBuffer;
        
        public Buffers(ByteBuffer readBuffer, ByteBuffer writeBuffer) {
            this.readBuffer = readBuffer;
            this.writeBuffer = writeBuffer;
        }
        
        public ByteBuffer getReadBuffer() {
            return readBuffer;
        }
        
        public ByteBuffer getWriteBuffer() {
            return writeBuffer;
        }
    }
}

相应的客户端代码可以使用前面提到的阻塞式或非阻塞式SocketChannel客户端。

6.5 SelectionKey详解

SelectionKey是Selector与Channel之间注册关系的表示,包含了以下重要信息:

  1. 通道(Channel): 获取关联的Channel
  2. 选择器(Selector): 获取关联的Selector
  3. 兴趣集(Interest Set): 表示关注的事件类型
  4. 就绪集(Ready Set): 表示已经就绪的事件类型
  5. 附加对象(Attachment): 可以附加任意对象,用于在处理事件时传递信息
6.5.1 SelectionKey常用方法
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

// 获取通道
Channel channel = key.channel();

// 获取选择器
Selector selector = key.selector();

// 获取附加对象
Object attachment = key.attachment();

// 在注册时附加对象
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, attachmentObject);

// 后续附加对象
key.attach(newAttachmentObject);

// 获取和修改兴趣集
int interestSet = key.interestOps();
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

// 检查事件就绪状态
boolean isAcceptable = key.isAcceptable(); // (key.readyOps() & SelectionKey.OP_ACCEPT) != 0
boolean isConnectable = key.isConnectable(); // (key.readyOps() & SelectionKey.OP_CONNECT) != 0
boolean isReadable = key.isReadable(); // (key.readyOps() & SelectionKey.OP_READ) != 0
boolean isWritable = key.isWritable(); // (key.readyOps() & SelectionKey.OP_WRITE) != 0

// 取消注册关系
key.cancel();

6.6 Selector高级主题

6.6.1 wakeup()方法

当一个线程被阻塞在selector.select()调用上,调用selector.wakeup()方法会让select()方法立即返回。这个特性可以用来安全地终止Selector线程:

import java.io.IOException;
import java.nio.channels.Selector;

public class SelectorWakeupExample {
    private static Selector selector;
    
    public static void main(String[] args) throws IOException, InterruptedException {
        selector = Selector.open();
        
        // 启动新线程运行Selector
        Thread selectorThread = new Thread(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("等待事件...");
                    selector.select();
                    // 处理事件...
                    System.out.println("处理完事件");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("Selector线程退出");
        });
        
        selectorThread.start();
        
        // 让Selector线程运行一段时间
        Thread.sleep(3000);
        
        // 唤醒阻塞的select()调用
        System.out.println("唤醒Selector");
        selector.wakeup();
        
        // 中断线程
        selectorThread.interrupt();
        
        // 等待线程终止
        selectorThread.join();
        
        // 关闭Selector
        selector.close();
    }
}
6.6.2 selectNow()方法

Selector提供了非阻塞的select方法selectNow(),该方法立即返回而不是阻塞等待:

// 非阻塞select调用
int readyChannels = selector.selectNow();

这在某些场景下很有用,例如需要定期执行其他操作的情况。

6.6.3 多Selector管理

对于复杂的应用程序,可能需要使用多个Selector来分离不同类型的处理,例如:

import java.io.IOException;
import java.nio.channels.Selector;

public class MultiSelectorExample {
    public static void main(String[] args) throws IOException {
        // 创建两个Selector
        Selector selector1 = Selector.open();
        Selector selector2 = Selector.open();
        
        // 将通道注册到不同的Selector上
        channel1.register(selector1, SelectionKey.OP_READ);
        channel2.register(selector2, SelectionKey.OP_READ);
        
        // 在不同的Selector上处理不同的通道
        selector1.select();
        selector2.select();
    }
}

6.7 常见问题与调试技巧

使用Selector时可能遇到的常见问题和解决方法:

  1. Selector空轮询:在某些操作系统和JDK版本中,Selector可能会陷入空轮询,导致CPU使用率飙升。

    • 解决方案:设置select操作的超时时间
    • 检测空轮询次数,超过阈值后重建Selector
  2. 多线程访问Buffer或Channel:Buffer和大多数Channel实现不是线程安全的,多线程访问会导致不可预期的结果。

    • 解决方案:每个线程使用独立的Buffer
    • 使用同步机制如锁保护共享资源
    • 考虑使用线程安全的数据结构如ConcurrentLinkedQueue传递数据
  3. Buffer的position/limit/capacity混淆:Buffer的三个属性(position/limit/capacity)容易混淆,导致读写错误。

    • 解决方案:始终使用flip()切换读写模式
    • 使用clear()或compact()准备写入
    • 使用方法rewind()重新读取
    • 封装提供更简单的API
  4. Selector事件处理顺序:Selector事件处理顺序可能与预期不一致,导致某些事件被忽略。

    • 解决方案:按照连接、读、写的顺序处理事件通常更合理
  5. Selector性能:在大量连接情况下,select()可能会变慢,考虑使用适当的超时

    • 解决方案:为长时间运行的select()操作设置合理的超时时间
  6. 异常处理:妥善处理I/O异常,尤其是远程对等方异常断开连接时。

    • 解决方案:对不同类型的异常采取不同的处理策略
    • 对于可恢复的I/O错误,考虑重试
    • 正确记录异常信息,便于诊断
    • 在高并发环境中,避免由于单个连接异常影响整个服务
  7. 资源清理:确保关闭不再使用的通道和选择器,以释放系统资源。

    • 解决方案:使用try-with-resources语句自动关闭资源
    • 确保在出现异常时关闭资源
    • 在对象不再使用时显式调用close()方法
  8. 取消键:当通道关闭时,相应的SelectionKey会自动取消,但最好显式调用cancel()。

    • 解决方案:在处理完SelectionKey后,从selectedKeys集合中移除,否则下次select()调用仍会包含这些键。
  9. 使用attachment:利用SelectionKey的attachment机制存储连接相关信息,避免使用全局Map等结构,提高性能。

    • 解决方案:在注册时附加对象,后续处理时获取

7. 文件操作实战

NIO提供了强大的文件操作功能,特别是通过Path、Files和FileChannel类来操作文件系统。

7.1 Path与Paths

Path接口代表了文件系统中的路径,是java.nio.file包的核心部分。

7.2 Files工具类

Files是JDK 7引入的工具类,提供了大量静态方法来操作文件。

7.3 文件读写实战

7.3.1 使用Files读写小文件
7.3.2 使用Channel读写大文件
7.3.3 内存映射文件操作

7.4 文件属性操作

NIO.2提供了丰富的文件属性操作方法:

7.5 文件变更监控

WatchService允许监控目录变化,对于需要实时响应文件变化的应用非常有用。

7.6 文件锁定

FileChannel提供了锁定文件区域的功能,对于多进程并发操作同一个文件非常有用。

8. 网络编程实战

NIO在网络编程领域的应用非常广泛,以下是一些常见的应用场景和示例。

8.1 构建高性能TCP服务器

8.1.1 基于Reactor模式的服务器
8.1.2 处理粘包和拆包问题
8.1.3 优化服务器性能

8.2 构建UDP应用

使用DatagramChannel可以构建UDP应用,适用于实时性要求高但允许丢包的场景:

8.3 基于NIO的HTTP服务器

实现一个简单的基于NIO的HTTP服务器:

8.4 使用SSL/TLS实现安全通信

NIO也可以结合SSL/TLS实现加密通信:

8.5 异步编程模型AsynchronousSocketChannel

Java 7引入了真正的异步I/O API,包括AsynchronousSocketChannel和AsynchronousServerSocketChannel:

9. 高级主题

9.1 Buffer的内存分配策略

9.1.1 堆内存vs直接内存
9.1.2 Buffer池化技术

9.2 零拷贝技术

零拷贝是一种减少数据复制操作的技术,可以显著提高I/O性能:

9.3 NIO与多线程

在多线程环境中使用NIO需要特别注意以下几点:

9.4 使用ByteBuffer的编码和解码

使用ByteBuffer处理字符编码:

9.5 自定义协议开发

使用NIO开发自定义协议的步骤:

9.6 NIO框架对比

市场上有多种基于NIO的框架,如Netty、MINA、Grizzly等,以下是它们的特点和适用场景:

10. 常见问题与最佳实践

10.1 性能调优

10.1.1 Buffer大小的选择
10.1.2 选择合适的I/O模型
10.1.3 避免系统调用瓶颈

10.2 常见陷阱与解决方案

10.2.1 Buffer操作错误
10.2.2 Selector空循环
10.2.3 Channel关闭与资源泄漏

10.3 调试技巧

10.3.1 日志记录Buffer和Channel状态
10.3.2 监控Selector事件

public class FastFileCopy {
public static void main(String[] args) {
if (args.length != 2) {
System.out.println(“用法: java FastFileCopy <源文件> <目标文件>”);
return;
}

    String source = args[0];
    String target = args[1];
    
    copyFile(source, target);
}

private static void copyFile(String source, String target) {
    Path sourcePath = Paths.get(source);
    Path targetPath = Paths.get(target);
    
    try (FileChannel sourceChannel = FileChannel.open(sourcePath, StandardOpenOption.READ);
         FileChannel targetChannel = FileChannel.open(targetPath, 
                 StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
        
        long fileSize = sourceChannel.size();
        long position = 0;
        long bytesTransferred;
        
        // 确保所有字节都被复制
        while (position < fileSize) {
            // transferTo方法有最大传输限制,可能需要多次调用
            bytesTransferred = sourceChannel.transferTo(
                    position, fileSize - position, targetChannel);
            
            if (bytesTransferred <= 0) {
                break; // 防止无限循环
            }
            
            position += bytesTransferred;
        }
        
        System.out.println("文件复制完成! 复制了 " + position + " 字节");
        
    } catch (IOException e) {
        System.err.println("复制文件时出错: " + e.getMessage());
        e.printStackTrace();
    }
}

}


### 使用内存映射文件处理大文件

内存映射文件是处理大文件的有效方式,可以直接在内存中操作文件数据:

```java
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class MemoryMappedFileExample {
    public static void main(String[] args) {
        // 指定要处理的文件
        Path path = Paths.get("large_file.dat");
        
        // 创建或清空文件(如果需要)
        createEmptyFile(path, 1024 * 1024 * 100); // 创建100MB文件
        
        // 使用内存映射文件写入数据
        writeToMappedFile(path);
        
        // 使用内存映射文件读取数据
        readFromMappedFile(path);
    }
    
    private static void createEmptyFile(Path path, long size) {
        try (FileChannel channel = FileChannel.open(path, 
                StandardOpenOption.CREATE, 
                StandardOpenOption.WRITE, 
                StandardOpenOption.TRUNCATE_EXISTING)) {
            
            channel.truncate(size);
            System.out.println("创建了空文件: " + path + " (" + size + " 字节)");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private static void writeToMappedFile(Path path) {
        try (FileChannel channel = FileChannel.open(path, 
                StandardOpenOption.READ, 
                StandardOpenOption.WRITE)) {
            
            // 获取文件大小
            long fileSize = channel.size();
            System.out.println("准备映射文件: " + fileSize + " 字节");
            
            // 创建内存映射(整个文件)
            MappedByteBuffer buffer = channel.map(MapMode.READ_WRITE, 0, fileSize);
            
            System.out.println("开始写入数据...");
            long startTime = System.currentTimeMillis();
            
            // 写入一些模式数据
            for (int i = 0; i < fileSize / 8; i++) {
                if (i % 1_000_000 == 0) {
                    System.out.println("已写入 " + i + " 个长整数");
                }
                buffer.putLong(i);
            }
            
            // 确保数据写入
            buffer.force();
            
            long endTime = System.currentTimeMillis();
            System.out.println("写入完成! 耗时: " + (endTime - startTime) + " 毫秒");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private static void readFromMappedFile(Path path) {
        try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
            
            // 获取文件大小
            long fileSize = channel.size();
            
            // 创建只读映射
            MappedByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, fileSize);
            
            System.out.println("开始读取数据...");
            long startTime = System.currentTimeMillis();
            
            // 读取部分数据进行验证
            for (int i = 0; i < 10; i++) {
                long value = buffer.getLong(i * 8); // 直接随机访问
                System.out.println("位置 " + i + ": " + value);
            }
            
            // 读取最后10个长整数
            int lastOffset = (int)(fileSize - 80);
            buffer.position(lastOffset);
            for (int i = 0; i < 10; i++) {
                System.out.println("末尾位置 " + (lastOffset/8 + i) + ": " + buffer.getLong());
            }
            
            long endTime = System.currentTimeMillis();
            System.out.println("读取完成! 耗时: " + (endTime - startTime) + " 毫秒");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

实现文件锁定

文件锁定在多进程环境下共享文件时很有用:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Random;

public class FileLockingExample {
    public static void main(String[] args) {
        Path path = Paths.get("shared_data.txt");
        
        // 模拟多个进程访问同一个文件
        for (int i = 0; i < 3; i++) {
            final int processId = i;
            new Thread(() -> processSharedFile(path, processId)).start();
        }
    }
    
    private static void processSharedFile(Path path, int processId) {
        try (FileChannel channel = FileChannel.open(path, 
                StandardOpenOption.CREATE, 
                StandardOpenOption.READ,
                StandardOpenOption.WRITE)) {
            
            // 设置一个随机延迟让进程竞争更明显
            Random random = new Random();
            Thread.sleep(random.nextInt(1000));
            
            System.out.println("进程 " + processId + " 正在尝试获取文件锁...");
            
            // 尝试获取独占锁
            try (FileLock lock = channel.lock()) {
                System.out.println("进程 " + processId + " 获得了文件锁");
                
                // 读取当前文件内容
                channel.position(0);
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                StringBuilder content = new StringBuilder();
                
                while (channel.read(buffer) > 0) {
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    content.append(new String(bytes));
                    buffer.clear();
                }
                
                System.out.println("进程 " + processId + " 读取到内容: " + content);
                
                // 写入新内容
                String newData = "数据来自进程 " + processId + " 时间: " + System.currentTimeMillis() + "\n";
                
                channel.truncate(0);  // 清空文件
                channel.position(0);
                channel.write(ByteBuffer.wrap(newData.getBytes()));
                
                System.out.println("进程 " + processId + " 写入了新内容");
                
                // 模拟处理时间
                Thread.sleep(random.nextInt(2000) + 1000);
                
                System.out.println("进程 " + processId + " 释放文件锁");
                // 锁会在try-with-resources结构的末尾自动释放
            }
            
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

使用Path和Files API实现文件操作

Java 7引入的Path和Files API提供了更简洁的文件操作方式:

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ModernFileOperations {
    public static void main(String[] args) {
        // 创建路径
        Path sourcePath = Paths.get("example.txt");
        
        try {
            // 创建一个新文件并写入内容
            if (Files.notExists(sourcePath)) {
                Files.write(sourcePath, 
                        "Hello, NIO Path and Files API!\nThis is a test file.".getBytes(), 
                        StandardOpenOption.CREATE);
                System.out.println("文件已创建并写入内容");
            }
            
            // 读取文件内容(一次性读取所有行)
            List<String> lines = Files.readAllLines(sourcePath);
            System.out.println("文件内容:");
            lines.forEach(System.out::println);
            
            // 复制文件
            Path targetPath = Paths.get("example_copy.txt");
            Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("文件已复制到: " + targetPath);
            
            // 移动/重命名文件
            Path movedPath = Paths.get("example_moved.txt");
            Files.move(targetPath, movedPath, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("文件已移动/重命名到: " + movedPath);
            
            // 获取文件属性
            BasicFileAttributes attrs = Files.readAttributes(sourcePath, BasicFileAttributes.class);
            System.out.println("文件大小: " + attrs.size() + " 字节");
            System.out.println("创建时间: " + attrs.creationTime());
            System.out.println("最后修改时间: " + attrs.lastModifiedTime());
            
            // 列出目录内容
            Path dir = Paths.get(".");
            System.out.println("\n目录内容:");
            try (Stream<Path> paths = Files.list(dir)) {
                paths.forEach(p -> System.out.println(p.getFileName()));
            }
            
            // 使用通配符查找文件
            System.out.println("\n查找所有.txt文件:");
            try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.txt")) {
                for (Path path : stream) {
                    System.out.println(path.getFileName());
                }
            }
            
            // 递归遍历目录
            System.out.println("\n递归列出所有Java文件:");
            try (Stream<Path> paths = Files.walk(dir)) {
                List<Path> javaFiles = paths
                        .filter(p -> p.toString().endsWith(".java"))
                        .collect(Collectors.toList());
                
                javaFiles.forEach(p -> System.out.println(p));
            }
            
            // 删除测试文件
            Files.deleteIfExists(movedPath);
            System.out.println("已删除文件: " + movedPath);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用异步文件通道

Java 7引入的AsynchronousFileChannel支持异步文件操作:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;

public class AsynchronousFileExample {
    public static void main(String[] args) throws Exception {
        // 1. 使用Future方式
        readWithFuture();
        
        // 2. 使用CompletionHandler方式
        readWithCompletionHandler();
        
        // 3. 异步写入示例
        writeAsynchronously();
    }
    
    private static void readWithFuture() throws Exception {
        System.out.println("===== 使用Future读取 =====");
        Path path = Paths.get("example.txt");
        
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                path, StandardOpenOption.READ)) {
            
            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            
            // 开始异步读取操作,返回Future
            Future<Integer> operation = channel.read(buffer, 0);
            
            // 同步等待操作完成
            while (!operation.isDone()) {
                System.out.println("操作进行中...");
                Thread.sleep(100);
            }
            
            // 获取读取的字节数
            int bytesRead = operation.get();
            System.out.println("读取了 " + bytesRead + " 字节");
            
            // 准备缓冲区用于读取
            buffer.flip();
            
            // 读取数据
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            System.out.println("文件内容: " + new String(data, StandardCharsets.UTF_8));
        }
    }
    
    private static void readWithCompletionHandler() throws Exception {
        System.out.println("\n===== 使用CompletionHandler读取 =====");
        Path path = Paths.get("example.txt");
        
        // 使用CountDownLatch来等待异步操作完成
        CountDownLatch latch = new CountDownLatch(1);
        
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                path, StandardOpenOption.READ)) {
            
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            
            // 创建CompletionHandler来处理操作完成或失败
            channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("读取操作完成,读取了 " + result + " 字节");
                    
                    // 准备缓冲区用于读取
                    attachment.flip();
                    
                    // 读取数据
                    byte[] data = new byte[attachment.remaining()];
                    attachment.get(data);
                    System.out.println("文件内容: " + new String(data, StandardCharsets.UTF_8));
                    
                    // 释放锁,允许主线程继续
                    latch.countDown();
                }
                
                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("读取操作失败");
                    exc.printStackTrace();
                    latch.countDown();
                }
            });
            
            // 等待异步操作完成
            System.out.println("等待异步读取操作...");
            latch.await();
        }
    }
    
    private static void writeAsynchronously() throws Exception {
        System.out.println("\n===== 异步写入 =====");
        Path path = Paths.get("async_written.txt");
        
        CountDownLatch latch = new CountDownLatch(1);
        
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                path, 
                StandardOpenOption.WRITE, 
                StandardOpenOption.CREATE, 
                StandardOpenOption.TRUNCATE_EXISTING)) {
            
            String text = "这是通过AsynchronousFileChannel异步写入的内容!\n";
            ByteBuffer buffer = ByteBuffer.wrap(text.getBytes());
            
            // 开始异步写入
            channel.write(buffer, 0, null, new CompletionHandler<Integer, Void>() {
                @Override
                public void completed(Integer result, Void attachment) {
                    System.out.println("写入操作完成,写入了 " + result + " 字节");
                    latch.countDown();
                }
                
                @Override
                public void failed(Throwable exc, Void attachment) {
                    System.out.println("写入操作失败");
                    exc.printStackTrace();
                    latch.countDown();
                }
            });
            
            // 等待异步操作完成
            System.out.println("等待异步写入操作...");
            latch.await();
            
            System.out.println("文件写入完成: " + path.toAbsolutePath());
        }
    }
}

字符集和解码器

使用NIO处理不同字符集的文本:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class CharsetExample {
    public static void main(String[] args) throws IOException {
        // 列出所有可用的字符集
        System.out.println("可用的字符集:");
        for (String charsetName : Charset.availableCharsets().keySet()) {
            System.out.println(charsetName);
        }
        
        // 创建包含不同字符的字符串
        String text = "Hello, 你好, こんにちは, Привет, العالم";
        System.out.println("\n原始文本: " + text);
        
        // 使用不同的字符集编码和解码
        testCharset(text, StandardCharsets.UTF_8, "UTF-8");
        testCharset(text, StandardCharsets.ISO_8859_1, "ISO-8859-1");
        testCharset(text, StandardCharsets.UTF_16, "UTF-16");
        testCharset(text, Charset.forName("GBK"), "GBK");
        
        // 将文本写入不同编码的文件
        writeTextFile(text, "text_utf8.txt", StandardCharsets.UTF_8);
        writeTextFile(text, "text_utf16.txt", StandardCharsets.UTF_16);
        
        // 从不同编码的文件读取文本
        readTextFile("text_utf8.txt", StandardCharsets.UTF_8);
        readTextFile("text_utf16.txt", StandardCharsets.UTF_16);
        
        // 尝试使用错误的字符集读取文件
        readTextFile("text_utf16.txt", StandardCharsets.UTF_8);
    }
    
    private static void testCharset(String text, Charset charset, String charsetName) {
        System.out.println("\n===== 测试字符集: " + charsetName + " =====");
        
        try {
            // 创建编码器和解码器
            CharsetEncoder encoder = charset.newEncoder();
            CharsetDecoder decoder = charset.newDecoder();
            
            // 编码文本
            ByteBuffer byteBuffer = encoder.encode(CharBuffer.wrap(text));
            
            // 显示编码后的字节
            byte[] bytes = new byte[byteBuffer.remaining()];
            byteBuffer.get(bytes);
            System.out.println("编码为字节: " + bytesToHex(bytes));
            
            // 重置缓冲区并解码
            byteBuffer.flip();
            CharBuffer charBuffer = decoder.decode(byteBuffer);
            
            // 显示解码后的文本
            System.out.println("解码后的文本: " + charBuffer.toString());
            
        } catch (Exception e) {
            System.out.println("处理 " + charsetName + " 时出错: " + e.getMessage());
        }
    }
    
    private static void writeTextFile(String text, String fileName, Charset charset) throws IOException {
        Path path = Paths.get(fileName);
        
        try (FileChannel channel = FileChannel.open(path, 
                StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            
            // 使用指定字符集编码文本
            ByteBuffer buffer = charset.encode(text);
            
            // 写入文件
            channel.write(buffer);
            System.out.println("\n文本已使用 " + charset.name() + " 编码写入: " + fileName);
        }
    }
    
    private static void readTextFile(String fileName, Charset charset) throws IOException {
        Path path = Paths.get(fileName);
        
        try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate((int) channel.size());
            
            // 读取文件内容
            channel.read(buffer);
            buffer.flip();
            
            // 使用指定字符集解码
            CharBuffer charBuffer = charset.decode(buffer);
            
            System.out.println("\n使用 " + charset.name() + " 解码 " + fileName + " 的内容: " + charBuffer.toString());
        }
    }
    
    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString();
    }
}

实现简单的文本编辑器

下面是一个使用NIO实现的简单文本编辑器示例:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class SimpleTextEditor {
    private Path filePath;
    private List<String> lines = new ArrayList<>();
    private boolean modified = false;
    
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        SimpleTextEditor editor = new SimpleTextEditor();
        
        System.out.println("欢迎使用简单文本编辑器");
        System.out.println("可用命令: open <文件名>, save, list, add, edit <行号>, delete <行号>, exit");
        
        String command;
        while (true) {
            System.out.print("> ");
            command = scanner.nextLine().trim();
            
            if (command.equals("exit")) {
                if (editor.modified) {
                    System.out.print("文件已修改,是否保存? (y/n): ");
                    String response = scanner.nextLine().trim().toLowerCase();
                    if (response.equals("y")) {
                        editor.saveFile();
                    }
                }
                break;
            } else if (command.startsWith("open ")) {
                editor.openFile(command.substring(5).trim());
            } else if (command.equals("save")) {
                editor.saveFile();
            } else if (command.equals("list")) {
                editor.listContent();
            } else if (command.equals("add")) {
                System.out.println("输入文本 (输入单独的'.' 结束):");
                StringBuilder text = new StringBuilder();
                String line;
                while (!(line = scanner.nextLine()).equals(".")) {
                    text.append(line).append("\n");
                }
                editor.addText(text.toString());
            } else if (command.startsWith("edit ")) {
                try {
                    int lineNum = Integer.parseInt(command.substring(5).trim());
                    editor.editLine(lineNum, scanner);
                } catch (NumberFormatException e) {
                    System.out.println("无效的行号");
                }
            } else if (command.startsWith("delete ")) {
                try {
                    int lineNum = Integer.parseInt(command.substring(7).trim());
                    editor.deleteLine(lineNum);
                } catch (NumberFormatException e) {
                    System.out.println("无效的行号");
                }
            } else {
                System.out.println("未知命令: " + command);
            }
        }
        
        System.out.println("编辑器已关闭");
        scanner.close();
    }
    
    private void openFile(String fileName) {
        filePath = Paths.get(fileName);
        
        if (Files.exists(filePath)) {
            try {
                loadFileContent();
                System.out.println("已打开文件: " + filePath);
            } catch (IOException e) {
                System.out.println("打开文件时出错: " + e.getMessage());
            }
        } else {
            System.out.println("文件不存在,将创建新文件");
            lines.clear();
        }
        
        modified = false;
    }
    
    private void loadFileContent() throws IOException {
        lines.clear();
        
        try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ)) {
            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate((int) channel.size());
            
            // 读取文件内容
            channel.read(buffer);
            buffer.flip();
            
            // 使用UTF-8编码解码
            String content = StandardCharsets.UTF_8.decode(buffer).toString();
            
            // 分割成行
            String[] lineArray = content.split("\\r?\\n");
            for (String line : lineArray) {
                lines.add(line);
            }
        }
    }
    
    private void saveFile() {
        if (filePath == null) {
            System.out.println("没有打开的文件");
            return;
        }
        
        try (FileChannel channel = FileChannel.open(filePath, 
                StandardOpenOption.CREATE, 
                StandardOpenOption.WRITE, 
                StandardOpenOption.TRUNCATE_EXISTING)) {
            
            // 将所有行合并成一个字符串
            StringBuilder content = new StringBuilder();
            for (String line : lines) {
                content.append(line).append("\n");
            }
            
            // 编码并写入文件
            ByteBuffer buffer = StandardCharsets.UTF_8.encode(content.toString());
            channel.write(buffer);
            
            System.out.println("文件已保存: " + filePath);
            modified = false;
            
        } catch (IOException e) {
            System.out.println("保存文件时出错: " + e.getMessage());
        }
    }
    
    private void listContent() {
        if (lines.isEmpty()) {
            System.out.println("文件为空");
            return;
        }
        
        for (int i = 0; i < lines.size(); i++) {
            System.out.printf("%3d| %s%n", i + 1, lines.get(i));
        }
    }
    
    private void addText(String text) {
        String[] newLines = text.split("\\r?\\n");
        for (String line : newLines) {
            lines.add(line);
        }
        
        modified = true;
        System.out.println("文本已添加");
    }
    
    private void editLine(int lineNum, Scanner scanner) {
        if (lineNum < 1 || lineNum > lines.size()) {
            System.out.println("无效的行号");
            return;
        }
        
        System.out.println("当前内容: " + lines.get(lineNum - 1));
        System.out.print("新内容: ");
        String newContent = scanner.nextLine();
        
        lines.set(lineNum - 1, newContent);
        modified = true;
        
        System.out.println("行已更新");
    }
    
    private void deleteLine(int lineNum) {
        if (lineNum < 1 || lineNum > lines.size()) {
            System.out.println("无效的行号");
            return;
        }
        
        lines.remove(lineNum - 1);
        modified = true;
        
        System.out.println("行已删除");
    }
}

高级主题

在掌握了NIO的基础知识后,我们可以探索一些高级主题,这些主题在构建复杂的I/O应用程序时非常有用。

AIO (异步IO)

Java 7引入了真正的异步I/O API,称为AIO(Asynchronous I/O)或NIO.2。与NIO不同,AIO提供了纯异步的I/O操作,可以注册回调,当I/O操作完成时会自动调用回调函数。

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;

public class AIOExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("aio_test.txt");
        
        // 使用Future方式
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                path, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            
            ByteBuffer buffer = ByteBuffer.wrap("使用AIO的异步写入测试".getBytes());
            
            Future<Integer> result = channel.write(buffer, 0);
            while (!result.isDone()) {
                System.out.println("等待异步写入操作完成...");
                Thread.sleep(100);
            }
            
            System.out.println("写入完成: " + result.get() + " 字节");
        }
        
        // 使用CompletionHandler方式
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                path, StandardOpenOption.READ)) {
            
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            
            channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("读取完成: " + result + " 字节");
                    attachment.flip();
                    byte[] data = new byte[attachment.remaining()];
                    attachment.get(data);
                    System.out.println("内容: " + new String(data));
                }
                
                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("读取失败: " + exc.getMessage());
                }
            });
            
            // 等待异步操作完成
            Thread.sleep(1000);
        }
    }
}

零拷贝技术

零拷贝是一种优化技术,可以减少数据从内核空间到用户空间的复制,提高I/O性能。在Java NIO中,可以通过以下方式实现零拷贝:

  1. 使用transferTo和transferFrom方法:在两个通道之间直接传输数据,避免中间缓冲区
  2. 使用DirectBuffer:直接分配操作系统内存,减少JVM堆和本地内存之间的复制
  3. 使用MappedByteBuffer:将文件映射到内存,直接在内存中操作文件数据

零拷贝示例:

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class ZeroCopyExample {
    private static final int BUFFER_SIZE = 1024 * 1024; // 1MB
    
    public static void main(String[] args) throws IOException {
        // 准备源文件和目标文件
        String sourceFile = "source_large.dat";
        String targetFile = "target_large.dat";
        
        // 创建一个大文件用于测试
        createLargeFile(sourceFile, 100); // 创建100MB文件
        
        // 方法1: 使用传统方式复制
        long start = System.currentTimeMillis();
        copyWithTraditionalMethod(sourceFile, targetFile + ".traditional");
        System.out.println("传统方式耗时: " + (System.currentTimeMillis() - start) + " ms");
        
        // 方法2: 使用transferTo零拷贝
        start = System.currentTimeMillis();
        copyWithTransferTo(sourceFile, targetFile + ".transferto");
        System.out.println("TransferTo方式耗时: " + (System.currentTimeMillis() - start) + " ms");
        
        // 方法3: 使用内存映射零拷贝
        start = System.currentTimeMillis();
        copyWithMemoryMapping(sourceFile, targetFile + ".mmap");
        System.out.println("内存映射方式耗时: " + (System.currentTimeMillis() - start) + " ms");
    }
    
    private static void createLargeFile(String fileName, int sizeMb) throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(fileName, "rw")) {
            file.setLength(sizeMb * 1024 * 1024); // 设置文件大小
            
            // 写入一些随机数据
            file.seek(0);
            for (int i = 0; i < sizeMb; i++) {
                byte[] buffer = new byte[1024 * 1024]; // 1MB buffer
                for (int j = 0; j < buffer.length; j++) {
                    buffer[j] = (byte) (Math.random() * 256);
                }
                file.write(buffer);
            }
        }
        System.out.println("创建了 " + sizeMb + "MB 大小的测试文件: " + fileName);
    }
    
    private static void copyWithTraditionalMethod(String source, String target) throws IOException {
        try (FileChannel sourceChannel = FileChannel.open(Paths.get(source), StandardOpenOption.READ);
             FileChannel targetChannel = FileChannel.open(Paths.get(target),
                     StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            
            ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
            
            while (sourceChannel.read(buffer) > 0) {
                buffer.flip();
                targetChannel.write(buffer);
                buffer.clear();
            }
        }
    }
    
    private static void copyWithTransferTo(String source, String target) throws IOException {
        try (FileChannel sourceChannel = FileChannel.open(Paths.get(source), StandardOpenOption.READ);
             FileChannel targetChannel = FileChannel.open(Paths.get(target),
                     StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            
            long size = sourceChannel.size();
            long position = 0;
            
            while (position < size) {
                long transferred = sourceChannel.transferTo(position, size - position, targetChannel);
                position += transferred;
            }
        }
    }
    
    private static void copyWithMemoryMapping(String source, String target) throws IOException {
        try (FileChannel sourceChannel = FileChannel.open(Paths.get(source), StandardOpenOption.READ);
             FileChannel targetChannel = FileChannel.open(Paths.get(target),
                     StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
            
            long size = sourceChannel.size();
            
            // 设置目标文件大小
            targetChannel.truncate(size);
            
            // 映射源文件和目标文件
            MappedByteBuffer sourceBuffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);
            MappedByteBuffer targetBuffer = targetChannel.map(FileChannel.MapMode.READ_WRITE, 0, size);
            
            // 复制数据
            targetBuffer.put(sourceBuffer);
            
            // 强制写入磁盘
            targetBuffer.force();
        }
    }
}

NIO与设计模式

在使用NIO开发应用程序时,一些设计模式特别有用。下面是几个常用的设计模式:

Reactor模式

Reactor模式是异步非阻塞I/O的核心设计模式,它使用一个或多个线程处理大量连接:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ReactorPatternServer {
    private static final int PORT = 8080;
    
    public static void main(String[] args) throws IOException {
        // 创建Reactor实例
        Reactor reactor = new Reactor(PORT);
        
        // 启动服务器
        new Thread(reactor).start();
    }
    
    static class Reactor implements Runnable {
        final Selector selector;
        final ServerSocketChannel serverChannel;
        
        Reactor(int port) throws IOException {
            selector = Selector.open();
            serverChannel = ServerSocketChannel.open();
            serverChannel.socket().bind(new InetSocketAddress(port));
            serverChannel.configureBlocking(false);
            
            // 注册Accept事件
            SelectionKey sk = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            sk.attach(new Acceptor());
            
            System.out.println("服务器已启动,监听端口: " + port);
        }
        
        @Override
        public void run() {
            try {
                while (!Thread.interrupted()) {
                    selector.select();
                    Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                    while (it.hasNext()) {
                        SelectionKey key = it.next();
                        dispatch(key);
                        it.remove();
                    }
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        
        void dispatch(SelectionKey key) {
            Runnable handler = (Runnable) key.attachment();
            if (handler != null) {
                handler.run();
            }
        }
        
        // 接受连接的处理器
        class Acceptor implements Runnable {
            @Override
            public void run() {
                try {
                    SocketChannel channel = serverChannel.accept();
                    if (channel != null) {
                        new Handler(selector, channel);
                    }
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
    
    // 处理连接上的I/O事件
    static class Handler implements Runnable {
        final SocketChannel channel;
        final SelectionKey sk;
        ByteBuffer input = ByteBuffer.allocate(1024);
        ByteBuffer output = ByteBuffer.allocate(1024);
        static final int READING = 0, SENDING = 1;
        int state = READING;
        
        // 线程池用于处理业务逻辑
        static ExecutorService pool = Executors.newFixedThreadPool(10);
        
        Handler(Selector selector, SocketChannel c) throws IOException {
            channel = c;
            channel.configureBlocking(false);
            
            // 注册读事件,并传入this作为attachment
            sk = channel.register(selector, SelectionKey.OP_READ);
            sk.attach(this);
            
            // 唤醒选择器,重新进行选择
            selector.wakeup();
        }
        
        @Override
        public void run() {
            try {
                if (state == READING) {
                    read();
                } else if (state == SENDING) {
                    send();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
                closeChannel();
            }
        }
        
        void read() throws IOException {
            int readCount = channel.read(input);
            if (readCount > 0) {
                // 提交给线程池处理业务逻辑
                pool.execute(new Processer());
            } else if (readCount < 0) {
                // 客户端关闭连接
                closeChannel();
            }
        }
        
        // 业务逻辑处理器
        class Processer implements Runnable {
            @Override
            public void run() {
                processAndHandOff();
            }
        }
        
        // 处理业务逻辑,并准备发送
        synchronized void processAndHandOff() {
            input.flip();
            
            // 处理消息(这里简单地回显)
            output.clear();
            output.put("回声: ".getBytes());
            output.put(input);
            output.flip();
            
            // 切换到发送状态
            state = SENDING;
            
            // 注册写事件
            sk.interestOps(SelectionKey.OP_WRITE);
            sk.selector().wakeup();
        }
        
        void send() throws IOException {
            channel.write(output);
            
            // 检查是否还有数据要发送
            if (!output.hasRemaining()) {
                // 清空缓冲区,准备下次读取
                input.clear();
                output.clear();
                
                // 切换到读状态
                state = READING;
                
                // 注册读事件
                sk.interestOps(SelectionKey.OP_READ);
            }
        }
        
        void closeChannel() {
            try {
                sk.cancel();
                channel.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}
生产者-消费者模式

使用NIO实现生产者-消费者模式,可以用于处理高并发的I/O事件:

import java.nio.ByteBuffer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

public class ProducerConsumerExample {
    private static final int BUFFER_SIZE = 1024;
    private static final BlockingQueue<ByteBuffer> queue = new LinkedBlockingQueue<>(10);
    
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
        
        // 启动一个生产者
        pool.submit(new Producer());
        
        // 启动多个消费者
        for (int i = 0; i < 3; i++) {
            pool.submit(new Consumer(i));
        }
        
        // 等待一段时间后关闭线程池
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        pool.shutdownNow();
    }
    
    static class Producer implements Runnable {
        @Override
        public void run() {
            try {
                int messageCount = 0;
                while (!Thread.currentThread().isInterrupted()) {
                    // 创建新的ByteBuffer消息
                    ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
                    String message = "Message #" + (++messageCount);
                    buffer.put(message.getBytes());
                    buffer.flip();
                    
                    // 放入队列
                    queue.put(buffer);
                    System.out.println("生产者: 生产了 " + message);
                    
                    // 模拟处理时间
                    Thread.sleep((int)(Math.random() * 1000));
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
    static class Consumer implements Runnable {
        private final int id;
        
        Consumer(int id) {
            this.id = id;
        }
        
        @Override
        public void run() {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    // 从队列获取缓冲区
                    ByteBuffer buffer = queue.take();
                    
                    // 处理数据
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    String message = new String(data);
                    
                    System.out.println("消费者 #" + id + ": 消费了 " + message);
                    
                    // 模拟处理时间
                    Thread.sleep((int)(Math.random() * 2000));
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

自定义Channel和Buffer

在一些特殊场景下,可能需要创建自定义的Channel或Buffer实现。下面是一个简单的自定义Buffer示例:

import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.InvalidMarkException;

public class CustomBuffer<T> {
    private final T[] array;
    private int position = 0;
    private int limit;
    private int capacity;
    private int mark = -1;
    
    @SuppressWarnings("unchecked")
    public CustomBuffer(int capacity) {
        this.capacity = capacity;
        this.limit = capacity;
        this.array = (T[]) new Object[capacity];
    }
    
    public CustomBuffer<T> position(int newPosition) {
        if (newPosition < 0 || newPosition > limit) {
            throw new IllegalArgumentException("Position out of bounds");
        }
        position = newPosition;
        if (mark > position) mark = -1;
        return this;
    }
    
    public int position() {
        return position;
    }
    
    public CustomBuffer<T> limit(int newLimit) {
        if (newLimit < 0 || newLimit > capacity) {
            throw new IllegalArgumentException("Limit out of bounds");
        }
        limit = newLimit;
        if (position > limit) position = limit;
        if (mark > limit) mark = -1;
        return this;
    }
    
    public int limit() {
        return limit;
    }
    
    public int capacity() {
        return capacity;
    }
    
    public CustomBuffer<T> mark() {
        mark = position;
        return this;
    }
    
    public CustomBuffer<T> reset() {
        if (mark < 0) {
            throw new InvalidMarkException();
        }
        position = mark;
        return this;
    }
    
    public CustomBuffer<T> clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
    
    public CustomBuffer<T> flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
    
    public CustomBuffer<T> rewind() {
        position = 0;
        mark = -1;
        return this;
    }
    
    public int remaining() {
        return limit - position;
    }
    
    public boolean hasRemaining() {
        return position < limit;
    }
    
    public T get() {
        if (position >= limit) {
            throw new BufferUnderflowException();
        }
        return array[position++];
    }
    
    public T get(int index) {
        if (index < 0 || index >= limit) {
            throw new IndexOutOfBoundsException();
        }
        return array[index];
    }
    
    public CustomBuffer<T> put(T item) {
        if (position >= limit) {
            throw new BufferOverflowException();
        }
        array[position++] = item;
        return this;
    }
    
    public CustomBuffer<T> put(int index, T item) {
        if (index < 0 || index >= limit) {
            throw new IndexOutOfBoundsException();
        }
        array[index] = item;
        return this;
    }
    
    // 示例使用
    public static void main(String[] args) {
        // 创建一个整数缓冲区
        CustomBuffer<Integer> buffer = new CustomBuffer<>(10);
        
        // 填充数据
        for (int i = 0; i < 5; i++) {
            buffer.put(i * 10);
        }
        
        System.out.println("写入后 - 位置: " + buffer.position() + ", 上限: " + buffer.limit());
        
        // 切换到读模式
        buffer.flip();
        System.out.println("翻转后 - 位置: " + buffer.position() + ", 上限: " + buffer.limit());
        
        // 读取数据
        while (buffer.hasRemaining()) {
            System.out.println("读取: " + buffer.get());
        }
    }
}

常见问题与最佳实践

在使用Java NIO进行开发时,会遇到一些常见问题,以下是解决方案和最佳实践。

常见问题

1. DirectBuffer内存泄漏

问题:DirectBuffer是分配在堆外内存的,不受JVM垃圾回收机制直接管理,容易造成内存泄漏。

解决方案

  • 尽量重用DirectBuffer而不是频繁创建
  • 使用try-with-resources确保资源关闭
  • 当不再需要时,可以通过反射调用sun.misc.Cleaner手动释放
public static void cleanDirectBuffer(ByteBuffer buffer) {
    if (buffer == null || !buffer.isDirect()) return;
    
    try {
        Method cleaner = buffer.getClass().getMethod("cleaner");
        cleaner.setAccessible(true);
        Object cleanerObj = cleaner.invoke(buffer);
        
        Method clean = cleanerObj.getClass().getMethod("clean");
        clean.setAccessible(true);
        clean.invoke(cleanerObj);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
2. Selector空轮询导致CPU 100%

问题:在某些操作系统和JDK版本中,Selector可能会陷入空轮询,导致CPU使用率飙升。

解决方案

  • 设置select操作的超时时间
  • 检测空轮询次数,超过阈值后重建Selector
private Selector rebuildSelector(Selector oldSelector) throws IOException {
    // 创建新的Selector
    Selector newSelector = Selector.open();
    
    // 获取旧Selector上注册的所有键
    for (SelectionKey key : oldSelector.keys()) {
        // 跳过已取消的键
        if (!key.isValid()) continue;
        
        // 获取通道和感兴趣的事件
        SelectableChannel channel = key.channel();
        int interestOps = key.interestOps();
        Object attachment = key.attachment();
        
        // 从旧Selector中注销
        key.cancel();
        
        // 注册到新Selector
        channel.register(newSelector, interestOps, attachment);
    }
    
    // 关闭旧Selector
    oldSelector.close();
    
    return newSelector;
}
3. 多线程访问Buffer或Channel

问题:Buffer和大多数Channel实现不是线程安全的,多线程访问会导致不可预期的结果。

解决方案

  • 每个线程使用独立的Buffer
  • 使用同步机制如锁保护共享资源
  • 考虑使用线程安全的数据结构如ConcurrentLinkedQueue传递数据
public class ThreadSafeChannelExample {
    private final FileChannel channel;
    private final Object lock = new Object();
    
    public ThreadSafeChannelExample(FileChannel channel) {
        this.channel = channel;
    }
    
    public void writeData(ByteBuffer buffer) throws IOException {
        synchronized (lock) {
            channel.write(buffer);
        }
    }
    
    public int readData(ByteBuffer buffer) throws IOException {
        synchronized (lock) {
            return channel.read(buffer);
        }
    }
}
4. Buffer的position/limit/capacity混淆

问题:Buffer的三个属性(position/limit/capacity)容易混淆,导致读写错误。

解决方案

  • 始终使用flip()切换读写模式
  • 使用clear()或compact()准备写入
  • 使用方法rewind()重新读取
  • 封装提供更简单的API
public class BufferHelpers {
    public static String readAllAsString(ByteBuffer buffer) {
        buffer.flip(); // 切换到读模式
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        return new String(bytes, StandardCharsets.UTF_8);
    }
    
    public static void writeString(ByteBuffer buffer, String text) {
        buffer.clear(); // 准备写入
        buffer.put(text.getBytes(StandardCharsets.UTF_8));
    }
    
    public static ByteBuffer duplicate(ByteBuffer original) {
        original.flip();
        ByteBuffer copy = ByteBuffer.allocate(original.remaining());
        copy.put(original);
        copy.flip();
        original.flip(); // 恢复原缓冲区以便再次读取
        return copy;
    }
}

最佳实践

1. 适当的缓冲区大小选择

缓冲区大小对性能有显著影响。太小的缓冲区会导致频繁I/O操作,太大则浪费内存。

建议

  • 对于网络I/O,通常4KB~16KB是合理的选择
  • 对于文件I/O,64KB~1MB通常是个不错的选择
  • 根据实际需求和性能测试调整
2. 资源管理和清理

确保正确关闭通道和选择器,避免资源泄漏。

建议

  • 使用try-with-resources语句自动关闭资源
  • 确保在出现异常时关闭资源
  • 在对象不再使用时显式调用close()方法
// 使用try-with-resources管理资源
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
     FileChannel targetChannel = FileChannel.open(targetPath, 
             StandardOpenOption.CREATE,
             StandardOpenOption.WRITE)) {
    
    // 执行I/O操作...
    
} // 资源自动关闭
3. 优化Selector使用

Selector是高性能网络应用的核心,但需要正确使用。

建议

  • 避免在单个Selector上注册太多通道(考虑使用多个Selector)
  • 使用interestOps()方法动态调整感兴趣的事件
  • 及时从selectedKeys集合中移除已处理的键
  • 为长时间运行的select()操作设置合理的超时时间
4. 异常处理策略

I/O操作容易产生各种异常,需要谨慎处理。

建议

  • 对不同类型的异常采取不同的处理策略
  • 对于可恢复的I/O错误,考虑重试
  • 正确记录异常信息,便于诊断
  • 在高并发环境中,避免由于单个连接异常影响整个服务
private void handleClientWithRetry(SocketChannel clientChannel) {
    int retryCount = 0;
    final int MAX_RETRIES = 3;
    
    while (retryCount < MAX_RETRIES) {
        try {
            processClient(clientChannel);
            break; // 成功处理,退出循环
        } catch (IOException e) {
            retryCount++;
            LOGGER.warning("处理客户端出错,尝试重试 " + retryCount + "/" + MAX_RETRIES);
            
            if (retryCount >= MAX_RETRIES) {
                LOGGER.severe("达到最大重试次数,关闭连接: " + e.getMessage());
                try {
                    clientChannel.close();
                } catch (IOException closeEx) {
                    // 忽略关闭时的异常
                }
            }
            
            // 暂停一会再重试
            try {
                Thread.sleep(100 * retryCount);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}
5. 使用合适的I/O模型

根据应用场景选择合适的I/O模型。

建议

  • 对于小型应用或简单文件操作,传统I/O可能足够
  • 对于要处理大量连接的服务器,使用NIO非阻塞模式+Selector
  • 对于文件密集型应用,考虑内存映射文件和DirectBuffer
  • 对于异步处理需求,考虑AIO或结合线程池使用NIO
6. 性能监控和调优

定期监控应用性能,找出瓶颈并调优。

建议

  • 使用工具如JConsole、JVisualVM监控内存使用
  • 跟踪重要指标如I/O吞吐量、响应时间
  • 留意GC活动,特别是在使用大量堆内缓冲区时
  • 进行压力测试,找出系统的容量上限
7. 安全考虑

I/O操作可能涉及安全隐患,需要谨慎处理。

建议

  • 验证所有外部输入数据
  • 限制文件操作的路径,防止目录遍历攻击
  • 对敏感数据进行加密
  • 实施超时机制,防止慢客户端攻击
// 验证文件路径安全性
public boolean isPathSafe(Path path) {
    // 转换为规范路径
    Path normalizedPath;
    try {
        normalizedPath = path.normalize().toRealPath();
    } catch (IOException e) {
        return false;
    }
    
    // 检查是否在允许的根目录内
    Path allowedRoot = Paths.get("/allowed/directory").toAbsolutePath();
    return normalizedPath.startsWith(allowedRoot);
}

BufferHelpers {
public static String readAllAsString(ByteBuffer buffer) {
buffer.flip(); // 切换到读模式
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
return new String(bytes, StandardCharsets.UTF_8);
}

public static void writeString(ByteBuffer buffer, String text) {
    buffer.clear(); // 准备写入
    buffer.put(text.getBytes(StandardCharsets.UTF_8));
}

public static ByteBuffer duplicate(ByteBuffer original) {
    original.flip();
    ByteBuffer copy = ByteBuffer.allocate(original.remaining());
    copy.put(original);
    copy.flip();
    original.flip(); // 恢复原缓冲区以便再次读取
    return copy;
}

}


### 最佳实践

#### 1. 适当的缓冲区大小选择

缓冲区大小对性能有显著影响。太小的缓冲区会导致频繁I/O操作,太大则浪费内存。

**建议**:
- 对于网络I/O,通常4KB~16KB是合理的选择
- 对于文件I/O,64KB~1MB通常是个不错的选择
- 根据实际需求和性能测试调整

#### 2. 资源管理和清理

确保正确关闭通道和选择器,避免资源泄漏。

**建议**:
- 使用try-with-resources语句自动关闭资源
- 确保在出现异常时关闭资源
- 在对象不再使用时显式调用close()方法

```java
// 使用try-with-resources管理资源
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
     FileChannel targetChannel = FileChannel.open(targetPath, 
             StandardOpenOption.CREATE,
             StandardOpenOption.WRITE)) {
    
    // 执行I/O操作...
    
} // 资源自动关闭
3. 优化Selector使用

Selector是高性能网络应用的核心,但需要正确使用。

建议

  • 避免在单个Selector上注册太多通道(考虑使用多个Selector)
  • 使用interestOps()方法动态调整感兴趣的事件
  • 及时从selectedKeys集合中移除已处理的键
  • 为长时间运行的select()操作设置合理的超时时间
4. 异常处理策略

I/O操作容易产生各种异常,需要谨慎处理。

建议

  • 对不同类型的异常采取不同的处理策略
  • 对于可恢复的I/O错误,考虑重试
  • 正确记录异常信息,便于诊断
  • 在高并发环境中,避免由于单个连接异常影响整个服务
private void handleClientWithRetry(SocketChannel clientChannel) {
    int retryCount = 0;
    final int MAX_RETRIES = 3;
    
    while (retryCount < MAX_RETRIES) {
        try {
            processClient(clientChannel);
            break; // 成功处理,退出循环
        } catch (IOException e) {
            retryCount++;
            LOGGER.warning("处理客户端出错,尝试重试 " + retryCount + "/" + MAX_RETRIES);
            
            if (retryCount >= MAX_RETRIES) {
                LOGGER.severe("达到最大重试次数,关闭连接: " + e.getMessage());
                try {
                    clientChannel.close();
                } catch (IOException closeEx) {
                    // 忽略关闭时的异常
                }
            }
            
            // 暂停一会再重试
            try {
                Thread.sleep(100 * retryCount);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}
5. 使用合适的I/O模型

根据应用场景选择合适的I/O模型。

建议

  • 对于小型应用或简单文件操作,传统I/O可能足够
  • 对于要处理大量连接的服务器,使用NIO非阻塞模式+Selector
  • 对于文件密集型应用,考虑内存映射文件和DirectBuffer
  • 对于异步处理需求,考虑AIO或结合线程池使用NIO
6. 性能监控和调优

定期监控应用性能,找出瓶颈并调优。

建议

  • 使用工具如JConsole、JVisualVM监控内存使用
  • 跟踪重要指标如I/O吞吐量、响应时间
  • 留意GC活动,特别是在使用大量堆内缓冲区时
  • 进行压力测试,找出系统的容量上限
7. 安全考虑

I/O操作可能涉及安全隐患,需要谨慎处理。

建议

  • 验证所有外部输入数据
  • 限制文件操作的路径,防止目录遍历攻击
  • 对敏感数据进行加密
  • 实施超时机制,防止慢客户端攻击
// 验证文件路径安全性
public boolean isPathSafe(Path path) {
    // 转换为规范路径
    Path normalizedPath;
    try {
        normalizedPath = path.normalize().toRealPath();
    } catch (IOException e) {
        return false;
    }
    
    // 检查是否在允许的根目录内
    Path allowedRoot = Paths.get("/allowed/directory").toAbsolutePath();
    return normalizedPath.startsWith(allowedRoot);
}

通过遵循这些最佳实践,可以构建高效、可靠和安全的NIO应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈凯哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值