NIO

什么是NIO-------------------------------------
IO回顾 IO
Input Output(输入 输出)IO技术的作用:
解决设备和设备之间的数据传输问题IO的应用场景:图片上传、下载、打印机打印信息表、解析XML…
概念-------------------------------------
即 Java New IO
是1个全新的、 JDK 1.4后提供的 IO API
Java API中提供了两套NIO
一套是针对标准输入输出NIO,另一套就是网络编程NIO
作用-------------------------------------
NIO和IO有相同的作用和目的,但实现方式不同
可替代 标准Java IO 的IO API
IO是以流的方式处理数据,而NIO是以块的方式处理数据
块可以说是缓冲区,对于IO流来说,其中缓冲流也可以叫做缓冲区
流与块的比较-------------------------------------
NIO和IO最大的区别是数据打包和传输方式
IO是以流的方式处理数据,而NIO是以块的方式处理数据
面向流的IO一次一个字节的处理数据,一个输入流产生一个字节,一个输出流就消费一个字节
面向块的IO系统以块的形式处理数据,每一个操作都在一步中产生或消费一个数据块,按块要比按流快的多
举例-------------------------------------
拿水龙头来比喻:
流就像水龙头滴水,每次只有一滴
块就像水龙头往水壶放水,放满之后对一整个水壶的水进行操作,水壶充当缓冲区,主要的帮助就是可以一起操作解码的作用,而不会多次操作,相当于网络中的缓冲,与之前说明的缓冲io是类似的
新特性-------------------------------------
对比于 Java IO,NIO具备的新特性如下:

在这里插入图片描述

可简单认为:IO是面向流的处理,NIO是面向块(缓冲区)的处理
面向流的I/O 系统一次一个字节地处理数据
一个面向块(缓冲区)的I/O系统以块的形式处理数据
核心组件-------------------------------------
Java NIO的核心组件
包括:
通道(Channel)
缓冲区(Buffer)
选择器(Selector)
在NIO中并不是以流的方式来处理数据的,而是以buffer缓冲区和Channel管道配合使用来处理数据
Selector是因为NIO可以使用异步的非阻塞模式才加入的东西

在这里插入图片描述

在这里插入图片描述

当我们创建流时,只不过是有通道,来操作读取或者写入数据而已,即可以看出路径,如我知道了你家的位置
通过该位置来进行读取和写入,读取数据放内存,然后将内存数据写入文件,当然这些通道可以加缓冲区,即我们知道的缓冲流
但是每个通道只有一种作用(即单向),要么读取要么写入
而NIO,就将这样的通道,变成一个通道,使得,既可以读写,又可以写入,少了通道的建立(即双向)
且该通道之间只有一个缓冲区来进行数据运输,而不用向IO一样建立两个缓冲流了,即可以将NIO理解为IO缓冲流的合并版
对于文件的/或者//都可以写,没问题

在这里插入图片描述

简单理解一下:
Channel管道比作成铁路,buffer缓冲区比作成火车(运载着货物)
而我们的NIO就是通过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理
要时刻记住:Channel不与数据打交道,它只负责运输数据
与数据打交道的是Buffer缓冲区
Channel–>运输,相当于数据载体,就如我们使用io需要也需要创建对象,他们的中间就是载体,虽然是一串代码
Buffer–>数据
相对于传统IO而言,流是单向的
对于NIO而言,有了Channel管道这个概念,我们的读写都是双向的(铁路上的火车能从广州去北京、自然就能从北京返还到广州)
Buffer缓冲区---------------------------------
public abstract class Buffer
extends Object
作用:缓冲区,用来存放具体要被传输的数据,比如文件、scoket 等
这里将数据装入 Buffer 再通过通道进行传输
Buffer 就是一个数组,用来保存不同数据类型的数据
在 NIO 中,所有的缓冲区类型都继承于抽象类 Buffer,最常用的就是 ByteBuffer
对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应,它们之间的继承关系如下图所示

在这里插入图片描述

ByteBuffer的创建方式--------------------------------------
public abstract class ByteBuffer
extends Buffer
implements Comparable<ByteBuffer>
public abstract class Buffer
extends Object
当然,其他的缓冲区都类似
代码演示在堆中创建缓冲区:allocate(int capacity)
在系统内存创建缓冲区:allocateDirect(int capacity)
通过普通数组创建缓冲区:wrap(byte[] arr)
package com.lagou.task23.buffer;

