NIO的基本介绍、核心原理和三大件的使用

一、NIO 的基本介绍

  1. 同步非阻塞的IO(non-blocking IO)
  2. 三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
  3. NIO面向缓冲区编程 ,增加了操作的灵活性。
  4. NIO读和写都是非阻塞的,当 读操作–>当前资源未就绪 或者 写操作–>当前未完全写入 时,当前线程都可以做其他处理。也就是说,多个请求发过来,不必分配相同线程数去处理请求(例如1000个请求,根据实际情况可以分配5-10个线程处理即可)。
  5. NIO 和 BIO 对比
    • BIO 以流的方式处理数据,而 NIO 以缓冲区的方式处理数据,缓冲区 I/O 的效率比流 I/O 高很多
    • BIO 是阻塞的,NIO则是非阻塞的
    • BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据 总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的 事件(比如:连接请求, 数据到达等),因此使用单个线程就可以监听多个客户端通道

二、NIO的核心原理

三大组件的原理图:

请添加图片描述

关系说明:

1. 每个 channel 都会对应一个 Buffer 
2.  Selector 对应一个线程, 一个线程对应多个 channel(连接 )
3. 每个 channel 都注册到 Selector选择器上 
4. . Selector不断轮询查看Channel上的事件, 事件是通道Channel非常重要的概念 
5.  Selector 会根据不同的事件,完成不同的处理操作 
6. Buffer 是一个内存块 , 底层有一个数组 
7. 数据的读取写入是通过 Buffer, 这个和 BIO不同 , BIO 中是输入输出流, 不能双向,但是 NIO 的 Buffer 是可以读也可以写 , channel 是双向的. 

三、NIO的三大件

1、缓冲区(Buffer)

  • 基本介绍:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个数组,该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的 状态变化情况。Channel 提供从网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer.

  • 常用API介绍

    1. 在 NIO 中,Buffer是一个顶层父类,它是一个抽象类, 类的层级关系图,常用的缓冲区分别对应 byte,short, int, long,float,double,char 7种.

      请添加图片描述

    2. Buffer对象创建

      • 接口:
      方法名说明
      static ByteBuffer allocate(长度)创建byte类型的指定长度的缓冲区
      static ByteBuffer wrap(byte[] array)创建一个有内容的byte类型缓冲区
      • 引入junit包测试,测试代码如下:
          @Test
          public void testCreateBuffer(){
              //创建一个指定长度的缓冲区, 以ByteBuffer为例
              ByteBuffer byteBuffer = ByteBuffer.allocate(4);
              for (int i = 0; i < 4; i++) {
                  System.out.println(byteBuffer.get());
              }
      
              System.out.println("-------------创建一个有内容的缓冲区---------------");
              ByteBuffer byteBuffer1 = ByteBuffer.wrap("abc".getBytes());
              for (int i = 0; i < 3; i++) {
                  System.out.println((char) byteBuffer1.get());
              }
          }
      
      • 测试结果:

      请添加图片描述

    3. Buffer添加数据

      • 接口:
      方法名说明
      int position()/position(int newPosition)获得当前要操作的索引/修改当前要操作的索引位置
      int limit()/limit(int newLimit)最多能操作到哪个索引/修改最多能操作的索引位置
      int capacity()返回缓冲区的总长度
      int remaining()/boolean hasRemaining()还有多少能操作索引个数/是否还有能操作
      put(byte b)/put(byte[] src)添加一个字节/添加字节数组
      • 测试数据添加:
      @Test
      public void test1(){
          //创建一个指定长度的缓冲区
          ByteBuffer allocate = ByteBuffer.allocate(10);
          allocate.put("0123".getBytes());
          System.out.println("position:" + allocate.position());//4
          System.out.println("limit:" + allocate.limit());//10
          System.out.println("capacity:" + allocate.capacity());//10
          System.out.println("remaining:" + allocate.remaining());//6
      }
      
      • 测试结果:

      请添加图片描述

    4. Buffer读取数据

      • 接口

        方法名介绍
        flip()写切换读模式 limit设置为position位置, position设置为0
        get()读一个字节
        get(byte[] dst)读多个字节
        get(int index)读指定索引的字节
        rewind()将position设置为0,可以重复读
        clear()切换写模式,position设置为0,limit设置为capacity
        array()将缓冲区转换为字节数组返回
      • 测试读模式,在test1方法中添加代码

        //切换读模式
        System.out.println("读取数据--------------");
        allocate.flip();
        System.out.println("position:" + allocate.position());//4
        System.out.println("limit:" + allocate.limit());//10
        System.out.println("capacity:" + allocate.capacity());//10
        System.out.println("remaining:" + allocate.remaining());//6
        for (int i = 0; i < allocate.limit(); i++) {
            System.out.print(char)allocate.get());
        }
        
      • 测试结果

      请添加图片描述

      • 测试读取指定索引字节
      //读取指定索引字节
      System.out.println("读取指定索引字节--------------");
      System.out.println(allocate.get(1));
      
      • 测试结果

      请添加图片描述

      • 测试读取多个字节
      System.out.println("读取多个字节--------------");
      // 重复读取
      allocate.rewind();
      byte[] bytes = new byte[4];
      allocate.get(bytes);
      System.out.println(new String(bytes));
      
      • 测试结果

        请添加图片描述

      • 测试将缓冲区转化字节数组返回

      // 将缓冲区转化字节数组返回
      System.out.println("将缓冲区转化字节数组返回--------------");
      byte[] array = allocate.array();
      System.out.println(new String(array));
      
      • 测试结果

        请添加图片描述

      • 测试切换写模式,覆盖之前索引所在位置的值

      // 切换写模式,覆盖之前索引所在位置的值
      System.out.println("写模式--------------");
      allocate.clear();
      allocate.put("abc".getBytes());
      System.out.println(new String(allocate.array()));
      
      • 测试结果

      请添加图片描述

      注意

      1. capacity:容量(长度)limit: 界限(最多能读/写到哪里)posotion:位置(读/写 哪个索引)。
      2. 获取缓冲区里面数据之前,需要调用flip方法 。
      3. 再次写数据之前,需要调用clear方法,但是数据还未消失,等再次写入数据,被覆盖了才会消失。

