Java NIO 中的 selector  channel  buffer 

一  buffer 缓冲区

  NIO的Buffer(缓冲区)本质上是一个内存块,既可以写入数据,也可以从中读取数据。 实际上是一个容器,一个连续数组。读写的数据都必须经过Buffer。

  Buffer类是一个抽象类,且 Buffer 类是一个非线程安全类。

  缓冲区类:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、 MappedByteBuffer。

        

      • 前7种Buffer类型,覆盖了Java基本数据类型。第8种类型 MappedByteBuffer是专门用于内存映射的一种ByteBuffer类型。
      • 不同的Buffer子类,其能操作 的数据类型能够通过名称进行判断,比如IntBuffer只能操作Integer类型的对象。
      • 实际上,使用最多的还是ByteBuffer二进制字节缓冲区类型。

  Buffer类 中 重要的成员属性:

    capacity(容量):表示内部容量的大小。 属性一旦初始化,就不能再改变。

    position(读写位置):表示当前的位置。position属性的值与缓冲区的读写模式有关。 在不同的模式下,position属性值的含义是不同的,在缓冲区进行读写的模式改变时,position 值会进行相应的调整。使用(即调用)flip()   翻转方法。

        在写模式下:(1)进入到写入模式时,position值为0,表示当前的写入位置为从头开始。 (2)每当一个数据写到缓冲区之后,position会向后移动到下一个可写的位置。 (3)初始的position值为0,最大可写值为limit–1。当position值达到limit时,缓冲区就 已经无空间可写了。

        在读模式下:(1)当缓冲区刚开始进入到读取模式时,position会被重置为0。 (2)从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到 下一个可读的位置。 (3)在读模式下,limit表示可以读上限。position的最大值,为最大可读上限limit,当 position达到limit时,表明缓冲区已经无数据可读。  

        在从写入模式到读取模式的flip翻转过程中,position和limit属性值会进行调整,具体的 规则是: (1)limit属性被设置成写入模式时的position值,表示可以读取的最大数据位置; (2)position由原来的写入位置,变成新的可读位置,也就是0,表示可以从头开始读。            

    limit(读写的限制):表示可以写入或者读取的最大上限。

    mark (读写位置的临时备份):读位置或者写位置的一个备份,供后续恢复时使用。

  以下是简单的代码示例:

    //buffer的示例
    public static void nioBufferEg01(){
        //创建一个intbuffer
        //allocate() 为创建缓冲区,大小capacity为10,
        IntBuffer intBuffer = IntBuffer.allocate(10);
        //put 方法添加元素
        intBuffer.put(10);
        intBuffer.put(11);
        intBuffer.put(12);
        intBuffer.put(13);
     //intBuffer.put(0);
        //读写切换
        intBuffer.flip();
        //读出数据
        //用get方法每次从position的位置读取一个数据 可以指定下标读取哪一个元素 get(i)
        //hasRemaining() 方法表示 当前位置和限制之间是否有任何元素。当且仅当此缓冲区中至少剩余一个元素时为true。
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
        //clear 方法是将 position  与 limit 值 变为初始化的 0 与 10 与 数组中的 原有元素无关
        intBuffer.clear();
        //这里只是替换了第一个元素的值
        intBuffer.put(20);
        intBuffer.flip();
        //翻转后 再读取也只是读取 第一个元素,如果手动修改 limit 值为2,则会获取到 原有元素下标为1的 11
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }

  

  capacity大小 为固定10  ,存入4个 也是有10个元素,只是 翻转 读取的时候 limit 变为 position 的值 ,position变为0 从头开始读取。

  

  这里可以看到 执行 clear 方法后 将 position  与 limit 值 变为初始化的 0 与 10 ,put 一个元素  翻转后 只是 limit 变为了 1  
  这时候手动修改 limit 值 2 就可以 取到 下标为 0,1 的 两个元素,一个是新值,一个是旧值。所以 clear 方法与原数组内容无关

  

   总体来说,使用JavaNIOBuffer类的基本步骤如下:

    (1)使用创建子类实例对象的allocate()方法,创建一个Buffer类的实例对象。

    (2)调用put()方法,将数据写入到缓冲区中。

    (3)写入完成后,在开始读取数据前,调用Buffer.flip()方法,将缓冲区转换为读模式。

    (4)调用get()方法,可以从缓冲区中读取数据。

    (5)读取完成后,调用Buffer.clear()方法,将缓冲区转换为写入 模式,可以继续写入。