import java.nio.ByteBuffer;

/**
 *  演示ByteBuffer创建的三种方式
 */
public class Demo01Buffer创建方式 {

    public static void main(String[] args) {

        // 第一种创建方式:在堆中创建缓冲区:allocate(int capacity)
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);

        // 第二种创建方式:在系统内存创建缓冲区:allocateDirect(int capacity)
        ByteBuffer byteBuffer1 = ByteBuffer.allocateDirect(10);

        // 第三种创建方式:通过普通数组创建缓冲区:wrap(byte[] arr)
        byte[] arr = {97,98,99};
        ByteBuffer byteBuffer2 = ByteBuffer.wrap(arr);
    }

}

常用方法--------------------------------------
拿到一个缓冲区我们往往会做什么?很简单,就是读取缓冲区的数据/写数据到缓冲区中
所以,缓冲区的核心方法就是:
put(byte b) : 给数组添加元素
get() :获取一个元素
package com.lagou.task23.buffer;

import java.nio.ByteBuffer;
import java.util.Arrays;

public class Demo02Buffer的方法 {

    public static void main(String[] args) {

        // 1. 创建出buffer对象
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);


        // put(byte b): 给数组添加元素
        byteBuffer.put((byte) 10);
        byteBuffer.put((byte) 20);
        byteBuffer.put((byte) 30);


        // 把缓冲区数组转换成普通数组
        byte[] array = byteBuffer.array();

        //打印
        System.out.println(Arrays.toString(array)); //[10, 20, 30, 0, 0, 0, 0, 0, 0, 0]

        // get() :获取一个元素
        byte b = byteBuffer.get(1);
        //直接写byteBuffer.get(1).var也可以变成 byte b = byteBuffer.get(1);
        //这是idea的功能,与alt+enter类似
        System.out.println(b); //20


    }
}

其实对于这些缓冲区,只不过里面有内置的对应数组,就如IO流里的缓冲区一样,有内置数组
所以是可以获得这些数组,并打印出来的,当然,若没有添加元素,即打印默认值
所以可以将这样的缓冲区理解为对数组的包装,与其他包装类类似,当然由于是数组,即有地址
那么添加元素时,有类似集合的方法添加元素
对于方法来说,是非常严谨的
//如
byte a = 10
其中10会自动转化为a,是因为代码缘故
byteBuffer.put((byte) 10);
而对于方法里,直接写10的话,他并没有进行10的自动转化,所以需要强转,否则会报错(编译,idea提示和编译以及idea的结合),因为在方法中,可以认为他必然是操作的,那么会认为是int类型,自己可以进行测试
Buffer类维护了4个核心变量属性来提供关于其所包含的数组的信息。它们是:

在这里插入图片描述

容量Capacity----------------------------------------
缓冲区能够容纳的数据元素的最大数量
容量在缓冲区创建时被设定,并且永远不能被改变(不能被改变的原因也很简单,底层是数组嘛)
界限Limit----------------------------------------
缓冲区中可以操作数据的大小,代表了当前缓冲区中一共有多少数据(从limit开始后面的位置不能操作)
位置Position----------------------------------------
下一个要被读或写的元素的位置,Position会自动由相应的 get( )和 put( )函数更新
以上三个属性值之间有一些相对大小的关系:0 <= position <= limit <= capacity
例:- 如果我们创建一个新的容量大小为10ByteBuffer对象,在初始化的时候,position 设置为 0
limit 和 capacity 被设置为 10
在以后使用ByteBuffer对象过程中,capacity的值不会再发生变化,而其它两个个将会随着使用而变化
标记Mark----------------------------------------
一个备忘位置,用于记录上一次读写的位置
buffer代码演示-----------------------------------------
首先展示一下是创建缓冲区后,核心变量的值是怎么变化的
package com.lagou.task23.buffer;

import java.nio.ByteBuffer;

public class Demo03Buffer的核心属性 {