2、通道(Channel)

  • 基本介绍

    1. 常 用 的Channel实现类类 有 :FileChannel , DatagramChannel ,ServerSocketChannel和 SocketChannel 。FileChannel 用于文件的数据读写, DatagramChannel 用于 UDP 的数据读写, ServerSocketChannel 和SocketChannel 用于 TCP 的数据读写。

    2. SocketChannel 与ServerSocketChannel 类似 Socke和ServerSocket,可以完成客户端与服务端数据的通信工作。

  • 使用实现

  1. 服务端实现

    • 步骤

      1. 打开一个服务端通道
      2. 绑定对应的端口号
      3. 通道默认是阻塞的,需要设置为非阻塞
      4. 检查是否有客户端连接,当有客户端连接时会返回对应的通道
      5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
      6. 给客户端回写数据
      7. 释放资源
    • 代码实现

      public class Server {
      
          public static void main(String[] args) throws IOException, InterruptedException {
              //1. 打开一个服务端通道
              ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
              //2. 绑定对应的端口号
              serverSocketChannel.bind(new InetSocketAddress(9988));
              //3. 通道默认是阻塞的,需要设置为非阻塞
              serverSocketChannel.configureBlocking(false);
              System.out.println("server start...");
      
              while(true){
                  //4. 检查是否有客户端连接 有客户端连接会返回对应的通道
                  SocketChannel socketChannel = serverSocketChannel.accept();
                  if (socketChannel == null){
                      System.out.println("没有客户端连接...可以处理其他事情");
                      Thread.sleep(2 * 1000);
                      continue;
                  }
      
                  //5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
                  ByteBuffer allocate = ByteBuffer.allocate(1024);
                  /*
                      返回值:
                      正数: 表示本地读到有效字节数
                      0: 表示本次没有读到数据
                      -1: 表示读到末尾
                   */
                  int len = socketChannel.read(allocate);
                  System.out.println("客户端消息:" + new String(allocate.array(), 0,
                          len, StandardCharsets.UTF_8));
      
                  //6. 给客户端回写数据
                  socketChannel.write(ByteBuffer.wrap("hello,client...".getBytes(StandardCharsets.UTF_8)));
      
                  //7. 释放资源
                  socketChannel.close();
              }
      
          }
      }
      
  2. 客户端实现

    • 步骤
    1. 打开通道
    2. 设置连接IP和端口号
    3. 发送数据
    4. 读取服务器写回的数据
    5. 释放资源
    • 代码实现

      public class Client {
      
          public static void main(String[] args) throws IOException {
              //1. 打开通道
              SocketChannel socketChannel = SocketChannel.open();
      
              //2. 设置连接IP和端口号
              socketChannel.connect(new InetSocketAddress("127.0.0.1", 9988));
      
              //3. 写出数据
              socketChannel.write(ByteBuffer.wrap("hello,server...".getBytes(StandardCharsets.UTF_8)));
      
              //4. 读取服务器写回的数据
              ByteBuffer allocate = ByteBuffer.allocate(1024);
              int len = socketChannel.read(allocate);
              System.out.println("服务端消息:" +
                      new String(allocate.array(), 0, len, StandardCharsets.UTF_8));
      
              //5. 释放资源
              socketChannel.close();
          }
      }
      
  3. 测试效果

    请添加图片描述

