深入理解NIO三大核心

本文深入讲解NIO(Non-blocking I/O)的三大核心组件:缓冲区(Buffer)、选择器(Selector)和通道(Channel),并详细阐述了它们在非阻塞I/O模型中的作用和工作原理。文章通过代码示例展示了如何使用这些组件进行高效的数据读写。
摘要由CSDN通过智能技术生成

NIO三大核心

  • 在NIO中有三个核心对象
  • 缓冲区(Buffer)
  • 选择器(Selector)
  • 通道(channel)

什么是缓冲区Buffer?

  • 缓冲区实际上是一个容器对象,本质上是一个数组
  • 在NIO库中,几乎所有的数据都是使用缓冲区处理的
  • 在进行读写操作的时候,首先会经过缓冲区
  • 在面向流的IO系统中,所有的数据都是 读写 到Stream对象中

类继承关系

  • 在NIO库中,顶层抽象类Buffer定义了缓冲区的规范。它提供了各个基本数据类型对应的buffer
    在这里插入图片描述

工作原理

  • 在缓冲区中有三个重要的属性
  • position capacity limit
postiton:指定下一个将要被写入或者读取的元素索引,调用put/get方法时更新,初始化为0
limit   :指定剩余数据容量/剩余可存储数据空间
capacity:可以存储在缓冲区的最大数据容量
  • 一个简单示例
public class BufferPlay {

    public static void main(String[] args) throws Exception {
        //  文件IO处理   使用文件输入流读取文件  把数据从磁盘读取到内存—>内核缓冲区->进程缓冲区
        FileInputStream fis = new FileInputStream("filePath");
        //   创建文件的操作管道
        FileChannel channel = fis.getChannel();
        //  分配一个大小固定的缓冲区,即一个固定长度的byte数组
        ByteBuffer buffer = ByteBuffer.allocate(10);
        output("初始化", buffer);
        //  进行一次读操作
        channel.read(buffer);
        output("调用channel.read()", buffer);
        //  锁定操作范围
        buffer.flip();
        output("调用buffer.flip()", buffer);
        //  判断有无可读数据
        while (buffer.remaining() > 0) {
            byte b = buffer.get();
        }
        output("调用buffer.get()", buffer);
        //  复位
        buffer.clear();
        output("调用buffer.clear()", buffer);
        // 关闭管道
        channel.close();
    }

    private static void output(String string, ByteBuffer buffer) {
        System.out.println(string + " : ");
        // 容量,数组大小
        System.out.print("capacity: " + buffer.capacity() + ", ");
        // 当前操作数据所在的位置,也可以叫做游标
        System.out.print("position: " + buffer.position() + ", ");
        // 锁定值,flip,数据操作范围索引只能在position - limit 之间
        System.out.println("limit: " + buffer.limit());
        System.out.println();
    }

}
//  运行结果
初始化 : 
capacity: 10, position: 0, limit: 10

调用channel.read() : 
capacity: 10, position: 8, limit: 10

调用buffer.flip() : 
capacity: 10, position: 0, limit: 8

调用buffer.get() : 
capacity: 10, position: 8, limit: 8

调用buffer.clear() : 
capacity: 10, position: 0, limit: 10
  • 图解

在这里插入图片描述

缓冲区分配

public class BufferWrap {

    public void myMethod() {

        /*  调用 allocate(int i) 方法相当于创建了一个指定大小的数组,并封装为缓冲区对象
         *  也可以使用  wrap(byte[] arr) 方法,把一个现有的数组封装为缓冲区对象
         *
         */
        //  分配指定大小的缓冲区
        ByteBuffer allocate = ByteBuffer.allocate(10);

        //  定义一个 Byte 数组  容量为10
        byte[] arr = new byte[10];
        ByteBuffer result = ByteBuffer.wrap(arr);
    }
}

缓冲区分片

public class BufferSlice {

    /*
     *    缓冲区分片:在现有缓冲区上切出一片来作为一个新的缓冲区,即子缓冲区
     *               现有缓冲区与字缓冲区在底层数组层面是数据共享的
     *               子缓冲区相当于现有缓冲区的一个视图窗口
     */

    public static void main(String[] args) {
        //  分配一个缓冲区大小为10的数组
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //  向缓冲区中写入数据  0-9
        for (int i = 0; i < buffer.capacity(); ++i) {
            buffer.put((byte) i);
        }
        //  创建子缓冲区  设置字缓冲区的位置 为 原缓冲区位置的  3-6  容量为4
        buffer.position(3);
        buffer.limit(7);
        ByteBuffer slice = buffer.slice();
        // 改变子缓冲区的内容
        for (int i=0; i<slice.capacity(); ++i) {
            byte b = slice.get( i );
            b *= 10;
            slice.put(i, b);
        }
        //  重新设置  position  和  limit  的位置,方便读取验证数据
        buffer.position(0);
        buffer.limit(buffer.capacity());

        while (buffer.remaining() > 0) {
            System.out.print(buffer.get()+" ");
        }
    }
}

只读缓冲区

  • 只读缓冲区不允许写入数据