    public static void main(String[] args) {

        // 1. 创建出buffer对象
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        System.out.println("初始化---->capacity--->" + byteBuffer.capacity());//10
        System.out.println("初始化---->limit--->" + byteBuffer.limit());//10
        System.out.println("初始化---->position--->" + byteBuffer.position());//0

        System.out.println("-----------------------------");

        // 添加一些数据到缓冲区
        String s = "JavaEE";
        //不要超过limit的值,否则会报错
        byteBuffer.put(s.getBytes());

        System.out.println("put后---->capacity--->" + byteBuffer.capacity());//10
        System.out.println("put后---->limit--->" + byteBuffer.limit());//10
        System.out.println("put后---->position--->" + byteBuffer.position());//6

        System.out.println("-----------------------------");

            byteBuffer.flip();
        //转化为读模式,改变指针,和限制位置,超过限制(limit)会报错
        System.out.println("flip后---->capacity--->" + byteBuffer.capacity());//10
        System.out.println("flip后---->limit--->" + byteBuffer.limit());//6
        System.out.println("flip后---->position--->" + byteBuffer.position());//0

        System.out.println("-----------------------------");

        // (1)创建一个limit()大小的字节数组(因为就只有limit这么多个数据可读)
        byte[] bytes = new byte[byteBuffer.limit()];
        //不要超过limit的值,否则会报错

        // (2) 将读取出来的数据装进字节数组中
        byteBuffer.get(bytes);

        System.out.println("get后---->capacity--->" + byteBuffer.capacity());//10
        System.out.println("get后---->limit--->" + byteBuffer.limit());//6
        System.out.println("get后---->position--->" + byteBuffer.position());//6

        // (3) 输出数据
        System.out.println(new String(bytes,0,bytes.length));

        byteBuffer.clear();
        System.out.println("clear后---->capacity--->" + byteBuffer.capacity());//10
        System.out.println("clear后---->limit--->" + byteBuffer.limit());//10
        System.out.println("clear后---->position--->" + byteBuffer.position());//0


        byteBuffer.put("lagou".getBytes());
        System.out.println("第一次put后---->position--->" + byteBuffer.position());//5

        // 做一个标记:记录上一次读写位置 position的值 5
        byteBuffer.mark();

        byteBuffer.put("zimu".getBytes());
        System.out.println("第二次put后---->position--->" + byteBuffer.position());//9

        // 还原到标记位置
        byteBuffer.reset();
        //当然,由于是数组,改变下标位置,新加数据时,那么是会覆盖的
        System.out.println("reset后---->position--->" + byteBuffer.position());//5





    }
}

当想要从缓存区拿数据,NIO给了我们一个flip()方法。这个方法可以改动position和limit的位置
还是上面的代码,我们flip()一下后,再看看4个核心属性的值会发生什么变化:
很明显的是:limit变成了position的位置了,而position变成了0
看到这里你可能就会想到了:
当调用完filp()时,limit是限制读到哪里,而position是从哪里读,一般我们称filp()为"切换成读模式"
每当要从缓存区的时候读取数据时,就调用filp()“切换成读模式”

在这里插入图片描述