二  channel 通道

   Channel 和 BIO中 Stream(流) 是差不多的。在BIO中,一个网络连接会关联到两个流:输入流(InputStream),输出流(OutputStream),通过这两个流,进行输入和输出的操作。

   在NIO中,一个网络连接使用一个Channel(通道),IO操作都是通过 连接通道完成。一个通道类似于BIO中两个流的结合,既可以读取数据,也可以写入数据,所以 Channel(通道)双向的。

   JavaNIO中,一个socket连接使用一个Channel(通道),不同网络传输协议类型,在Java中都有不同的 NIO Channel(通道)相对应。

   JavaNIO 中 Channel 主要实现:

        1. FileChannel 用于文件IO操作 ;

        2.DatagramChannel 数据报通道,用于UDP协议的数据读写;

        3.SocketChannel 用于Socket套接字TCP连接的数据读写;

        4.ServerSocketChannel 服务器套接字通道(或服务器监听通道),允许我们监听TCP 连接请求,为每个监听到的请求,创建一个SocketChannel套接字通道;

  代码示例1:File Channel 的使用,使用一个buffer 完成文件的 读写

public static void nioFileChannelEg01() throws Exception {
        //文件inputStream 流
        FileInputStream fileInputStream = new FileInputStream("hello.txt");//这里文件可以随意定义
        //获取 channel
        FileChannel fileChannel01 = fileInputStream.getChannel();
        //文件outputStream 流
        FileOutputStream fileOutputStream = new FileOutputStream("JavaHello.txt");//这里文件可以随意定义
        //获取 channel
        FileChannel fileChannel02 = fileOutputStream.getChannel();
        //buffer 缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(128);
        //循环读取文件内容
        while (true) {
            //这里需要 buffer.clear 重置一下,如果没有重置 position 与limit 会相等为0 不会进入read ==-1 的判断
            byteBuffer.clear();
            //将 input channel 中的内容读到 buffer 中
            int read = fileChannel01.read(byteBuffer);
            System.out.println("read" + read);//这里观察一下 read 每次读多少 ,验证buffer.clear方法
            if (read == -1) {
                break;
            }
            //翻转buffer
            byteBuffer.flip();
            System.out.println(new String(byteBuffer.array()));//将buffer 内容输出到控制台
            //将 buffer 中的内容写到 output channel 中
            fileChannel02.write(byteBuffer);
        }
        //关闭相关的 流
        fileChannel02.close();
        fileInputStream.close();
    }

  代码示例2:使用 channel 的  transferFrom 方法 进行文件的 复制

 public static void nioFileChannelCopyEg02() throws Exception {
        //文件inputStream 流
        FileInputStream fileInputStream = new FileInputStream("hello.txt");
        //获取 channel
        FileChannel fileInputChannel = fileInputStream.getChannel();
        //文件outputStream 流
        FileOutputStream fileOutputStream = new FileOutputStream("HelloJava.txt");
        //使用 channel 的 transferFrom 进行文件 copy(第一个参数是 目标,第二个参数是 从0开始,第三个参数是 在哪里结束)
        fileOutputStream.getChannel().transferFrom(fileInputChannel, 0, fileInputChannel.size());
        //关闭相关的 流
        fileInputStream.close();
        fileOutputStream.close();
    }

  补充代码 mappedByteBuffer 的简单使用 :可以让文件 直接在内存(堆外内存)修改,操作系统不需要拷贝一次(具体内容后续补充)

  // mappedByteBuffer 可以让文件 直接在内存(堆外内存)修改,操作系统不需要拷贝一次
    public static void nioMappedByteBuffer() throws Exception {
        RandomAccessFile rw = new RandomAccessFile("hello.txt", "rw");
        FileChannel channel = rw.getChannel();
        //参数一:FileChannel.MapMode.READ_WRITE 使用读写模式。 参数二:直接修改的起始位置 0开始。参数三 :映射到内存的大小,就是 修改的范围是0-4
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        mappedByteBuffer.put(0,(byte) 72); //写入 H 的 ASCII 码
        mappedByteBuffer.put(4,(byte) '4');//最多修改到这里
        //mappedByteBuffer.put(5,(byte) '8');//这里会 java.lang.IndexOutOfBoundsException
        mappedByteBuffer.force();//手动将内存数据刷新到 磁盘
        rw.close();
        System.out.println("success!");
    }