//  创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
//  创建一个只读缓冲区-从原有缓冲区基础上复制
ByteBuffer readOnly = buffer.asReadOnlyBuffer();
  • 只读缓冲区与原缓冲区共享数据,原缓冲区内容发生变化,只读缓冲区内容随之变化

  • 只读缓冲区不能转换为常规缓冲区,修改只读缓冲区的内容会报异常

直接缓冲区

  • 为加快IO速度,使用一种特殊方式为其分配内存的缓冲区
  • 操作系统在进行IO操作之前,会尝试跳过中间缓冲区,直接将内容分配给直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect();

内存映射

  • 内存映射是一种读写文件数据的方法
  • 速度优于常规基于流或者通道的IO
  • 通过使文件中的数据出现在内存数组中来完成,一般只有文件中实际读写的内容部分才会映射到内存中
public class BufferMapped {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile("filePath", "rw");
        FileChannel channel = raf.getChannel();

        //  把缓冲区和文件系统进行映射关联
        MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 8);
        //  修改数据——执行完毕在对应文件中查看
        map.put(1, (byte) 100);
        map.put(7, (byte) 101);

        raf.close();
        channel.close();
    }
}

选择器Selector

传统的Server/Client

  • 传统的Server/Client交互基于TPR (Thread per Request)
  • 服务器会为每一个客户端请求建立一个线程,由该线程负责处理一个用户请求
  • 请求数量很多的情况下,系统难以承受

NIO模型中采用的方式

  • NIO中的非阻塞IO,采用了基于Reactor的工作模式,IO调用不会被阻塞
  • 通过注册特定的IO事件,如可读数据到达、新的套接字连接等。发生特定事件时,系统返回通知
  • 实现非阻塞IO的核心对象就是选择器Selector
  • Selector是注册各种IO事件的地方,对不同事件发生做出相对响应

图片引用自咕泡学院Tom老师的课堂笔记
图片引用自咕泡学院Tom老师的课堂笔记

  • 当有事件发生时,可以从Selector中获取相应的SelectionKey

  • 从Selectionkey中可以找到事件发生的具体SelectableChannel

  • SelecttableChannel处理完毕,可对key重新进行注册

  • 代码示例

public class NioServerSocket {

    /**
     * selector 选择器
     */
    private Selector selector;

    /**
     * 缓冲区
     */
    private ByteBuffer buffer = ByteBuffer.allocate(1024);

    private int port = 8080;

    public NioServerSocket(int port) {
        //  初始化   轮询器
        try {
            this.port = port;
            ServerSocketChannel server = ServerSocketChannel.open();

            //  socket 服务绑定到目标  IP:PORT  默认为本地IP即 localhost
            server.bind(new InetSocketAddress(this.port));

            //NIO:BIO的升级版本   兼容BIO  NIO模型默认采用阻塞式
            //  设置false:非阻塞
            server.configureBlocking(false);

            //  可以接收请求了
            this.selector = Selector.open();

            // ON_ACCEPT :设置为可接收请求状态
            server.register(this.selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public void listen() {
        System.out.println("listen on "+ this.port + "");
        //轮询主线程
        try {
            while (true) {
                //  接收所有的请求  select()方法会阻塞,直到拿到一个key
                this.selector.select();
                Set<SelectionKey> selectionKeys = this.selector.selectedKeys();
                //  不断地迭代
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                //  同步体现:每次只能拿到一个key,每次只能处理一种状态
                while (iterator.hasNext()) {
                    //  每一个key代表一种状态
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    process(key);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void process(SelectionKey key) throws IOException {
        //  针对每一种 key 的状态给一个反应
        if(key.isAcceptable()) {
            ServerSocketChannel severChannel = (ServerSocketChannel) key.channel();
            SocketChannel channel = severChannel.accept();
            channel.configureBlocking(false);
            //  当数据准备就绪的时候,将状态改为可读
            key = channel.register(this.selector, SelectionKey.OP_READ);
        } else if (key.isReadable()){
            //  key.channel  从多路复用器中拿到客户端的引用
            //  多路复用体现在,key.channel 每一次拿到的对象都是同一个引用
            SocketChannel channel = (SocketChannel) key.channel();
            int length = channel.read(this.buffer);
            if (length > 0) {
                this.buffer.flip();
                String content = new String(this.buffer.array(), 0, length);
                key = channel.register(this.selector, SelectionKey.OP_WRITE);
                key.attach(content);
                System.out.println("读取内容:"+content);
            }
        } else if (key.isWritable()) {
            SocketChannel channel = (SocketChannel) key.channel();
            String content = (String) key.attachment();
            channel.write(ByteBuffer.wrap(("输出"+content).getBytes()));
            channel.close();
        }
    }

    public static void main(String[] args) {
        new NioServerSocket(8080).listen();
    }

}

通道Channel

  • 数据通过Buffer对象来处理
  • 数据 和 Buffer对象交互中,通过通道Channel来完成

NIO中Channel结构

  • NIO中所有的通道对象都实现了Channel接口

在这里插入图片描述

Channel的作用

  • 使用NIO读写数据
从  InputStream 中获取 Channel
创建Buffer
将数据通过Channle 读/写 到Buffer
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值