切换成读模式之后,我们就可以读取缓冲区的数据了
读完我们还想写数据到缓冲区,那就使用clear()函数,这个函数会“清空”缓冲区
数据没有真正被清空,只是被遗忘掉了,其底层原理是
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
/*
public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw createCapacityException(capacity);
        return new HeapByteBuffer(capacity, capacity);
    }
*/
//创建的是HeapByteBuffer对象
String s = "JavaEE";
byteBuffer.put(s.getBytes());
//HeapByteBuffer类的put方法是
/*
public ByteBuffer put(byte[] src, int offset, int length) {

        checkBounds(offset, length, src.length);
        if (length > remaining())
            throw new BufferOverflowException();
        System.arraycopy(src, offset, hb, ix(position()), length);
        position(position() + length);
        return this;

    }
*/
//System.arraycopy(src, offset, hb, ix(position()), length);
//final byte[] hb
//将数组复制到hb数组里
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
//HeapByteBuffer类的get方法是(最终会操作到的,自己调试就行,上面的put也是如此)
/*
 public ByteBuffer get(byte[] dst, int offset, int length) {
        checkBounds(offset, length, dst.length);
        if (length > remaining())
            throw new BufferUnderflowException();
        System.arraycopy(hb, ix(position()), dst, offset, length);
        position(position() + length);
        return this;
    }
*/
//System.arraycopy(hb, ix(position()), dst, offset, length);
//将hb的数组复制到你创建的数组中
//所以实际上当你使用clear方法时,之所以被遗忘了,是因为hb数组还在,即数据的确没有清空
//但当你重新put方法时,hb数组就会被重置
//因为System.arraycopy方法,在复制时,会替代(覆盖)对应的数据
当然,无论加入单个数据,还是数组,都是放在对应的hb数组里面,所以也证明了我上面说的话
缓冲区是一个数组的包装
Channel通道------------------------------------------
通道(Channel):由 java.nio.channels 包定义 的
Channel 表示 IO 源与目标打开的连接
Channel 类似于传统的"流"
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作
数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中
白话: 就是数据传输用的通道,作用是打开到IO设备的连接,文件、套接字都行
例:相当于一根管子,buffer中的数据可以通过管子写入被操作的资源当中(缓冲==文件)
也可以将资源通过管子写入到buffer中去(文件==缓冲)
就如IO流里:文件 -> 缓冲 -> 内存 -> 缓冲 -> 文件,文件到内存输入流,内存到文件输出流,缓冲区是中转站
而且当另外一个文件过来时
又要创建对应的两个流,即文件只有读取或者写入操作,所以就出现了NIO
而NIO就是将两个流合并:文件 == 缓冲 == 内存,不是单向的了,而中间的==就是通道,即通道是数据的真正走向
该通道使得两个文件都可以有读取和写入操作,但是通道也是需要IO流来获取
即可以说成NIO就是带有双向合并的缓冲IO流,其中通道是流的单向,双向指的的缓冲区,缓冲区是中转站
但是要得到双向的NIO,其实还是要利用IO流,因为虽然是双向的,但是里面其实还是IO流的原理(只不过集成了而已,所以说io也可以双向的哦,只不过io他显示的分开,所以我们才说是单向的),即文件有可能是不一样的
在传递时,还是需要IO的指定文件,只不过简化了IO流
因为原来的缓冲流需要创建缓冲区,而NIO则对缓冲区更加的有细致操作了
由四个缓冲区(对于IO来说,单个文件的读取和写入,需要创建四个流)
变成了一个缓冲区了(对于NIO来说,是通道是两流合并,缓冲区共用)
Channel API------------------------------------------
Java 为 Channel 接口提供的最主要实现类如下:

在这里插入图片描述

FileChannel基本使用-------------------------------------
使用FileChannel完成文件的复制
package com.lagou.task23.channel;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class Channel完成文件复制 {

    /**
     * 需求:将 D:\nio_img文件夹中wx_lagou.jpg 复制到工程中
     * @param args
     */
    public static void main(String[] args) throws IOException {

        // 创建输入流和输出流(依赖于IO流获取channel)
        FileInputStream fileInputStream = new FileInputStream("D:\\nio_img\\wx_lagou.jpg");
        FileOutputStream fileOutputStream = new FileOutputStream("D:\\NIO\\复制.jpg");


        // 通过IO流获取channel通道
        FileChannel channel1 = fileInputStream.getChannel();
        FileChannel channel2 = fileOutputStream.getChannel();


        // 创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 循环读写
        //channel1也有write方法,即也有写入功能
        //更加的证明了NIO是双向的
        while (channel1.read(byteBuffer) != -1){//返回读取文件的字节个数,并放入里面的字节数组
            //读完文件数据或者到limit限制就停止读取,limit限制先进行判断,因为将数据放入数组里面
            //是需要确定位置的,如果到了limit限制,则不会读取文件内容,即文件内容位置不变
            //可以利用这个限制来实现循环
            //即只要开始读文件时,有文件数据,那么就不会返回-1
            //position的大小与文件读取字节个数对应
            byteBuffer.flip();
            //之所以放前面是防止文件的内容少与容量大小时,会使得limit正好限制读取个数
            //若少于容量大小时,用clear方法的话,那么写的时候就会写入byte的默认值
            channel2.write(byteBuffer);//写到limit限制停止
            //当前面调用flip方法时,然后进行写入,那么position就回到limit的位置
            //如果不调用clear方法,那么读取的就是空的,由于读取的是空的,那么文件的数据位置就会一直不变
            //那么就一直不会返回-1(因为我有数据不会返回-1,可是你却在最后,我们读的是空的),且会一直写入内容,造成文件持续增大
            //所以需要clear方法,来调整位置,进行重置
            byteBuffer.clear();
        }
        //读取容量(byteBuffer)大小的文件数据,放入byteBuffer数组里面
        //若文件容量小于该容量大小,那么停止读取,即position的大小与文件读取对应
        //将容量(byteBuffer)大小的数组数据,写入文件里面
        //但是要注意position与limit的位置
        //当position在读取容量大小的数据时,会发生改变,直接到最后,但是这个时候,文件读取完了还好
        //如果文件没有读取完
        //那么就会一直不返回-1,之所以写入文件和读取文件也是可以为空的,没有报错,因为没有指定的异常
        //那么这时候,我们需要重新改变位置,就需要flip方法和clear方法了
        //进行读写转化和重置操作
       

        //关流
        fileInputStream.close();
        fileOutputStream.close();

    }
}

