Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >NIO详解

NIO详解

作者头像
Java微观世界
发布于 2025-01-21 02:17:31
发布于 2025-01-21 02:17:31
46000
代码可运行
举报
文章被收录于专栏:springbootspringboot
运行总次数:0
代码可运行

一、NIO介绍

NIO (New lO)也有人称之为java non-blocking lO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。

NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作

NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。

NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。

NIO有三大核心部分: Buffer(缓冲区),Channel(通道),Selector(选择器)。

二、Buffer(缓冲区)

  • 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存
  • 这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存
  • 缓冲区主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中
  • 所有缓冲区都是Buffer抽象类的子类.

1、常见Buffer子类

  • ByteBuffer:用于存储字节数据(最常用)
  • ShortBuffer:用于存储Short类型数据
  • IntBuffer:用于存储Int类型数据
  • LongBuffer:用于存储Long类型数据
  • FloatBuffer:用于存储Float类型数据
  • DoubleBuffer:用于存储Double类型数据
  • CharBuffer:用于存储字符数据

ByteBuffer最常用,ByteBuffer三个子类的类图如下

1.1、HeapByteBuffer
  • 存储内存是在JVM堆中分配
  • 在堆中分配一个数组用来存放 Buffer 中的数据
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer>
{

	//在堆中使用一个数组存放Buffer数据
    final byte[] hb;
    ...
}
  • 通过allocate()方法进行分配,在jvm堆上申请堆上内存
  • 如果要做IO操作,会先从本进程的堆上内存复制到系统内存,再利用本地IO处理
  • 读写效率较低,受到 GC 的影响
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ByteBuffer heapByteBuffer = ByteBuffer.allocate(1024);
1.2、DirectByteBuffer
  • DirectBuffer 背后的存储内存是在堆外内存(操作系统内存)中分配,jvm内存只保留堆外内存地址
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public abstract class Buffer {
    //堆外内存地址
    long address;
    ...
}
  • 通过allocateDirect()方法进行分配,直接从系统内存中申请
  • 如果要作IO操作,直接从系统内存中利用本地IO处理
  • 使用直接内存会具有更高的效率,但是它比申请普通的堆内存需要耗费更高的性能
  • 读写效率高(少一次拷贝),不会受 GC 影响,分配的效率低
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(1024);

2、Buffer结构

ByteBuffer 有以下重要属性

  • 容量 (capacity) :作为一个内存块,Buffer具有一定的固定大小, 也称为"容量"
    • 缓冲区容量不能为负,并且创建后不能更改
  • 限制 (limit):表示缓冲区中可以操作数据的大小 (limit 后数据不能进行读写)
    • 缓冲区的限制不能为负,并且不能大于其容量
    • 写入模式,限制等于 buffer的容量
    • 读取模式下,limit等于写入的数据量
  • 位置 (position):下一个要读取或写入的数据的索引
    • 缓冲区的位置不能为负,并且不能大于其限制

ByteBuffer写入和读取原理

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void simpleTest() {
    // 1. 分配一个指定大小的缓冲区
    ByteBuffer buf = ByteBuffer.allocate(10);
    // 2. 利用put()存入数据到缓冲区中
    buf.put("data".getBytes());
    // 3. 切换读取数据模式
    buf.flip();
    // 判断缓冲区中是否还有元素
    while (buf.hasRemaining()) {
        // 4. 利用 get()读取单个字节
        byte b = buf.get();
        System.out.println("实际字节 " + (char) b);
    }
    // 清空缓冲区
    buf.clear();
}
输出结果:
实际字节 d
实际字节 a
实际字节 t
实际字节 a
  • 创建容量为10的ByteBuffer
  • 写模式下,position 是写入位置,limit 等于容量,下图表示写入了 4 个字节后的状态
  • flip 动作发生后,position 切换为读取位置,limit 切换为读取限制
  • 读取 4 个字节后,状态如下
  • clear 动作发生后,状态如下,然后切换至写模式

特别说明:compact方法,是把未读完的部分向前压缩,然后切换至写模式

3、常见方法

