Java NIO 三大核心(Buffer、Channel、Selector)理解

我是傲骄鹿先生,沉淀、学习、分享、成长。

如果你觉得文章内容还不错的话,希望不吝您的「一键三连」,文章里面有不足的地方希望各位在评论区补充疑惑、见解以及面试中遇到的奇葩问题

目录​​​​​​​

一、Buffer 的机制及子类

1、Buffer(缓冲区)基本介绍

二、Channel 的基本介绍

1、FileChannel 类

2、使用 FileChannel 写入文本文件

3、使用 FileChannel 读取文本文件

4、使用 FileChannel 复制文件

5、使用 transferFrom 复制文件

扫描二维码关注公众号,回复: 17286620 查看本文章

三、Channel 和 Buffer 的注意事项

四、Selector

1、Selector 的基本介绍

2、Selector 特点

3、Selector 常用方法

4、Selector 相关方法说明

五、NIO 非阻塞网络编程过程分析

NIO 非阻塞网络编程代码示例

运行结果


一、Buffer 的机制及子类

1、Buffer(缓冲区)基本介绍

缓冲区本质上是一个可以读写数据的内存块,可以理解为是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。

Channel 提供从文件、网络读取数据的渠道,但是读取或者都必须经过 Buffer。

在 Buffer 子类中维护着一个对应类型的数组,用来存放数据:

public abstract class IntBuffer extends Buffer implements Comparable<IntBuffer>{

    // These fields are declared here rather than in Heap-X-Buffer in order to
    // reduce the number of virtual method invocations needed to access these
    // values, which is especially costly when coding small buffers.
    //
    final int[] hb;                  // Non-null only for heap buffers
    final int offset;
    boolean isReadOnly;                 // Valid only for heap buffers

    // Creates a new buffer with the given mark, position, limit, capacity,
    // backing array, and array offset
    //
    IntBuffer(int mark, int pos, int lim, int cap,   // package-private
                 int[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }

    // Creates a new buffer with the given mark, position, limit, and capacity
    //
    IntBuffer(int mark, int pos, int lim, int cap) { // package-private
        this(mark, pos, lim, cap, null, 0);
    }
}
Buffer 常用子类 描述
ByteBuffer 存储字节数据到缓冲区
ShortBuffer 存储字符串数据到缓冲区
CharBuffer 存储字符数据到缓冲区
IntBuffer 存储整数数据据到缓冲区
LongBuffer 存储长整型数据到缓冲区
DoubleBuffer 存储浮点型数据到缓冲区
FloatBuffer 存储浮点型数据到缓冲区

Buffer 中定义了四个属性来提供包含的数据元素。

// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
capacity 容量,即可以容纳的最大数据量;在缓冲区被创建时候就被指定,无法修改
limit 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,但极限是可以修改的
position 当前位置,下一个要被读或者写的索引,每次读写缓冲区数据都会改变该值,为下次读写做准备
Mark 标记当前 position 位置,当 reset 后回到标记位置。

二、Channel 的基本介绍

NIO 的通道类似于流,但有如下区别:

  1. 通道是双向的可以进行读写,而流是单向的只能读,或者写。
  2. 通道可以实现异步读写数据。
  3. 通道可以从缓冲区读取数据,也可以写入数据到缓冲区。

常用的 Channel 有:FileChannel、DatagramChannel、SocketChannel、SocketServerChannel。

1、FileChannel 类

FileChannel 主要用来对本地文件进行 IO 操作,常见的方法有:

  1. public int read(ByteBuffer dst) :从通道中读取数据到缓冲区中。
  2. public int write(ByteBuffer src):把缓冲区中的数据写入到通道中。
  3. public long transferFrom(ReadableByteChannel src,long position,long count):从目标通道中复制数据到当前通道。
  4. public long transferTo(long position,long count,WriteableByteChannel target):把数据从当前通道复制给目标通道。

2、使用 FileChannel 写入文本文件

public class NIOFileChannel {