上述可以知道flip方法,不止可以切换为读模式,也可以切换为写模式
无论称为读模式还是写模式,实际上,都是position和limit的位置,造成相应的读取和写入的变化而已
可以知道NIO也是需要IO流的,所以也称为Java New IO

在这里插入图片描述

网络编程收发信息------------------------------------
网络编程里也是进行数据的传递,只不过不是文件而已,而是两者之间内存的数据进行传递
可以将他们的内存都可以看成一个文件,那么就回到了文件之间的传递了,而中间站由原来的内存,变为网络了
即我们也可以用NIO进行一个管道的连接,使得我和他都可以读取和写入到网络
再进行网络包的传递(网络包这里称为数据的传递),只不过缓冲区不能共享了(虽然基本是不共享的)
因为只对网络包操作了
package com.lagou.task23.channel;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

/**
 * BIO: 同步阻塞
 */
public class Test客户端 {

    public static void main(String[] args) throws IOException {

        Socket socket = new Socket("127.0.0.1", 9999);
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("你好呀".getBytes());
        outputStream.close();
        socket.close();

    }



}

package com.lagou.task23.channel;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * BIO: 同步阻塞  NIO:同步非阻塞(并发支持高)
 */
public class Test服务器 {

    public static void main(String[] args) throws IOException {

        ServerSocket serverSocket = new ServerSocket(9999);
        Socket socket = serverSocket.accept();
        InputStream inputStream = socket.getInputStream();
        byte[] bytes = new byte[1024];
        int len = inputStream.read(bytes);
        System.out.println(new String(bytes,0,len));
        socket.close();


    }
}

package com.lagou.task23.channel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIO客户端 {

    public static void main(String[] args) throws IOException {


        // 1. 创建对象
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));

        // 创建缓冲区数组
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 设置数据
        byteBuffer.put("哈哈哈".getBytes());

        byteBuffer.flip();

        // 输出数据
        socketChannel.write(byteBuffer);

        socketChannel.close();

    }
}

在这里插入图片描述

输出空的,没报错,要么没有指定的异常,要么已经处理了
accept阻塞问题-----------------------------------
accept的阻塞简称BIO,同步阻塞,用NIO的话,就可以实现同步非阻塞(并发支持高)
package com.lagou.task23.channel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class NIO服务器 {

    public static void main(String[] args) throws IOException, InterruptedException {

        // 1.创建服务器端对象,监听对应的端口
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 绑定要监听的端口
        serverSocketChannel.bind(new InetSocketAddress(9999));

        // 设置为非阻塞连接
        serverSocketChannel.configureBlocking(false);


        while (true) {
            // 2.连接客户端 阻塞
            SocketChannel socketChannel = serverSocketChannel.accept();//若设置为非阻塞
            //当没有客户端来时,返回null
            
            //而无限循环,是为了让客户端有机会进来(针对普通的网络io,其实就是将多线程关系提取了)
           
     

            if (socketChannel != null) {

                // 3.读取数据
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

                // 读取到的字节长度
                int len = socketChannel.read(byteBuffer);
                //返回读取文件的字节个数
                //与读取文件类似,读完文件数据或者到limit限制就停止读取

                // 打印
                System.out.println(new String(byteBuffer.array(), 0, len));

                // 结束循环
                break;

            }else {

                // 没有连接到服务器的客户端
                System.out.println("做一些别的事");
                Thread.sleep(2000);

            }
        }
    }
}

Selector选择器----------------------------------