位置相关

  • int capacity() :返回 Buffer 的 capacity 大小
  • int limit() :返回 Buffer 的界限(limit) 的位置
  • int position() :返回缓冲区的当前位置 position
  • int remaining() :返回 position 和 limit 之间的元素个数
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void test1() {
    // 分配一个指定大小的缓冲区
    ByteBuffer buf = ByteBuffer.allocate(1024);
    System.out.println(buf.position());// 0: 表示当前的位置为0
    System.out.println(buf.limit());// 1024: 表示界限为1024,前1024个位置是允许我们读写的
    System.out.println(buf.capacity());// 1024:表示容量大小为1024
    System.out.println(buf.remaining());// 1024:表示position和limit之间元素个数
}

读写相关

  • put(byte b):将给定单个字节写入缓冲区的当前位置
  • put(byte[] src):将 src 中的字节写入缓冲区的当前位置
  • put(int index, byte b):将指定字节写入缓冲区的索引 位置(不会移动 position)
  • boolean hasRemaining(): 判断缓冲区中是否还有元素
  • get() :读取单个字节
  • get(byte[] dst):批量读取多个字节到 dst 中
  • get(int index):读取指定索引位置的字节(不会移动 position)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void test2() {
    ByteBuffer buf = ByteBuffer.allocate(10);
    // 默认写模式,写入数据
    buf.put("abcde".getBytes());
    System.out.println(buf.position());// 5: 当前位置5,表示下一个写入的位置是5
    System.out.println(buf.limit());// 10: 表示界限为10,前10个位置是允许写入的
    // 切换为读模式
    buf.flip();
    System.out.println(buf.position());// 0: 从0位置开始读取数据
    System.out.println(buf.limit());// 5: 表示界限为5,前5个位置是允许读取的
    // 读取两个字节
    byte[] dst = new byte[2];
    buf.get(dst);
    System.out.println(new String(dst, 0, 2)); // 输出:ab
    System.out.println(buf.position());// 2: 从2位置开始读取数据,因为0,1已经读取
    System.out.println(buf.limit());// 5: 表示界限为5,前5个位置是允许读取的
    // 根据索引读取,position不会移动
    byte b = buf.get(3);
    System.out.println((char) b); // 输出:d
    System.out.println(buf.position());// 2: 依然是2,没有移动
}

切换模式相关

  • Buffer flip() :将缓冲区的界限设置为当前位置, 并将当前位置重置为0(切换为读模式)
  • Buffer clear() :清空缓冲区(切换为写模式)
  • Buffer compact() :向前压缩未读取部分(切换为写模式)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void test3() {
    ByteBuffer buf = ByteBuffer.allocate(10);
    // 默认写模式,写入数据
    buf.put("hello".getBytes());
    // 切换为读模式
    buf.flip();
    // 读取两个字节
    byte[] dst = new byte[2];
    buf.get(dst);
    System.out.println(buf.position());// 2: 当前位置2,前两个位置已经读取,读取下一个位置是2
    System.out.println(buf.limit());// 5: 表示界限为5,前5个位置是允许读取的
    // 向前压缩未读取,并切换为写模式
    buf.compact();
    System.out.println(buf.position());// 3: 当前位置3,因为之前有两个位置没有被读取,放到了最前面,写入的下一个位置是3
    System.out.println(buf.limit());// 10: 表示界限为10,前10个位置是允许写入的
}

修改Buffer相关

  • Buffer limit(int n):设置缓冲区界限为 n,并返回修改后的 Buffer 对象
  • Buffer position(int n) :设置缓冲区的当前位置为 n, 并返回修改后的 Buffer 对象

标记相关

  • Buffer mark(): 对缓冲区设置标记
  • Buffer reset() :将位置 position 转到以前设置的mark 所在的位置
  • Buffer rewind() :将位置设为为0, 取消设置的mark
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void test4() {
    ByteBuffer buf = ByteBuffer.allocate(10);
    // 默认写模式,写入数据
    buf.put("hello".getBytes());
    // 切换为读模式
    buf.flip();
    // 读取两个字节
    System.out.println((char) buf.get());
    buf.mark();
    System.out.println((char) buf.get());
    System.out.println((char) buf.get());
    buf.reset();
    System.out.println((char) buf.get());
    System.out.println((char) buf.get());
    System.out.println((char) buf.get());
    System.out.println((char) buf.get());
    // hello读完再读,抛异常java.nio.BufferUnderflowException
    // System.out.println((char) buf.get());
}
输出:
h
e
l
e
l
l
o