    public static void main(String[] args) throws IOException {
        String str = "Hello,Java菜鸟程序员";
        //创建一个输出流
        FileOutputStream fileOutputStream = new FileOutputStream("hello.txt");
        //获取通道
        FileChannel channel = fileOutputStream.getChannel();
        //创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(100);
        //写入byteBuffer
        byteBuffer.put(str.getBytes());
        //切换模式
        byteBuffer.flip();
        //写入通道
        channel.write(byteBuffer);
        //关闭
        channel.close();
        fileOutputStream.close();
    }
}

3、使用 FileChannel 读取文本文件

public class NIOFileChannel {
    public static void main(String[] args) throws IOException {
      FileInputStream fileInputStream = new FileInputStream("hello.txt");
      FileChannel channel = fileInputStream.getChannel();
      ByteBuffer byteBuffer = ByteBuffer.allocate(100);
      channel.read(byteBuffer);
      System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));     
      //Hello,Java菜鸟程序员
      channel.close();
      fileInputStream.close();
    }
}

4、使用 FileChannel 复制文件

public class NIOFileChannel {

    public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream = new FileInputStream("hello.txt");
        FileOutputStream fileOutputStream = new FileOutputStream("world.txt");
        FileChannel inChannel = fileInputStream.getChannel();
        FileChannel outChannel = fileOutputStream.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1);
        while (inChannel.read(byteBuffer) != -1) {
            byteBuffer.flip();
            outChannel.write(byteBuffer);
            //清空重置
            byteBuffer.clear();
        }
        fileOutputStream.close();
        fileInputStream.close();
    }
}

5、使用 transferFrom 复制文件

public class NIOFileChannel {

    public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream = new FileInputStream("hello.txt");
        FileOutputStream fileOutputStream = new FileOutputStream("world.txt");
        FileChannel inChannel = fileInputStream.getChannel();
        FileChannel outChannel = fileOutputStream.getChannel();
        //从哪拷贝,从几开始到几结束 对应的还有transferTo()方法.
        outChannel.transferFrom(inChannel, 0, inChannel.size());
        outChannel.close();
        inChannel.close();
        fileOutputStream.close();
        fileInputStream.close();
    }
}

三、Channel 和 Buffer 的注意事项

  1. ByteBuffer 支持类型化的 put 和 get,put 放入什么数据类型,get 就应该使用相应的数据类型来取出,否则可能会产生 ByteUnderflowException 异常。
  2. 可以将一个普通的 Buffer 转换为只读的 Buffer:asReadOnlyBuffer()方法。
  3. NIO 提供了 MapperByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,而如何同步到文件由 NIO 来完成。
  4. NIO 还支持通过多个 Buffer(即 Buffer 数组)完成读写操作,即Scattering(分散)和 Gathering(聚集)。

    • Scattering(分散):在向缓冲区写入数据时,可以使用 Buffer 数组依次写入,一个 Buffer 数组写满后,继续写入下一个 Buffer 数组。
    • Gathering(聚集):从缓冲区读取数据时,可以依次读取,读完一个 Buffer 再按顺序读取下一个。

四、Selector