在这里插入图片描述

一个线程操作一条路
多路复用的概念
一个选择器可以同时监听多个服务器端口, 帮多个服务器端口同时等待客户端的访问,主要原理是使用操作系统层面的非阻塞IO,在RabbitMQ中间件中,也是如此,只不过他是Erlang来处理的,而不是java
Selector的和Channel的关系----------------------------------
Channel和Buffer比较好理解 ,联系也比较密切
他们的关系简单来说就是:数据总是从通道中读到buffer缓冲区内,或者从buffer写入到通道中,因为通道是流的合并
就如IO流一样,需要流来进行数据的传递,因为IO流的缓冲流就是文件数据到缓冲,再到内存,然后到缓存,再到文件
NIO就合并了而已
选择器和他们的关系----------------------------------
选择器(Selector) 是 Channel(通道)的多路复用器,Selector 可以同时监控多个 通道的 IO(输入输出) 状况
Selector的作用----------------------------------
选择器提供选择执行已经就绪的任务的能力
从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力
Selector 允许单线程处理多个Channel
仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道
事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销
可选择通道(SelectableChannel)----------------------------------
注意:并不是所有的Channel,都是可以被Selector 复用的
比方说,FileChannel就不能被选择器复用
判断一个Channel 能被Selector 复用,有一个前提:判断他是否继承了一个抽象类SelectableChannel
如果继承了SelectableChannel,则可以被复用,否则不能
SelectableChannel 的结构如下图:

在这里插入图片描述

通道和选择器注册之后,他们不是绑定的关系,不是一对一的关系
一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次
通道和选择器之间的关系,使用注册的方式完成
SelectableChannel可以被注册到Selector对象上,在注册的时候,需要指定通道的哪些操作,是Selector感兴趣的

在这里插入图片描述

Channel注册到Selector-------------------------------------
使用Channel.register(Selector sel,int ops)方法,将一个通道注册到一个选择器时,register(注册的意思)
第一个参数:指定通道要注册的选择器是谁
第二个参数:指定选择器需要查询的通道操作
可以供选择器查询的通道操作,从类型来分,包括以下四种:
可读 : SelectionKey.OP_READ
可写 : SelectionKey.OP_WRITE
连接 : SelectionKey.OP_CONNECT
接收 : SelectionKey.OP_ACCEPT
如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:
int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
选择键(SelectionKey)-----------------------------------
Channel和Selector的关系确定好后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到
这个工作,使用选择器Selector的select()方法完成
select方法的作用,对感兴趣的通道操作,进行就绪状态的查询
Selector可以不断的查询Channel中发生的操作的就绪状态
并且挑选感兴趣的操作就绪状态
一旦通道有操作的就绪状态达成,并且是Selector感兴趣的操作,就会被Selector选中,放入选择键集合中

在这里插入图片描述