总结Buffer读写数据四个步骤

  1. 写入数据到Buffer
  2. 调用flip()方法,转换为读取模式
  3. 从Buffer中读取数据
  4. 调用buffer.clear()方法或者buffer.compact()方法清除缓冲区并转换为写入模式

4、字符串与ByteBuffer互转

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class TestByteBufferString {
    public static void main(String[] args) {
        // 字符串转为ByteBuffer
        // 方式一:put
        ByteBuffer buffer1 = ByteBuffer.allocate(16);
        buffer1.put("hello".getBytes());

        // 方式二:Charset
        ByteBuffer buffer2 = StandardCharsets.UTF_8.encode("hello");

        // 方式三:wrap
        ByteBuffer buffer3 = ByteBuffer.wrap("hello".getBytes());

        // ByteBuffer转为字符串
        // 方式一:Charset
        String str1 = StandardCharsets.UTF_8.decode(buffer1).toString();
        
        // 方式二:String
        String str2 = new String(buffer2.array(), 0, buffer2.limit());
    }
}

三、Channel(通道)

传统流是单向的,只能读或者写,而NIO中的Channel(通道)是双向的,可以读操作,也可以写操作。

1、常见Channel实现类

  • FileChannel:用于读取、写入、映射和操作文件的通道
  • DatagramChannel:通过UDP读写网络中的数据通道
  • ServerSocketChannel和SocketChannel:通过TCP读写网络中的数据的通道
    • 类似于Socke和ServerSocket(阻塞IO),不同的是前者可以设置为非阻塞模式

2、FileChannel(文件通道)

  • FileChannel只能工作在阻塞模式下
2.1、常用方法

获取FileChannel

不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有getChannel方法。

  • 通过FileInputStream获取的 channel 只能读
  • 通过FileOutputStream获取的 channel 只能写
  • 通过RandomAccessFile是否能读写根据构造RandomAccessFile时的读写模式决定
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 只能读
FileChannel channel1 = new FileInputStream("hello.txt").getChannel();
// 只能写
FileChannel channel2 = new FileOutputStream("hello.txt").getChannel();

// 以只读方式打开指定文件
FileChannel channel3 = new RandomAccessFile("hello.txt", "r").getChannel();
// 以读、写方式打开指定文件。如果该文件尚不存在,则尝试创建该文件
FileChannel channel4 = new RandomAccessFile("hello.txt", "rw").getChannel();

读取数据

  • int read(ByteBuffer dst):从Channel到中读取数据到ByteBuffer,返回值表示读到的字节数量-1表示到达了文件的末尾
  • long read(ByteBuffer[] dsts): 将Channel中的数据“分散”到ByteBuffer数组中
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void testRead() throws IOException {
    // 获取只读文件通道
    FileChannel channel = new RandomAccessFile("hello.txt", "r").getChannel();

    // 创建字节缓冲区
    ByteBuffer buf = ByteBuffer.allocate(1024);

    // 循环读取通道中的数据,并写入到 buf 中
    while (channel.read(buf) != -1) {
        // 缓存区切换到读模式
        buf.flip();
        // 读取 buf 中的数据
        while (buf.position() < buf.limit()) {
            // 将buf中的数据追加到文件中
            System.out.println((char) buf.get());
        }
        // 清空已经读取完成的 buffer,以便后续使用
        buf.clear();
    }

    // 关闭通道
    channel.close();
}

写入数据

  • int write(ByteBuffer src):将ByteBuffer中的数据写入到Channel
  • long write(ByteBuffer[] srcs):将ByteBuffer数组中的数据“聚集”到 Channel
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void testRead() throws IOException {
    // 获取写文件通道
    FileChannel channel = new FileOutputStream("hello.txt").getChannel();

    // 将ByteBuffer数据写到通道
    channel.write(ByteBuffer.wrap("abc".getBytes()));

    // 强制将数据刷出到物理磁盘
    channel.force(false);

    // 关闭通道
    channel.close();
}

