1.NIO的全称

nio 即非阻塞IO,是JDK1.4开始引入的一套全新的IO API。

包含在java.io包下,主要有三大组件:缓冲区buffer、通道channel、选择器selexcor。

2.为什么要用NIO

在传统的java io中:

  • I/O是阻塞式的:线程发起读写操作,必须等操作完成结束后才能继续,效率低下。

  • 每个连接都要创建一个新的线程,造成资源浪费

而NIO采用非阻塞IO+多路复用,单线程就能够处理多个连接,提高伸缩和性能。

3.NIO的核心概念

  • Buffer缓冲区:数据缓冲区,所有的读写操作都在其中进行

  • Channel通道:双向通道,可同时进行读写操作

  • selector选择器:同时监听多个channel事件

4.具体流程

  1. 把数据写入buffer

  2. 从buffer读取数据

  3. 使用channel进行传输

  4. 通过selector注册多个channel

单线程就可以处理成千上万个连接,提升了服务器的吞吐能力。

5.应用场景

聊天服务器、http服务器、游戏服务器、大量文件传输

Netty框架就是在NIO上进行封装的

三大组件

组件

作用

类举例

Buffer(缓冲区)

存储数据的容器,所有数据读写都通过它进行

ByteBufferCharBufferIntBuffer

Channel(通道)

数据传输通道,负责与 I/O 源(文件、网络)交互

FileChannelSocketChannelServerSocketChannelDatagramChannel

Selector(选择器)

管理多个通道的事件,可以用单线程监控多个 Channel

Selector

1️⃣ Buffer(缓冲区)

  • 本质:一块连续的内存块,数据读写的中转站。

  • 特点:有位置(position)、容量(capacity)、界限(limit)三个核心属性。

  • 常用操作

    • put():写入数据到缓冲区。

    • get():从缓冲区读取数据。

    • flip():切换读写模式。

    • clear():清空缓冲区以供再次写入。

  • 好处:比传统流式 I/O 高效,可以反复读写,不必每次都向内核申请内存。

2️⃣ Channel(通道)

  • 本质:类似流,但 双向(既可读也可写)。

  • 常见类型

    • FileChannel:文件数据传输。

    • SocketChannel:TCP客户端通道。

    • ServerSocketChannel:TCP服务端监听通道。

    • DatagramChannel:UDP通道。

  • 特点

    • 支持非阻塞模式(配合 Selector)。

    • 通过 Channel 把数据读到 Buffer 或写出 Buffer。

  • 例子

FileChannel channel = new FileInputStream("data.txt").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer); // 从文件读数据到缓冲区

3️⃣ Selector(选择器)

  • 本质:多路复用器,可以用一个线程监控多个 Channel 的状态。

  • 作用

    • 注册 Channel,并监听它们的事件(如 OP_READOP_WRITEOP_ACCEPT)。

    • 当 Channel 就绪时,会通知 Selector,应用程序即可处理数据。

  • 好处:无需为每个连接分配一个线程,大大提高可扩展性和性能。

  • 典型用法

Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);

while (true) {
    selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    for (SelectionKey key : keys) {
        // 根据key的状态处理对应的事件
    }
}

📝 总结

  • Buffer:负责存储数据(内存)。

  • Channel:负责数据传输(通道)。

  • Selector:负责事件管理(调度)。
    三者结合就能实现 非阻塞 I/O + 多路复用

ByteBuffer

具体使用:

public static void main(String[] args) {
		// FileChannel
		try (FileChannel channel = new FileInputStream("data.txt").getChannel()) {
			// 使用ByteBuffer
			ByteBuffer buffer = ByteBuffer.allocate(10);
			while (true) {
				int len = channel.read(buffer);
				log.info("读取到的字节数:{}", len);
				if (len == -1) {
					break;
				}
				buffer.flip();// 切换成读模式
				while (buffer.hasRemaining()) {
					byte b = buffer.get();
					log.info("实际字节:{}", (char) b);
				}
				buffer.clear();// 切换成写模式
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

具体流程:

  1. 向buffer中写入数据,例如调用channel.read()

  2. 切换为读模式,调用buffer.flip()

  3. 使用buffer.get()获取具体内容

  4. 切换为写模式,调用clear或compact

  5. 重复步骤1~4

内部结构:

三个属性:capacity容量、position位置、limit界限

初始:

写模式:移动position,下图为写入4个字节后的状态

读模式:调用flip()动作后,移动position到首位置,limit移动到最后一个字节的位置

读取写个字节后的状态:

执行clear后:

常用方法

public static void main(String[] args) {
		// 分配空间
		ByteBuffer buffer = ByteBuffer.allocate(10);
		// 写入数据
		buffer.put(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
		buffer.flip();// 切换成读模式
		System.out.println(buffer.get());
		System.out.println(buffer.get());
		System.out.println(buffer.get());
		System.out.println("======");
		// rewind();从头开始读
		buffer.rewind();
		System.out.println(buffer.get());
		System.out.println(buffer.get());
		System.out.println("======");
		// mark();标记当前位置
		buffer.mark();
		System.out.println(buffer.get());
		System.out.println(buffer.get());
		System.out.println("======");
		// reset();回到mark的位置
		buffer.reset();
		System.out.println(buffer.get());
		System.out.println("======");
		// get(i);//通过索引获取
		System.out.println(buffer.get(3));

	}

解析黏包和半包

黏包和半包是网络编程(尤其是 TCP 编程)里经常遇到的两个现象。

黏包:多条应用层消息在接收方看起来被“黏”到了一起,变成一条。

半包:一条完整的应用层消息在接收方被拆成了多段,接收时只能收到部分内容。

解决代码:

public static void main(String[] args) {
		ByteBuffer source = ByteBuffer.allocate(32);
		source.put("Hello World\nI’m ZhangSan\nH".getBytes());
		split(source);
		source.put("ow Are You?\n".getBytes());
		split(source);
		source.flip();
	}

	private static void split(ByteBuffer source) {
		source.flip();
		for (int i = 0; i < source.limit(); i++) {
			if (source.get(i) == '\n') {
				int len = i + 1 - source.position();
				ByteBuffer target = ByteBuffer.allocate(len);
				for (int j = 0; j < len; j++) {
					target.put(source.get());
				}
				System.out.println(new String(target.array()));
			}
		}
		source.compact();
	}

文件编程

两个channel传输数据

使用transferTo方法,主要用于 高效地将文件内容从一个通道传输到另一个通道

方法签名

public abstract long transferTo(long position, long count, WritableByteChannel target)
        throws IOException

参数

  • position:文件开始传输的位置(字节偏移量)。

  • count:要传输的字节数。

  • target:目标通道(必须是 WritableByteChannel,比如 SocketChannelFileChannel 等)。

返回值

  • 实际传输的字节数(可能比 count 少)。

特点

  1. 零拷贝 (Zero-copy):底层会尝试使用操作系统提供的零拷贝机制(如 Linux 的 sendfile),避免了 Java 用户态和内核态之间的数据拷贝,提高性能,减少内存占用。

  2. 效率高:特别适合文件传输场景(例如实现高性能的 HTTP 文件下载、NIO 网络编程中的大文件传输)。

  3. 限制

    • 传输大小可能受操作系统限制(比如 Linux 单次调用最大 2GB)。

    • 有些平台上实现不支持 transferTo,会退化为普通循环读写。

    • 如果目标通道不支持(例如非 FileChannelSocketChannel),可能会抛异常。

示例

public static void main(String[] args) {
		try (FileChannel from = new RandomAccessFile("H:\\vmos\\centos7.iso\\CentOS-7-x86_64-DVD-2009 (1).iso", "r").getChannel();
			 FileChannel to = new RandomAccessFile("to.iso","rw").getChannel()
		) {
			//size 文件大小
			long size = from.size();
			long start = System.currentTimeMillis();
			//left 文件剩余字节数
			for (long left = size; left > 0;) {
				log.info("position:{},left:{}", (size - left), left);
				left -= from.transferTo(size - left, left, to);
			}
			log.info("耗时:{}s", (System.currentTimeMillis() - start) / 1000);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

Paths

位置:java.nio.file.Paths

作用:提供静态方法将字符串路径或 URI 转换为 Path 对象。(在 Java 7 引入的 NIO.2 中,Path 是代替传统 File 的核心类,功能更强大)

常用方法:

  • 根据字符串路径或uri构建Path对象

  • Path p = Paths.get("C:/Users/test.txt");

  • Path p = Paths.get(new URI("file:///C:/test.txt"));

需要配合Files类来操作文件

Files

位置:java.nio.file.Files

作用:对 Path 对象进行操作(创建、复制、移动、删除、读写内容、检查属性等)。

(可看作 Path 的“工具类”)

文件/目录操作

方法

功能

示例

createFile(Path path)

创建新文件(已存在会抛异常)

Files.createFile(Paths.get("a.txt"));

createDirectory(Path dir)

创建单层目录

Files.createDirectory(Paths.get("newDir"));

createDirectories(Path dir)

创建多层目录(父目录不存在也行)

Files.createDirectories(Paths.get("a/b/c"));

delete(Path path)

删除文件或空目录(不存在抛异常)

Files.delete(path);

deleteIfExists(Path path)

删除文件或目录(不存在不抛异常)

Files.deleteIfExists(path);

copy(Path src, Path dest, CopyOption...)

复制文件或目录

Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);

move(Path src, Path dest, CopyOption...)

移动或重命名

Files.move(src, dest);

exists(Path path, LinkOption...)

判断文件或目录是否存在

Files.exists(path);

notExists(Path path, LinkOption...)

判断不存在

Files.notExists(path);

文件属性(元数据)

方法

功能

示例

size(Path path)

获取文件大小(字节)

long s = Files.size(path);

getLastModifiedTime(Path path)

获取最后修改时间

Files.getLastModifiedTime(path);

isDirectory(Path path)

是否目录

Files.isDirectory(path);

isRegularFile(Path path)

是否普通文件

Files.isRegularFile(path);

isReadable/Executable/Writable(Path)

权限检查

Files.isReadable(path);

getOwner(Path path)

获取所有者

Files.getOwner(path);

setAttribute/getAttribute(Path,String,Object...)

读写任意属性

Files.getAttribute(path, "basic:size");

文件内容读写

方法

功能

示例

newBufferedReader(Path, Charset)

按行读文本

try (BufferedReader br = Files.newBufferedReader(path)) {...}

newBufferedWriter(Path, Charset, OpenOption...)

按行写文本

try (BufferedWriter bw = Files.newBufferedWriter(path)) {...}

readAllBytes(Path path)

读所有字节到 byte[]

byte[] data = Files.readAllBytes(path);

readAllLines(Path path)

读所有行到 List<String>

List<String> lines = Files.readAllLines(path);

write(Path path, byte[] bytes, OpenOption...)

写字节

Files.write(path, data);

write(Path path, Iterable<? extends CharSequence> lines, Charset, OpenOption...)

写多行

Files.write(path, lines);

lines(Path path) (Java 8+)

流式读取行

Files.lines(path).forEach(System.out::println);

目录流与遍历

方法

功能

示例

newDirectoryStream(Path dir)

遍历目录(返回 DirectoryStream

try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {...}

walk(Path start) (Java 8+)

深度遍历文件树(Stream)

Files.walk(Paths.get("."), 3).forEach(System.out::println);

find(Path start, int depth, BiPredicate) (Java 8+)

按条件查找文件

Files.find(dir, 2, (p,a)->p.toString().endsWith(".txt")).forEach(System.out::println);

walkFileTree(Path start, FileVisitor<? super Path> visitor)

自定义遍历文件树

Files.walkFileTree(...)

示例

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;

public class PathsFilesDemo {
    public static void main(String[] args) throws IOException {
        // 1. 获取 Path
        Path path = Paths.get("example.txt");

        // 2. 写入文本
        Files.write(path, "Hello World".getBytes(StandardCharsets.UTF_8));

        // 3. 读取文本
        String content = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
        System.out.println(content);

        // 4. 判断文件属性
        System.out.println("Exists? " + Files.exists(path));
        System.out.println("Size: " + Files.size(path) + " bytes");

        // 5. 复制文件
        Files.copy(path, Paths.get("copy.txt"), StandardCopyOption.REPLACE_EXISTING);

        // 6. 遍历目录
        Files.walk(Paths.get("."))
             .filter(Files::isRegularFile)
             .forEach(System.out::println);
    }
}