Selector的使用流程------------------------------------
package com.lagou.task23.selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class Selector服务器 {

    public static void main(String[] args) throws IOException {

        // 小目标:通道注册到选择器上:实现

        // 1、获取seletor选择器
        Selector selector = Selector.open();

        // 2、获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocketChannel serverSocketChanne2 = ServerSocketChannel.open();
        ServerSocketChannel serverSocketChanne3 = ServerSocketChannel.open();

        serverSocketChannel.bind(new InetSocketAddress(9999));
        serverSocketChanne2.bind(new InetSocketAddress(8888));
        serverSocketChanne3.bind(new InetSocketAddress(7777));

        // 3、设置为非阻塞 ** 
        //(与selector一起使用时,channel必须要处在非阻塞模式下,如果是阻塞的,会抛出异常)
        serverSocketChannel.configureBlocking(false);
        serverSocketChanne2.configureBlocking(false);
        serverSocketChanne3.configureBlocking(false);
        //这个设置非阻塞,必须要对应操作,如下面的SelectionKey.OP_ACCEPT
        //当SelectionKey.OP_ACCEPT换成其他时,就会报错

        // 4、将通道注册到选择器上 指定 监听事件 为 ‘接收’事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        serverSocketChanne2.register(selector, SelectionKey.OP_ACCEPT);
        serverSocketChanne3.register(selector, SelectionKey.OP_ACCEPT);


        // select():查询已经就绪的通道操作 返回值:表示有多少通道已经就绪
        //  阻塞:阻塞到至少有一个通道上的事件就绪了
        //在寻常的客户端连接时,需要accept方法阻塞,但其实accept方法里也是经过一系列操作的
        //而这里可以理解为,在客户的连接过来时,进行判断,看看他是要什么操作
        //而在获得这个管道操作前,就会阻塞,每有一个,那么selector.select()的值就会加一
        //其中,在注册时,都是将selector对应的操作放入SelectionKey里面的数组里,每个注册都有对应的数组
        //最后取出来,而该类型是可以得到AbstractSelector,即变为对应的管道,来操作
       
        /*
        底层的一些代码
         SelectionKey k = findKey(sel);
           
                    k = ((AbstractSelector)sel).register(this, ops, att);
        */
        // 5、采用轮询的方式(一直执行selector.select(),直到返回),查询准备就绪的事件
        //正是因为会阻塞,那么基本上不可能会等于0,这就是用来判断是否向下执行的
        while (selector.select() > 0){ //注意:一般需要高并发才会出现进入多个,一般多线程可能由于先后问题,所以你测试的时候,基本只会有一个

            // 6、集合中就是所有已经准备就绪的操作集合
            Set<SelectionKey> set = selector.selectedKeys();//取出已经对应数组的操作
            //来一个对应一个
            Iterator<SelectionKey> selectionKeys = set.iterator();
            //这里如果不加泛型的话,那么下面的一些操作,就要强转了
            //或者用Object也可以,父类类型指向子类类型,多态的使用

            while (selectionKeys.hasNext()){

                // 7、已经‘准备就绪’的事件
                SelectionKey selectionKey = selectionKeys.next();//得到后,会指向下一个元素

                //8、判断事件的类型 ---ACCEPT
                //可以调用方法来判断,但由于这里只写了一种类型,那么就不演示了
                //如selectionKey.isAcceptable()方法,判断是否为accept类型,即连接类型
                //当然,还有其他类型,对应于SelectionKey.OP_ACCEPT的类型
                ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) 
                    selectionKey.channel();

                // 9、接收客户端发送过来的数据
                SocketChannel socketChannel = serverSocketChannel1.accept();
                //由于这里只有一个变量,即对应一个端口,实现不了多个不同端口的连入

                // 10、读取数据
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                int len = socketChannel.read(byteBuffer);

                // 11、打印
                System.out.println(new String(byteBuffer.array(),0,len));

                // 12、资源关闭
                socketChannel.close();

            }
            selectionKeys.remove();
            //防止其他的进来时,会操作一样的,因为前面的阻塞不能有一样的,如果没有删除,发现还存在,那么对应直接返回-1或者内部出现异常(可能不会打印出来),使得结束这个循环,那么自然整个程序结束
            }

        serverSocketChannel.close();
        serverSocketChanne2.close();
        serverSocketChanne3.close();


    }
}

package com.lagou.task23.selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIO客户端 {

    public static void main(String[] args) throws IOException {


        // 1. 创建对象
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 7777));

        // 创建缓冲区数组
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 设置数据
        byteBuffer.put("哈哈哈".getBytes());

        byteBuffer.flip();

        // 输出数据
        socketChannel.write(byteBuffer);

        socketChannel.close();

    }
}

就是先注册,而注册时,每个管道连接都对应一个注册和一个数组(存放操作)
而到你连接进来时,注册会发现,而使得可以循环
然后将你的数组的操作,放入一个可以迭代的地方,使得可以取出,最后最好还是要删你操作的,防止其他操作与你重复
轮询查询就绪操作-------------------------------------
万事俱备,下一步是查询就绪的操作
通过Selector的 select() 方法,可以查询出已经就绪的通道操作,这些就绪的状态集合
包存在一个元素是SelectionKey对象的Set集合中
select()方法返回的int值,表示有多少通道已经就绪而一旦调用select()方法,并且返回值不为0时
通过调用Selector的selectedKeys()方法来访问已选择键集合,然后迭代集合的每一个选择键元素
根据就绪操作的类型,完成对应的操作
像这种一个线程可以处理多个访问后的结果,我们通常会成为多路复用,简单来说多路复用就是保留相当于多个线程的结果,而这个结果由一个线程来完成,又或者说,由一个监听变成了多个监听
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