1、Selector 的基本介绍

  1. Java 的 NIO 使用了非阻塞的 I/O 方式。可以用一个线程处理若干个客户端连接,就会使用到 Selector(选择器)。
  2. Selector 能够检测到多个注册通道上是否有事件发生(多个 Channel 以事件的形式注册到同一个 selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。
  3. 只有在连接真正有读写事件发生时,才会进行读写,减少了系统开销,并且不必为每个连接都创建一个线程,不用维护多个线程。
  4. 避免了多线程之间上下文切换导致的开销。

2、Selector 特点

Netty 的 I/O 线程 NioEventLoop 聚合了 Selector(选择器 / 多路复用器),可以并发处理成百上千个客户端连接。

当线程从某客户端 Socket 通道进行读写时,若没有数据可用,该线程可以进行其他任务。

线程通常将非阻塞 I/O 的空闲时间用于其他通道上执行 I/O 操作,所以单独的线程可以管理多个输入输出通道。

由于读写操作都是非阻塞的,就可以充分提高 I/O 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。

一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构性能、弹性伸缩能力和可靠性都得到极大地提升。

3、Selector 常用方法

public abstract class Selector implement Closeable{

    public static Selector open(); //得到一个选择器对象

    public int select(long timeout); //监控所有注册的通道,当其中的IO操作可以进行时,将对应的selectionkey加入内部集合并返回,参数设置超时时间

    public Set<SelectionKey> selectionKeys(); //从内部集合中得到所有的SelectionKey

}

4、Selector 相关方法说明

  • selector.select()://若未监听到注册管道中有事件,则持续阻塞
  • selector.select(1000)://阻塞 1000 毫秒,1000 毫秒后返回
  • selector.wakeup()://唤醒 selector
  • selector.selectNow(): //不阻塞,立即返回

五、NIO 非阻塞网络编程过程分析

  1. 当客户端连接时,会通过 SeverSocketChannel 得到对应的 SocketChannel。
  2. Selector 进行监听,调用 select()方法,返回注册该 Selector 的所有通道中有事件发生的通道个数。
  3. 将 socketChannel 注册到 Selector 上,public final SelectionKey register(Selector sel, int ops),一个 selector 上可以注册多个 SocketChannel。
  4. 注册后返回一个 SelectionKey,会和该 Selector 关联(以集合的形式)。
  5. 进一步得到各个 SelectionKey,有事件发生。
  6. 再通过 SelectionKey 反向获取 SocketChannel,使用 channnel()方法。
  7. 可以通过得到的 channel,完成业务处理。
SelectionKey 中定义了四个操作标志位: OP_READ表示通道中发生读事件; OP_WRITE—表示通道中发生写事件; OP_CONNECT—表示建立连接; OP_ACCEPT—请求新连接。

NIO 非阻塞网络编程代码示例

public class Server {

    public static void main(String[] args) throws IOException {
        //创建serverSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //绑定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //得到Selector对象
        try (Selector selector = Selector.open()) {
            //把ServerSocketChannel注册到selector,事件为OP_ACCEPT
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            //如果返回的>0,表示已经获取到关注的事件
            while (selector.select() > 0) {
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    //获得到一个事件
                    SelectionKey next = iterator.next();
                    //如果是OP_ACCEPT,表示有新的客户端连接
                    if (next.isAcceptable()) {
                        //给该客户端生成一个SocketChannel
                        SocketChannel accept = serverSocketChannel.accept();
                        accept.configureBlocking(false);
                        //将当前的socketChannel注册到selector,关注事件为读事件,同时给socket Channel关联一个buffer
                        accept.register(selector, SelectionKey.OP_READ,ByteBuffer.allocate(1024));
                        System.out.println("获取到一个客户端连接");
                    //如果是读事件
                    } else if (next.isReadable()) {
                        //通过key 反向获取到对应的channel
                        SocketChannel channel = (SocketChannel) next.channel();
                        //获取到该channel关联的buffer
                        ByteBuffer buffer = (ByteBuffer) next.attachment();
                        while (channel.read(buffer) != -1) {
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, buffer.limit()));
                            buffer.clear();
                        }
                    }
                    iterator.remove();
                }
            }
        }
    }

}
public class Client {

    public static void main(String[] args) throws IOException {
        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置为非阻塞
        socketChannel.configureBlocking(false);
        //提供服务器端的IP和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        //连接服务器
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("连接需要时间,客户端不会阻塞...先去吃个宵夜");
            }
        }
        //连接成功,发送数据
        String str = "hello,Java菜鸟程序员";
        ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
        socketChannel.write(byteBuffer);
        socketChannel.close();
        System.out.println("客户端退出");
    }

}

运行结果

系列文章持续更新,微信搜一搜「傲骄鹿先生 」,回复【面试】有准备的一线大厂面试资料。

猜你喜欢

转载自blog.csdn.net/cyl101816/article/details/126379682