IO模型4-NIO三大核心

1. 缓存区Buffer

用来处理Channel中数据的一种数据结构, 与Channel一致都是双向的, 可读可写

1.1 分类

  • ByteBuffer: 存储字节
  • ShortBuffer:存储短整型
  • IntBuffer:存储整形
  • LongBuffer:存储长整型
  • CharBuffer:存储字符类型
  • FloatBuffer:存储浮点类型
  • DoubleBuffer:存储双精度

因为数据都是用字节的方式传播, 使用最多的是ByteBuffer

1.2 Buffer常用的方法

  1. allocate(int length): 创建一个buffer, 长度为length
  2. put:
    • put(int i): 存放数据, 并将指针指向下一节点
    • put(int index, int i): 存放数据, 指定存放的下标, 指针不变
    • put(int[] src): 存放一个数组, 将数组的数据按顺序放入buffer中, 指针指向结尾
    • put(int[] src, int offset, int length): 存放数据, 存放指定的长度
    • put(IntBuffer src): 存放另一个buffer中的内容
  3. get:
    • get(): 获取数据,并将指针指向下一节点
    • get(int index): 获取指定下标数据
    • public abstract int get(int index); - 获取指定位置的数据,也是子类实现
    • get(int[] dst): 将数据读取到指定的数组中
    • get(int[] dst, int offset, int length): 将buffer中的一定长度的数据读取到指定的数组中
  4. array(): 直接拿出buffer中的所有数据, 返回一个数组
  5. rewind(): 将buffer倒带, 相当于清楚buffer中的数据(具体是将position设置为0, mark作废)
  6. flip(): buffer的读写转换, 原理是修改指针位置, 修改可读buffer长度, 每次写完buffer, 要进行读buffer, 必须执行flip()

1.3 举例IntBuffer

  1. Buffer的创建
    // 通过allocate创建一个 长度为5的buffer, 意为可以存放5个int
    IntBuffer buffer = IntBuffer.allocate(5);
    
  2. 使用Buffer-存放数据
    • 通过循环存放数据
    	IntBuffer buffer = IntBuffer.allocate(5);
        for (int i = 0; i < 5; i++) {
            // 存放数据,如果使用put(int i)方法, 那么buffer指针会自动+1
            // 每次存放数据不用选定下标位置
            buffer.put(i);
        }
    
    • 通过下标存放数据
         // 如果使用put(int index,int i)规定了数据存放的下标, 那么buffer指针不会自动+1
         buffer.put(i, j);
    
    • 通过存放数组
    	// 通过数组存放数据
    	buffer.put(intArray);
    
  3. 使用Buffer-读取数据
    • 使用循环读取数据
    	for (int i = 0; i < 5; i++) {
            // 存放数据,如果使用get()方法, 那么buffer指针会自动+1
            // 每次存放数据不用选定下标位置
            buffer.get(i);
            // 如果使用下标获取数据, 怎么指针不会变化 
        }
    

1.4 ByteBuffer

  • 读写常见的方法
    • 存放数据
    • putInt(int) 当前阶段存放int数据
    • putLong(long) 当前节点存放long数据
    • putShort(short) 当前阶段存放short数据
    • putDouble(double) 当前节点存放double数
    • 取出数据, 数据类型为对应类型正常取出, 不对应类型报异常BufferUnderflowException(出现隐式转换则不会报异常)
    • getInt() 取出当前节点的数据,
    • getLong() 同上, 数据类型为Long
    • getShort() 同上, 数据类型为short
    • getDouble() 同上, 数据类型为Double
  • 针对byteBuffer可存放不同数据类型, 当数据需要进行get的时候也需要get对应的数据
        ByteBuffer byteBf = ByteBuffer.allocate(5);
        byteBf.putInt(1);
        byteBf.putLong(2L);
        byteBf.putShort((short) 3);
        byteBf.putDouble(4D);
        // 如果需要get也要安装对饮顺序get, 否则会出现BufferUnderflowException异常
        byteBf.flip();
        int anInt = byteBf.getInt();
        long aLong = byteBf.getLong();
        short aShort = byteBf.getShort();
        double aDouble = byteBf.getDouble();
    