其他

  • long position() :返回此通道的文件位置
  • long size() :返回此通道的文件的当前大小
  • void force(boolean metaData) :强制将所有对此通道的文件更新写入到存储设备中
  • FileChannel position(long p) :设置此通道的文件位置
  • FileChannel truncate(long s) :将此通道的文件截取为给定大小
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void testOther() throws IOException {
    // 获取写文件通道
    FileChannel channel = new FileOutputStream("hello.txt").getChannel();

    System.out.println(channel.position());// 0:当前位置为0,表示下次写入的位置为0
    System.out.println(channel.size());// 0:文件大小为0

    // 写入3个字符到 hello.txt 文件中
    channel.write(ByteBuffer.wrap(("abc").getBytes()));

    System.out.println(channel.position());// 3:当前位置为3,表示下次写入的位置为3
    System.out.println(channel.size());// 3:文件大小为3,因为写入3个字符

    channel.position(5);// 设置当前位置为5,表示下次写入的位置为5

    // 再写入123,此时会跳过索引3和4,写入索引5
    channel.write(ByteBuffer.wrap(("123").getBytes()));

    // 将数据刷出到物理磁盘
    channel.force(false);

    // 关闭通道
    channel.close();
}

输出结果:索引3和4的位置为空,这是应该特殊字符吧

2.2、复制(transferTo/transferFrom)
  • 两个方式都能实现复制的功能
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 方法一(目标文件调用者)
 */
@Test
public void transferFrom() throws Exception {
    // 1、字节输入管道
    FileInputStream is = new FileInputStream("hello.txt"); // 源文件输入流
    FileChannel fromChannel = is.getChannel();
    // 2、字节输出流管道
    FileOutputStream fos = new FileOutputStream("hello的副本.txt"); // 目标文件输出流
    FileChannel toChannel = fos.getChannel();
    // 3、复制
    toChannel.transferFrom(fromChannel, fromChannel.position(), fromChannel.size());
    fromChannel.close();
    toChannel.close();
}

/**
 * 方法二(资源文件调用者)
 */
@Test
public void transferTo() throws Exception {
    // 1、字节输入管道
    FileInputStream is = new FileInputStream("hello.txt"); // 源文件输入流
    FileChannel fromChannel = is.getChannel();
    // 2、字节输出流管道
    FileOutputStream fos = new FileOutputStream("hello的副本.txt"); // 目标文件输出流
    FileChannel toChannel = fos.getChannel();
    // 3、复制
    fromChannel.transferTo(fromChannel.position(), fromChannel.size(), toChannel);
    fromChannel.close();
    toChannel.close();
}
  • 超过2g大小的文件传输(因为超过2g,多出的部分会丢失)
  • 循环复制,每次30MB(FileUtils.copyFile(final File srcFile, final File destFile)方法的内部实现)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void transferFromBig() throws IOException {
    // 使用try-with-resources语句确保流在使用完毕后被正确关闭
    try (FileInputStream fis = new FileInputStream("hello.txt"); // 源文件输入流
         FileChannel input = fis.getChannel(); // 获取源文件的文件通道
         FileOutputStream fos = new FileOutputStream("hello的副本.txt"); // 目标文件输出流
         FileChannel output = fos.getChannel()) { // 获取目标文件的文件通道
        final long size = input.size(); // 获取源文件的大小
        long pos = 0;
        long count;
        // 循环读取源文件内容,直到全部复制完毕
        while (pos < size) {
            // 计算剩余待复制的字节数
            final long remain = size - pos;
            // 根据剩余字节数决定本次要复制的字节数,最多30MB
            count = remain > 1024 * 1024 * 30 ? 1024 * 1024 * 30 : remain;
            // 从源文件通道复制数据到目标文件通道
            final long bytesCopied = output.transferFrom(input, pos, count);
            if (bytesCopied == 0) {
                // 如果没有复制任何数据,跳出循环
                break;
            }
            // 更新已复制的字节位置
            pos += bytesCopied;
        }
    }
}

3、ServerSocketChannel和SocketChannel(TCP网络通道)

3.1、阻塞模式
  • 阻塞模式下,相关方法都会导致线程暂停
    • ServerSocketChannel.accept 会在没有连接建立时让线程暂停
    • SocketChannel.read 会在没有数据可读时让线程暂停
    • 阻塞的表现其实就是线程暂停,暂停期间不会占用cpu,线程相当于闲置什么也不能做
  • 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
  • 但多线程下,有新的问题,体现在以下方面
    • 32位jvm一个线程320k,64位jvm一个线程1024k,如果连接数过多,必然导致OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低
    • 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接

