Java:33-NIO
·
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
例:- 如果我们创建一个新的容量大小为10的ByteBuffer对象,在初始化的时候,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()方法来访问已选择键集合,然后迭代集合的每一个选择键元素
根据就绪操作的类型,完成对应的操作
像这种一个线程可以处理多个访问后的结果,我们通常会成为多路复用,简单来说多路复用就是保留相当于多个线程的结果,而这个结果由一个线程来完成,又或者说,由一个监听变成了多个监听
更多推荐



所有评论(0)