1.5 只读Buffer

  • 可以将我们的读写buffer, 转换成只读buffer, 防止我们存放是数据被修改
  • 如果我们写入数据那么会出现ReadOnlyBufferException
// 将我们的byteBuffer转换成readOnlyBuffer(只读buffer), 只允许get, 不允许put
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();

1.6 MappedByteBuffer(文件直接在堆外内存修改)

  • 是nio引入的文件内存映射方案
  • 即可以让文件直接在堆外内存进行修改, 不通过系统的数据拷贝到我们的堆中
  • 避免了一次数据拷贝, 提高了性能
        /**
         * 创建一个RandomAccessFile对象
         * RandomAccessFile支持"随机访问"的方式, 不同于流需要一个一个的执行
         * 可以直接跳转到文件的任意位置进行修改
         * 这里通过RandomAccessFile直接获取Channel对象
         */
        RandomAccessFile file = new RandomAccessFile("1.txt", "rw");
        FileChannel channel = file.getChannel();
        /**
         * 参数1: 使用什么模式, 读写
         * 参数2: 可以修改的起始位置
         * 参数3: 映射到虚拟内存的大小
         * 这里是读写模式创建buffer,起始位置为0, 大小为5, 一共可修改5个字节
         */
        MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        map.put(0, (byte) 'L');
        map.put(1, (byte) 'X');
        // index不可为5, 大小设置为5, index最大值为4, 否则会出现越界异常
        map.put(4, (byte) 'C');

1.7 Buffer的分散聚合

  • 分散Scattering
    • 将数据写入到buffer中, 我们可以创建一个buffer数组, 依次写入, 就行一个buffer写满了, 再写另一个buffer
  • 聚合Gathering
    • 从buffer中数据数据, 采用buffer数组, 依次读取, 就行一个buffer读完了, 再读另一个buffer
	    // 创建一个ByteBuffer数组, 并初始化大小为5
        ByteBuffer[] buffers = new ByteBuffer[2];
        Arrays.stream(buffers).forEach(buffer -> buffer = ByteBuffer.allocate(5));
        // 将channel中的数据读取到buffer数组中(分散)
        channel.read(buffers);
        // 将buffer数组中的数据写入到channel中(聚合)
        // 注: 先反转buffer
        Arrays.stream(buffers).forEach(ByteBuffer::flip);
        channel.write(buffers);

1.8 Buffer的四个属性

在这里插入图片描述

  • mark: 标志
    • 就是在我们正常读取或者写入流程中, 在读/写某个节点的时候, 将当前节点标记
    • 标记: 通过buffer.mark(), 方法标记当前节点, 设置mark = position
    • 恢复: 通过buffer.reset() , 将指针位置恢复到标记节点, 设置position = mark
  • position: 本次要读/写的位置, 可以理解为指针, 当本次读/写完成后, 会指向下一节点
  • limit: buffer的可读终点, 就是我们最多能读/写多少个数据, 可在操作中进行修改, position只能读到limit-1的位置
  • capacity: 容量, 指当前buffer能存放的最大数据量, 只有在创建buffer的时候给定

buffer的创建,读,写,flip操作对4个属性的修改

  • 创建buffer
    • buffer会将我们的容量capacity设置为给定的值
    • 并将limit设置为容量大小
  • 向buffer中写入数据
    • 会修改position的值, 将position的值设置为下一节点的下标
  • flip操作
    • 将position的值赋值给limit, 表示读buffer的时候最多能读到上次写入buffer的最末position位置
    • 修改position的值, 将其设置为0
    • 设置mark值为-1, 就是没标记的意思
      在这里插入图片描述
  • 从buffer中读取数据
    • 此时position为0
    • 会修改position的值, 将position的值设置为下一节点的下标

2. 通道Channel