服务端

  • 默认情况与Socke和ServerSocket一样,是阻塞IO,accept和read为阻塞方法
  • 当没有客户端连接时,线程会阻塞在accept()方法,等待客户端的连接
  • 当客户端连接,当没有发送数据时,线程会阻塞在read()方法,等待客户端的发送
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void server() throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(16);
    // 1. 创建一个ServerSocketChannel通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 2. 绑定监听端口
    serverSocketChannel.bind(new InetSocketAddress(8080));
    // 3. 连接集合
    List<SocketChannel> channels = new ArrayList<>();
    while (true) {
        // 4. accept 建立与客户端连接, SocketChannel 用来与客户端之间通信
        log.debug("connecting...");
        SocketChannel sc = serverSocketChannel.accept(); // 阻塞方法,线程停止运行
        log.debug("connected... {}", sc);
        channels.add(sc);
        // 遍历连接集合
        for (SocketChannel channel : channels) {
            // 5. 接收客户端发送的数据
            log.debug("before read... {}", channel);
            channel.read(buffer); // 阻塞方法,线程停止运行,等待客户端发消息读取
            buffer.flip(); // 转为读模式
            // 打印出响应信息
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
            buffer.clear(); // 清空缓冲区,转为写模式
            log.debug("after read...{}", channel);
        }
    }
}

客户端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void client() throws IOException {
    // 创建一个SocketChannel通道,并连接到本地的8080端口
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.connect(new InetSocketAddress("localhost", 8080));
    socketChannel.write(ByteBuffer.wrap("a".getBytes()));
    System.in.read();
}
3.2、非阻塞模式
  • 非阻塞模式下,相关方法都会不会让线程暂停
    • 在ServerSocketChannel.accept没有连接建立时,会返回null,继续运行
    • 在SocketChannel.read没有数据可读时,会返回0,但线程不会阻塞
  • 但非阻塞模式下,即使没有连接建立和可读数据,线程仍然在不断运行,白白浪费了cpu

服务端

  • 设置ServerSocketChannel和SocketChannel.configureBlocking(false)即为非阻塞模式
  • 这种情况程序不会阻塞,程序一直运行,也就代表着cpu一刻不停,不论是否有新连接和数据读取
  • 下文通过Selector解决浪费cpu的问题
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void server() throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(16);
    // 1. 创建了服务器
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.configureBlocking(false); // 非阻塞模式
    // 2. 绑定监听端口
    serverSocketChannel.bind(new InetSocketAddress(8080));
    // 3. 连接集合
    List<SocketChannel> channels = new ArrayList<>();
    while (true) {
        // 4. accept 建立与客户端连接, SocketChannel 用来与客户端之间通信
        // 非阻塞,线程还会继续运行,如果没有连接建立,但sc是null
        SocketChannel socketChannel = serverSocketChannel.accept();
        if (socketChannel != null) {
            log.debug("connected... {}", socketChannel);
            socketChannel.configureBlocking(false); // 非阻塞模式
            channels.add(socketChannel);
        }
        for (SocketChannel channel : channels) {
            // 5. 接收客户端发送的数据
            // 非阻塞,线程仍然会继续运行,如果没有读到数据,read 返回 0
            int read = channel.read(buffer);
            if (read > 0) {
                buffer.flip();
                // 打印出响应信息
                while (buffer.hasRemaining()) {
                    System.out.print((char) buffer.get());
                }
                buffer.clear();
                log.debug("after read...{}", channel);
            }
        }
    }
}

四、Selector(选择器)

  • Java的NIO用非阻塞的IO方式,可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
  • Selector能够检测多个注册的通道上是否有事件发生,才会处理,如果没有事件发生,则处于阻塞状态,防止cpu浪费

1、Selector的应用

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//1. 获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
serverSocketChannel.configureBlocking(false);
//3. 绑定连接
serverSocketChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

监听的事件类型(SelectionKey四个int常量)

  • :SelectionKey.OP_READ (1)
  • :SelectionKey.OP_WRITE (4)
  • 连接:SelectionKey.OP_CONNECT (8)
  • 接收:SelectionKey.OP_ACCEPT (16)

若注册时不止监听一个事件,则可以使用“位或”操作符连接

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 监听读和写事件
serverSocketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);

2、多路复用

  • 单线程可以配合Selector完成对多个Channel可读写事件的监控,这称之为多路复用
  • 多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用
  • 如果不用Selector的非阻塞模式,线程大部分时间都在做无用功,而Selector能够保证有事件发生cpu才运行