3、Selector (选择器)

  • 思考:为什么要有Selector选择器?

    请添加图片描述

    • 每一个channel对应一个socket连接,当有客户端连接时,都会将对应的 channel注册到Selector上,当channel有事件发生时(连接、读/写),Selector会轮询到,然后在处理对应的事件。

    • Selector只需要一个线程去管理,也就是管理多个连接和请求。

  • Selector选择器处理流程

    请添加图片描述

  • 常用API介绍

    1. Selector 类是一个抽象类

    请添加图片描述

    • 常用接口:
    方法名介绍
    open()得到一个选择器对象
    select()阻塞监控所有注册的通道,当有对应的事件操作时, 会将SelectionKey放入 集合内部并返回事件数量
    select(1000)阻塞 1000 毫秒,监控所有注册的通道,当有对应的事件操作时, 会将 SelectionKey放入集合内部并返回
    selectedKeys()返回存有SelectionKey的集合
    1. SelectionKey

      请添加图片描述

      • 常用方法
    方法名介绍
    isAcceptable()是否是连接继续事件
    isConnectable()是否是连接就绪事件
    isReadable()是否是读就绪事件
    isWritable()是否是写就绪事件
  • SelectionKey中定义的4种事件

事件描述
SelectionKey.OP_ACCEPT接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
SelectionKey.OP_CONNECT连接就绪事件,表示客户端与服务器的连接已经建立成功
SelectionKey.OP_READ读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
SelectionKey.OP_WRITE写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)
  • Selector编码

    • 服务端:

      • 实现步骤

        1. 打开一个服务端通道

        2. 绑定对应的端口号

        3. 通道默认是阻塞的,需要设置为非阻塞

        4. 创建选择器

        5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT

        6. 检查选择器是否有事件

        7. 获取事件集合

        8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()

        9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ

        10. 判断是否是客户端读就绪事件SelectionKey.isReadable()

        11. 得到客户端通道,读取数据到缓冲区

        12. 给客户端回写数据

        13. 从集合中删除对应的事件, 因为防止二次处理.

      • 服务端代码

        public class SelectorServer {
        
        
            public static void main(String[] args) throws IOException {
                //1. 打开一个服务端通道
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                //2. 绑定对应的端口号
                serverSocketChannel.bind(new InetSocketAddress(9988));
                //3. 通道默认是阻塞的,需要设置为非阻塞
                serverSocketChannel.configureBlocking(false);
                //4. 创建选择器
                Selector selector = Selector.open();
                //5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
                serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
                System.out.println("server start...");
        
                while (true){
        
                    //6. 检查选择器是否有事件
                    int select = selector.select(2000);
                    if (select == 0){
                        System.out.println("当前没有事件待处理...");
                        continue;
                    }
                    //7. 获取事件集合
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()){
                        //8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
                        SelectionKey key = iterator.next();
                        if (key.isAcceptable()){
                            //9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
                            SocketChannel socketChannel = serverSocketChannel.accept();
                            System.out.println("有客户端连接...");
                            //将通道必须设置为非阻塞的状态,因为selector选择器需要轮询监听每个通道的事件
                            socketChannel.configureBlocking(false);
                            //指定监听事件为OP_READ 读就绪事件
                            socketChannel.register(selector, SelectionKey.OP_READ);
                        }
                        //10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
                        if (key.isReadable()){
        
                            //11. 得到客户端通道,读取数据到缓冲区
                            SocketChannel socketChannel = (SocketChannel) key.channel();
                            ByteBuffer allocate = ByteBuffer.allocate(1024);
                            int read = socketChannel.read(allocate);
                            if (read > 0){
                                System.out.println("客户端消息:" + new String(allocate.array(), 0, read
                                        , StandardCharsets.UTF_8));
                                //12. 给客户端回写数据
                                socketChannel.write(ByteBuffer.wrap("hello,server...".getBytes()));
                                socketChannel.close();
                            }
                        }
                        //13. 从集合中删除对应的事件, 因为防止二次处理.
                        iterator.remove();
                    }
                }
        
            }
        }
        
    • 客户端没有变化,仍然使用channel下的客户端编码

    • 测试结果:

请添加图片描述

总结

  • NIO,同步非阻塞的I/O模型,具备Buffer、Channel、Selector三大组件。
  • 由于使用缓冲区,比BIO的流效率高,且双向处理数据更方便。
  • 由于具有Selector组件,可以轮询各个客户端连接对应的SocketChannel注册的事件,因此单线程可以处理多个连接请求,比BIO一个请求占用一个线程要高效的多。
  • 扩展:原生的NIO仍然具有问题,如:编程复杂,要求较高 且 还有JDK NIO的bug存在。目前主流使用Netty框架提供异步的、基于事件驱动的网络应用程序框架。关于Netty的具体介绍敬请查看Netty基本介绍 和 线程模型
  • 3
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值