channel可以看做是一个socket连接, 可以看做是一个流, 与流不同的是channel是双向的

  • 通道可以同时进行读写, 而流只能读或者写
  • 通道可以实现异步读写数据
  • 通道可以读buffer中的数据, 也可以写数据到buffer中
  • Channel是NIO包里的一个接口

2.1 常用的Channel类

  1. FileChannel
    • 常用与文件的读写, 实现类是FileChannelImpl
    • 通过FileChannel.open()创建FileChannel, 或者通过file流对象获取Channel(getChannel())
      • open创建
      	/**
           * FileChannel.open(Path path, OpenOption... options)
           * 参数1: Path对象, 定义一个文件地址信息
           * 参数2: 可变参数, 指定文件读写模式, 常用StandardOpenOption指定
           * 这里指定读写模式
           */
          FileChannel channel = FileChannel.open(Paths.get("D://1.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE);
      
      • 通过流对象获取
              /**
              * 通过流对象获取的Channel只有读或者写的模式
              *  - 输入流只有读模式
              *  - 输出流只有写模式
              *  - 注意: 写模式会出现替换和追加的方式, 
              * 		可在流对象的第二个参数boolean append指定(true/false)
              */
              // 通过输入流创建Channel
              FileInputStream in = new FileInputStream("D://1.txt");
              FileChannel inChannel = in.getChannel();
              // 通过输出流创建Channel
              FileOutputStream out = new FileOutputStream("D://1.txt");
              FileChannel outChannel = out.getChannel();
      
    • FileChannel创建时会指定对当前文件是读模式/写模式/读写模式
  2. DatagramChannel
    • 常用语UPD数据的读写
  3. ServerSockerChannel
    • 用于TCP数据的读写, ServerSocketChannel主要用于监听获取SocketChannel
  4. SocketChannel
    • 用于TCP数据的读写, socketChannel主要是对数据进行读写

2.2 FileChannel文件通道

  • 常用的方法
    • read(Buffer buffer): 将通道中的数据读到buffer中
    • write(Buffer buffer): 将buffer中的数据写入到通道中
    • transferFrom(ReadableByteChannel src, long position, long count)
      • 从目标通道(参数1)复制数据到当前通道
      • 指定从哪个位置开始position, 指定读多少数据(count)
      • 零拷贝, 性能高, 在windwos中每次只能发送8M, 需要循环发送
    • transferTo(long position, long count, WritableByteChannel target)
      • 从当前通道复制数据到目标通道(参数3), 数据长度同上
      • 零拷贝, 性能高, 在windwos中每次只能发送8M, 需要循环发送
  • 读取数据及写入数据
        // 通过输入流创建Channel
        FileInputStream in = new FileInputStream("D://1.txt");
        FileChannel inChannel = in.getChannel();
        // 通过输出流创建Channel
        FileOutputStream out = new FileOutputStream("D://2.txt");
        FileChannel outChannel = out.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(10);
        // 循环操作
        while (true) {
            // 将数据从1.txt中读出到buffer中
            int read = inChannel.read(buffer);
            // read返回值为-1 表示读完了
            if (read == -1) {
                break;
            }
            // 将buffer中的数据写入到2.txt中
            buffer.flip();
            outChannel.write(buffer);
        }
        // 最后关闭Channel对象/流对象
        ...
  • 拷贝文件,使用transferFrom/transferTo
        // 通过输入流创建Channel
        FileInputStream in = new FileInputStream("D://1.txt");
        FileChannel inChannel = in.getChannel();
        // 通过输出流创建Channel
        FileOutputStream out = new FileOutputStream("D://3.txt");
        FileChannel outChannel = out.getChannel();
		// 从inChannel中把数据拷贝到outChannel中
        outChannel.transferFrom(inChannel, 0, inChannel.size());
        // 最后关闭Channel对象/流对象
        ...

2.3 ServerSocketChannel,SocketChannel

  • ServerSocketChannel 可以理解为ServerSocket, 具体是监听新的客户端
    • 通过ServerSocketChannel监听是否有连接,并获取连接SocketChannel
    • 处理SocketChannel数据
    • 常用的API
      • open(): 获得一个ServerSocketChannel通道
      • bind(SocketAddress address): 绑定要监听的端口
      • configureBlocking(): 设置阻塞模式, false非阻塞, true阻塞
      • accept(): 接收一个连接, 并返回, 设置非阻塞后, 没有连接将返回空
      • register(Selector sel, int ops): 将当前通道注册到一个选择器上, 并指定监听时事件(这里常用OP_ACCEP事件)
  • SokcerChannel可以理解为Socket, 具体是做读写数据用的
    • 通过Channel的方法, 读写其中的数据
    • 将缓存区buffer中的数据写入到通道, 或者将通道中的数据写入到buffer缓存区
    • 常用的API:
      • open(): 得到一个SocketChannel通道
      • configureBlocking(): 设置阻塞模式, false非阻塞, true阻塞
      • connect(SocketAddress address): 连接服务器, 一般通过accept的方法是已经连接成功的
      • finishConnect(): 如果connect()连接失败, 就要通过该方法连接
      • write(Buffer buffer): 将缓存区的数据写入通道
      • read(Buffer buffer): 将通道中的数据写入缓存区
      • register(Selector sel, int ops): 将当前通道注册到一个选择器上, 并指定监听时事件(常用读写事件, OP_WRITE|OP_READ)
		// 获得一个serverSocketChannel, 注册功能在后面使用
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 绑定要监听的端口
        serverSocketChannel.bind(new InetSocketAddress(9090));
        // 设置非阻塞, 如果accept没有连接, 返回null
        serverSocketChannel.configureBlocking(false);
        // 循环处理获取socket
        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            if (socketChannel == null){
                break;
            }
            // 设置socketChannel非阻塞
            socketChannel.configureBlocking(false);
            // 定义一个buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 循环读取数据, 如果1024能装下数据 只有一次循环, 否则循环拿出所有数据
            while (true) {
                int read = socketChannel.read(buffer);
                // read == -1 表示读完了
                if (read == -1) {
                    break;
                }
                // buffer反转
                buffer.flip();
                // 数据处理, 这里做打印
                System.out.println(new String(buffer.array(),0,buffer.limit()));
            }

        }

3. 选择器Selector

  • NIO基于事件驱动, Selector可以检测注册到Selector上的连接是否有事件发生
  • 如果有事件发生, 就可以获取事件, 然后对每个事件进行处理
  • 这样就形成了一个线程管理多个通道(IO多路复用)
  • 只有事件发生才进行读写, 这样大大降低了性能开销
  • 而且使用一个线程管理多个连接(区别于BIO), 就不用维护多个线程了
  • 一个线程对应一个Selector, 一个Selector对应多个连接

3.1 Selector常用方法

  • open(): 获得一个Selector对象
  • select():
    • 监控所有注册的通道上是否有事件发生, 返回事件数目, 这是阻塞方法
    • 如果有事件发生, 则将对应的连接key存入到我们Selector对象的内部集合中
    • 可以使用提供的非阻塞方法
    • select(long timeout): 指定超过时间, 超过时间没有事件返回一个0
    • selectNow(): 马上返回, 如果没有事件发生立马返回0
  • selectedKeys():
    • 获取有事件发生的集合, 通过select(), 将发生事件的连接加入到了selectedKeys中
    • 通过selectorKey可以反向获取SocketChannel

3.2 SelectorKey

  • 表示Selector和SocketChannel的一种注册关系, 共4种
    • OP_ACCEPT: 表示有新的连接, 可以进行accpet, 值为16
    • OP_CONNECT: 表示连接成功, 值为8
    • OP_WRITE: 表示读操作, 值为4
    • OP_READ: 表示写操作, 值为1
  • keys存放的就是注册的key值, 通过keys()方法获取
  • publicSelectedKeys存放的就是我们有事件发生的key, 通过selectedKeys()方法获取
    在这里插入图片描述
  • 通过拿到selectedKey(), 获得我们的连接, 然后进行数据处理
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值