服务端

  • 一个服务端通道ServerSocketChannel和多个SocketChannel客户端通道注册到selector上
  • 当没有事件发生时,线程会阻塞再selector.select()方法,有事件发生,返回事件数量,进入while循环
  • selectionKey表示某个注册的客户端的接入或者读写事件
  • read()方法的三种返回值
    • 返回值大于0:读到了数据,直接对字节进行编解码
    • 返回值等于0:没有读到字节,属于正常场景,忽略
    • 返回值为-1:链路已经关闭,需要关闭SocketChannel释放资源
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void server() throws IOException {
    // 1.获取管道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 2.设置非阻塞模式
    serverSocketChannel.configureBlocking(false);
    // 3.绑定端口
    serverSocketChannel.bind(new InetSocketAddress(8888));
    // 4.获取选择器
    Selector selector = Selector.open();
    // 5.将通道注册到选择器上,并且开始指定监听的接收事件
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    // 6.轮询已经就绪的事件
    // select方法, 没有事件发生,线程阻塞,有事件,线程才会恢复运行,返回事件数量
    // 事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则即使item.remove(),selector.select()还是会获取到没处理的事件
    while (selector.select() > 0) {
        System.out.println("开启事件处理");
        // 7.获取选择器中所有注册的通道中已准备好的事件
        Iterator<SelectionKey> it = selector.selectedKeys().iterator();
        // 8.开始遍历事件
        while (it.hasNext()) {
            SelectionKey selectionKey = it.next();
            System.out.println("客户端通道事件对象key:" + selectionKey);
            // 9.判断这个事件具体是啥
            if (selectionKey.isAcceptable()) { // 客户端接入事件
                // 10.获取当前接入事件的客户端通道
                SocketChannel socketChannel = serverSocketChannel.accept();
                // 11.切换成非阻塞模式
                socketChannel.configureBlocking(false);
                // 12.将本客户端注册到选择器
                socketChannel.register(selector, SelectionKey.OP_READ);
            } else if (selectionKey.isReadable()) { // 读事件
                // 13.获取当前选择器上的读通道
                SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                // 14.读取
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                /*
				 * read()方法的三种返回值
				 * 返回值大于0:读到了直接,对字节进行编解码
				 * 返回值等于0:没有读到字节,属于正常场景,忽略
				 * 返回值为-1:链路已经关闭,需要关闭SocketChannel释放资源
				 */
                int len = socketChannel.read(buffer);
                if (len > 0) {
                    buffer.flip(); // 转为读模式
                    System.out.println(new String(buffer.array(), 0, len));
                    buffer.clear(); // 清空缓冲区,转为写模式
                } else if(len < 0) {
                    // 如果读不到数据,取消事件
                    // 否则客户端断开时,len=-1,数据没有读取到也就是没有处理,会一直循环调用此读事件内容
                    selectionKey.cancel();
                    socketChannel.close();
                }
            }
            // 15.处理完毕后,移除当前事件
            it.remove();
        }
    }
}

客户端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static void main(String[] args) throws Exception {
    // 1、获取通道
    SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("localhost", 8888));
    // 2、切换成非阻塞模式
    sChannel.configureBlocking(false);
    // 3、分配指定缓冲区大小
    ByteBuffer buf = ByteBuffer.allocate(1024);
    // 4、发送数据给服务端
    Scanner sc = new Scanner(System.in);
    while (true) {
        System.out.println("请说:");
        String msg = sc.nextLine();
        buf.put((msg).getBytes());
        buf.flip();
        sChannel.write(buf);
        buf.clear();
    }
}

五、零拷贝

1、传统IO

  • 传统的 IO 将一个文件通过 socket 写出
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");

byte[] buf = new byte[(int)f.length()];
file.read(buf);

Socket socket = ...;
socket.getOutputStream().write(buf);

内部工作流程是这样的:

  1. java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 java 程序的用户态切换至内核态,去调用操作系统的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用 DMA(可以理解为硬件单元)来实现文件读,其间也不会使用 cpu
  2. 内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 cpu 会参与拷贝,无法利用 DMA
  3. 调用write方法,这时将数据从用户缓冲区(byte[] buf)写入socket 缓冲区,cpu 会参与拷贝
  4. 接下来要向网卡写数据,这项能力 java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将socket 缓冲区的数据写入网卡,不会使用 cpu