示例代码3:ServerSocketChannel 与 SocketChannel 通过 buff[] 数组的形式 分散聚合 数据( 代码只适用于演示,不能做生产用途,存在 bug )
   /**
     * Scattering:将数据写入到buffer时:可以采用buffer数组,依次写入 [分散]
     * Gathering: 从buffer读取数据时,可以采用buffer数组,依次读
     */
    public static void nioBufferScatteringAndGatheringDemo() throws Exception {
        //使用 ServerSocketChannel和 SocketChannel 网络socket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
        //绑定端口到socket,并启动
        serverSocketChannel.socket().bind(inetSocketAddress);
        System.out.println("serverSocket 启动 端口 7000!");
        //创建buffer数组
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(5);
        byteBuffers[1] = ByteBuffer.allocate(2);
        //等待连接(telnet测试连接)
        SocketChannel socketChannel = serverSocketChannel.accept();
        System.out.println("连接请求:" + socketChannel.getRemoteAddress());
        int messageLength = 0;
        while (true) {//循环读取
            long read = socketChannel.read(byteBuffers);
            System.out.println("read" + read);//当前读了多少个元素
            messageLength += read;
            System.out.println(new String(byteBuffers[0].array()));
            System.out.println(new String(byteBuffers[1].array()));
            //输出元素位置 观察 position limit
            Arrays.stream(byteBuffers).map(buffer -> "position=" + buffer.position() + ",limit=" + buffer.limit()).forEach(System.out::println);
            //将所有的  buff 翻转
            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.flip());
            //将数据 读出  显示到客户端
            long w = socketChannel.write(byteBuffers);//回送
            //当前写了多少个元素
            System.out.println("byteWrite = " + w);
            //将所有的buffer 进行clear
            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());
            //当前循环读了多少个元素, 写了多少个元素, 消息总长度是多少。
            System.out.println("byteRead=" + read + ",byteWrite=" + w + ",messageLength= " + messageLength);
        }
    }

  结果:平台输出

  