java 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的

  • 用户态与内核态的切换发生了 3 次,这个操作比较重量级
  • 数据拷贝了共 4

2、NIO优化

2.1、DirectByteBuffer
  • java可以使用DirectByteBuffer将堆外内存(系统内存)映射到jvm内存中来直接访问使用
  • java中的DirectByteBuffer对象仅维护了此内存的虚引用

用户态与内核态的切换次数与数据拷贝次数

  • 用户态与内核态的切换发生了 3
  • 数据拷贝了共 3
2.2、linux2.1提供的sendFile方法
  • 进一步优化(底层采用了linux 2.1后提供的sendFile方法),java 中对应着两个channel调用 transferTo/transferFrom方法拷贝数据
  1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
  2. 数据从内核缓冲区传输到 socket 缓冲区,cpu 会参与拷贝
  3. 最后使用 DMA 将socket 缓冲区的数据写入网卡,不会使用 cpu

用户态与内核态的切换次数与数据拷贝次数

  • 用户态与内核态的切换发生了 1
  • 数据拷贝了共 3
2.3、linux 2.4
  1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
  2. 只会将一些 offset 和 length 信息拷入socket 缓冲区,几乎无消耗
  3. 使用 DMA 将内核缓冲区的数据写入网卡,不会使用 cpu

用户态与内核态的切换次数与数据拷贝次数

  • 用户态与内核态的切换发生了 1
  • 数据拷贝了共 2

整个过程仅只发生了一次用户态与内核态的切换,数据拷贝了 2 次。所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到 jvm 内存中。零拷贝适合小文件传输。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-04-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java NIO?看这一篇就够了!
✎前言 现在使用NIO的场景越来越多,很多网上的技术框架或多或少的使用NIO技术,譬如Tomcat,Jetty。学习和掌握NIO技术已经不是一个JAVA攻城狮的加分技能,而是一个必备技能。在前面2篇文
方志朋
2019/06/21
1.1K0
Java NIO?看这一篇就够了!
java nio 详_java NIO 详解
Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。本系列教程将有助于你学习和理解Java NIO。
全栈程序员站长
2022/09/08
8180
java nio 详_java NIO 详解
NIO学习之NIO概述和FileChannel详解
Java NIO(New IO 或 Non Blocking IO)是从 Java 1.4 版本开始引入的一个新的IO API,可以替代标准的 Java IO API。NIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。
大忽悠爱学习
2021/12/07
4650
NIO学习之NIO概述和FileChannel详解
NIO~~
NIO 有三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)
大忽悠爱学习
2022/05/06
9590
NIO~~
NIO最全教程,看这一篇就够了
Java NIO(New IO 或 Non Blocking IO)是从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API。NIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。
公众号 IT老哥
2020/09/16
7300
NIO最全教程,看这一篇就够了
NIO全解析说明
Java NIO是一个用来替代标准Java IO API的新型数据传递方式,像现在分布式架构中会经常存在他的身影。其比传统的IO更加高效,非阻塞,异步,双向
迹_Jason
2019/05/30
8740
Java知识点——NIO和BIO
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jvUiQgVM-1584882587306)(img/NIO图例.png)]
用户7073689
2020/03/24
4060
Java NIO笔记
limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据,写模式下,等于Buffer的capacity。 position:在写模式下,position表示当前的位置。初始值为0,最大可为capacity-1. capacity:一个内存块,Buffer的固定的大小值。
haifeiWu
2018/09/11
4930
🎯 Java NIO 基础
✏️ 写在前面的话: Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景。 Netty作为一款基于Java开发的高性能网络框架,想要从认识到熟悉再到掌握最终理解,因此我们需要从最基础的NIO开始学习。如果你已经学习并掌握了NIO相关知识,那么可以直接进入Netty相关文章的学习;如果没有了解过也没有关系,那我们就从当前文章开始学习吧!🎉🎉🎉 这里我们先简单了解一下这一篇文章中我们将要学习的内容: 首先是NIO的基本介绍,了解NIO的三大组件 ByteBuffer 字节缓冲区的基本使用
爱吃糖的范同学
2023/02/11
8680
NIO前言:一、NIO与IO的区别二、通道和缓冲区三、NIO的网络通信总结:
所谓NIO,就是New IO的缩写。是从JDK 1.4开始引入的全新的IO API。NIO将以更高效的方式进行文件的读写操作,可完全代替传统的IO API使用。而且JDK 1.7对NIO又进行了更新,可以称作NIO 2.0。
贪挽懒月
2018/12/27
6.8K0
Netty基础—4.NIO的使用简介
在NIO中,所有的数据都是通过使用Buffer缓冲区来处理的。如果要通过NIO,将数据写到文件和网络或从文件和网络中读取数据,那么就需要使用Buffer缓冲区来进行处理。
东阳马生架构
2025/05/20
1010
java nio
文章目录 1. 缓冲区(Buffer) 1.1. 常用的方法 1.2. 核心属性 1.3. 直接缓冲区 1.4. 非直接缓冲区 2. 通道(Channel) 2.1. 获取通道 2.2. 实例 2.3. 通道之间指定进行数据传输 2.4. 分散读取 2.5. 聚集写入 2.6. NIO阻塞式 3. Selector(选择器) 3.1. SelectionKey 3.2. NIO非阻塞式 4. 参考文章 缓冲区(Buffer) 负责数据的存取,实际上就是一个数组,用于存储不同的数据 除了布尔类型之后,其他
爱撒谎的男孩
2019/12/31
1.2K0
03-Java NIO 编程 入门
缓冲区(Buffer) : 缓冲区本质上是一个可以读写数据的内存块, 可以理解成是一个容器对象(含数组), 该对象提供了一组方法,可以更轻松的使用内存块,缓冲区内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,Channel提供了从文件,网络读取数据的渠道,但是读取或写入的数据必须经由Buffer, 如图: [后面举例说明]
彼岸舞
2022/02/18
4330
03-Java NIO 编程 入门
Netty-nio
channel 有一点类似于 stream,它就是读写数据的双向通道,可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel,而之前的 stream 要么是输入,要么是输出,channel 比 stream 更为底层
sgr997
2022/11/10
7440
Netty-nio
Java.NIO编程一览笔录
Java标准IO 与 Java NIO 的简单差异示意: Java标准IO Java NIO API调用 简单 复杂 底层实现 面向流(stream),单向 面向通道(channel),释放CPU、内存压力 成效 同步阻塞 同步非阻塞 数据窥视 阻塞读取,要么足够,要么没有 使用缓冲区(Buffer), 读数据时需要检查是否足够 处理数据的线程数 1:1(一个线程处理一个流) 1:N(选择器(Selector),多路复用,可以一个或几个少量线程管理多个通道) Java N
斯武丶风晴
2018/03/01
1.3K0
Java.NIO编程一览笔录
JavaIO流:NIO梳理
NIO 也叫 Non-Blocking IO 是同步非阻塞的 IO 模型。线程发起 IO 请求后,立即返回。同步指的是必须等待 IO 缓冲区内的数据就绪,而非阻塞指的是,用户线程不原地等待 IO 缓冲区,可以先做一些其他操作,但是要定时轮询检查 IO 缓冲区数据是否就绪。
栗筝i
2022/12/02
4680
JavaIO流:NIO梳理
NIO从入门到踹门
java.nio全称java non-blocking IO,是指JDK1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络(来源于百度百科)。
java技术爱好者
2020/09/22
1K0
NIO从入门到踹门
NIO
详细知识参考我有道云笔记 package com.shi.nio; import java.nio.ByteBuffer; /** * * @author shiye * 一、缓冲区(Buffer):在 Java NIO 中负责数据的存取。缓冲区就是数组。用于存储不同数据类型的数据 * * 根据数据类型不同(boolean 除外),提供了相应类型的缓冲区: * ByteBuffer * CharBuffer * ShortBuffer * IntBuffer * LongBuf
用户5927264
2019/10/15
7840
Netty系列| Netty创始人告诉你为什么选择NIO
NIO模型 同步非阻塞 NIO有同步阻塞和同步非阻塞两种模式,一般讲的是同步非阻塞,服务器实现模式为一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
狼王编程
2021/06/01
1.4K0
Netty系列| Netty创始人告诉你为什么选择NIO
《最佳实践》-NIO知识梳理
NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。
九灵
2020/12/13
3710
相关推荐
Java NIO?看这一篇就够了!
更多 >
交个朋友
加入腾讯云官网粉丝站
蹲全网底价单品 享第一手活动信息
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验