三  selector 选择器

    1. selector  选择器的使命是完成IO的多路复用,其主要工作是通道的注册、监听、事件查询。一个 channel 通道代表一条连接通路 ,通过选择器可以同时监控多个通道的IO(输入输出) 状况。

    2. 选择器和通道的关系,是监控和被监控的关系。 选择器提供了独特的API方法,能够选出(select)所监控的通道已经发生了哪些IO事件。

      在NIO编程中,一般是一个单线程处理一个选择器,一个选择器可以监控很多通道。所 以,通过选择器,一个单线程可以处理数百、数千、数万、甚至更多的通道。这样会大量地减少线程之间上下文 切换的开销。

        通道和选择器之间,通过register(注册)的方式完成。调用通道的 Channel.register (Selector sel,int ops)方法,可以将通道实例注册到一个选择器中。

      register方法有 两个参数:第一个参数,指定通道注册到的选择器实例;第二个参数,指定选择器要监控的IO事 件类型。

    3. 可供选择器监控的通道IO事件类型,包括以下四种:

      (1)可读:SelectionKey.OP_READ

      (2)可写:SelectionKey.OP_WRITE

      (3)连接:SelectionKey.OP_CONNECT

      (4)接收:SelectionKey.OP_ACCEPT

       以上的事件类型常量定义在SelectionKey类中。如果选择器要监控通道的多种事件,用“位或”运算符来实现( int key =  SelectionKey.OP_READ | SelectionKey.OP_WRITE )。

    4.SelectionKey选择键,Selector不直接管理Channel,而是直接管理SelectionKey,通过SelectionKey与 Channel发生关系。两核心成员keys、selectedKeys定义在Selector的抽象实现类SelectorImpl中。

      一个Channel 最多能向 Selector 注册一次,注册 之后就形成了唯一的SelectionKey,被Selector管理起来。

      核心成员keys, 专门管理注册的 SelectionKey,Channel注册到Selector后 创建一个唯一的 SelectionKey,添加在 keys 成员中,(这是一个HashSet类型的集合)。

      核心成员selectedKeys,用于存放已经发生了IO事件的SelectionKey。

      

        SelectionKey是IO事件的记录者(或存储者),SelectionKey 有两个核心成员, 存储着自己关联的Channel上的感兴趣IO事件和已经发生的IO事件。int interestOps(),int readyOps(),通过比特位来完成记录多个事件。

      具体的IO事件所占用的哪一个比特位, 通过常量的方式定义在SelectionKey中。

      

    5.SelectableChannel可选择通道。不是所有的通道,都是可以被选择器监控或选择的

      FileChannel 就不能被选择器复用。判断一个通道能否被选择器监控或选择,有一个前提:判断它是否继承 了抽象类SelectableChannel(可选择通道),如果是则可以被选择,否则不能。

      简单地说,一条通道若能被选择,必须继承SelectableChannel类。 它提供了实现通道的可选择性所需要的公共方法。

      Java NIO中所有网络链接Socket套接字通道,都继承了SelectableChannel类,是可选择的。 而FileChannel文件通道,没有继承SelectableChannel,因此不是可选择通道。

    6. 选择器使用流程 : (1)获取选择器实例; (2)将通道注册到选择器中; (3)轮询感兴趣的IO就绪事件(选择键集合)。

       第一步:获取选择器实例。选择器实例是通过调用静态工厂方法open()来获取的,具体 如下:

       1.//调用静态工厂方法open()来获取Selector实例

      Selector selector = Selector.open();

      第二步:将通道注册到选择器实例。要实现选择器管理通道,需要将通道注册到相应的 选择器上,

      // 2.获取通道

      ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

      // 3.设置为非阻塞   

      serverSocketChannel.configureBlocking(false);

      // 4.绑定连接

      serverSocketChannel.bind(new InetSocketAddress(18899));

      // 5.将通道注册到选择器上,并制定监听事件为:“接收连接”事件

      serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

      第三步:选出感兴趣的IO就绪事件(选择键集合)。通过Selector选择器的select()方法,选出已经注册的、已经就绪的IO事件,并且保存到SelectionKey选择键集合中。

      SelectionKey 集合保存在选择器实例内部,其元素为SelectionKey类型实例。调用选择器的selectedKeys() 方法,可以取得选择键集合。 接下来,需要迭代集合的每一个选择键,根据具体IO事件类型,执行对应的业务操作。

      大致的处理流程如下:

      //轮询,选择感兴趣的IO就绪事件(选择键集合)

      while (selector.select() > 0) {

        Set selectedKeys = selector.selectedKeys();

        Iterator keyIterator = selectedKeys.iterator();

        while(keyIterator.hasNext()) {

          SelectionKey key = keyIterator.next();

          //根据具体的IO事件类型,执行对应的业务操作

          if(key.isAcceptable()) {

          // IO事件:ServerSocketChannel服务器监听通道有新连接

          } else if (key.isConnectable()) { // IO事件:传输通道连接成功

          } else if (key.isReadable()) { // IO事件:传输通道可读

          } else if (key.isWritable()) { // IO事件:传输通道可写 }

          //处理完成后,移除选择键

          keyIterator.remove();

      }}

      处理完成后,需要将选择键从这个SelectionKey集合中移除,防止下一次循环的时候, 被重复的处理。

      SelectionKey集合不能添加元素,如果试图向SelectionKey选择键集合中添加 元素,则将抛出java.lang.UnsupportedOperationException异常。

      用于选择就绪的IO事件的select()方法,有多个重载的实现版本,具体如下:

        (1)select():阻塞调用,一直到至少有一个通道发生了注册的IO事件。

        (2)select(long timeout):和select()一样,但最长阻塞时间为timeout指定的毫秒数。

        (3)selectNow():非阻塞,不管有没有IO事件,都会立刻返回。 select()方法的返回值的是整数类型(int),表示发生了IO事件的数量。

            更准确地说,是 从上一次select到这一次select之间,有多少通道发生了IO事件,更加准确地说,是指发生了 选择器感兴趣(注册过)的IO事件数。

 代码示例 1: 读取客户端通道的输入数据,读取完成后直接关闭

   服务端代码

public static void selectorSeverDemo() throws Exception {
        // 1.获取选择器
        Selector selector = Selector.open();
        // 2.获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 3.设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 4.绑定连接
        serverSocketChannel.bind(new InetSocketAddress(5555));
        System.out.println("服务器启动成功");
        // 5.将通道注册的“接收新连接”IO事件,注册到选择器上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 6.轮询感兴趣的IO就绪事件(选择键集合)
        while (selector.select() > 0) {
            // 7.获取选择键集合
            Iterator<SelectionKey> selectedKeys =
                    selector.selectedKeys().iterator();
            while (selectedKeys.hasNext()) {
                // 8.获取单个的选择键,并处理
                SelectionKey selectedKey = selectedKeys.next();
                // 9.判断key是具体的什么事件
                if (selectedKey.isAcceptable()) {
                    // 10.若选择键的IO事件是“连接就绪”事件,就获取客户端连接
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 11.将新连接切换为非阻塞模式
                    socketChannel.configureBlocking(false);
                    // 12.将该新连接的通道的可读事件,注册到选择器上
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectedKey.isReadable()) {
                    // 13.若选择键的IO事件是“可读”事件, 读取数据
                    SocketChannel socketChannel = (SocketChannel) selectedKey.channel();
                    // 14.读取数据,然后丢弃
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int length = 0;
                    while ((length = socketChannel.read(byteBuffer)) > 0) {
                        byteBuffer.flip();//翻转buffer
                        System.out.println(new String(byteBuffer.array(), 0, length));
                        byteBuffer.clear();
                    }
                    socketChannel.close();
                }
                // 15.移除选择键
                selectedKeys.remove();
            }
        }
        // 16.关闭连接
        serverSocketChannel.close();
    }

  客户端代码:

public static void startClient() throws Exception {
        InetSocketAddress address = new InetSocketAddress("127.0.0.1",5555);
        // 1.获取通道
        SocketChannel socketChannel = SocketChannel.open(address);
        // 2.切换成非阻塞模式
        socketChannel.configureBlocking(false);
        //不断地自旋、等待连接完成,或者做一些其他的事情
        while (!socketChannel.finishConnect()) {
        }
        System.out.println("客户端连接成功!");
        // 3.分配指定大小的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put("hello world Java NIO".getBytes());
        byteBuffer.flip();
        //发送到服务器
        socketChannel.write(byteBuffer);
        socketChannel.shutdownOutput();
        socketChannel.close();
    }

 

posted on 2025-06-09 18:02  zy平平仄仄  阅读(16)  评论(0)    收藏  举报