Java NIO学习对 拉勾教育Java学习的 学习笔记(里面有很多都是拉勾课件里的内容,我在听课的时候添加了些东西。有很多内容都在代码中,导致代码上很乱。。慢慢改吧)
- 1 什么是NIO
- 1.1 标准IO回顾
- 1.2 NIO
- 1.3 NIO的作用
- 1.4 流 和 块
- 1.5 NIO新特性
- 1.5.1 面向流(Stream Oriented)和面向缓冲区(Buff Oriented)
- 1.5.2 阻塞(Blocking IO)和非阻塞(Non-Blocking IO)
- 1.5.3 选择器(Selector)
- 1.6 NIO的核心组件
- 2 核心组件
- 2.1 缓冲区(buffer)
- 2.1.1 概念
- 2.1.2 创建缓冲区的三种方式
- 2.1.3 缓冲区常用方法
- 2.1.4 核心属性
- 2.2 Channel管道
- 2.2.1 概念
- 2.2.2 Channel API
- 2.2.3 FIleChannel的基本使用
- 2.2.4 网络编程中NIO Channel管道的使用
- 2.2.5 accept阻塞问题的解决
- 4 Selector选择器
- 4.1 多路复用器的概念
- 4.2 Selector与Channel的关系
- 4.3 可选择通道(SelectableChannel)
- 4.4 Channel如何注册到Selector
- 4.5 选择键(SelectionKey)
- 4.6 Selector的使用流程
- 4.6.1 创建Selector
- 4.6.2 将Channel注册到Selector
- 4.6.3 轮询查询就绪操作
- 5 拉勾教育课件里的代码 ---- NIO编程实例
什么是IO?
- IO:Input OutPut(输入 输出)
- IO技术的作用:解决设备和设备之间数据传输的问题
- IO使用在上传 下载 XML的解析…对数据的读写操作
Java程序中,对于数据的输入/输出操作 都是以“流”的方式进行的。java.io包下提供了各种“流”类的接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。
1.2 NIOJava New IO,JDK1.4后提供的新的 IO API,在Java API中提供了两套NIO:
一套是针对标准输入输出的NIO,另一套是网络编程的NIO。
NIO对于文件的读写速度快于标准IO,包括在大数据量涌入的情况下,NIO相对于标准IO,对于并发的支持要更好一些,NIO也是很多底层框架的实现。
NIO与IO有这共同的作用和目的,都是对数据的处理,但是它们的实现方式不同:
- IO是以 流 的方式处理数据
- NIO是以 块 (缓冲区)的方式处理数据
面向流的IO是一个字节一个字节的处理数据,对于一个输入一次产生一个一个字节,那么一个输出流就一次消费一个字节
面向块的IO系统是以块的形式处理数据,每一个操作都在一步中产生消费一个数据块。
这就解释了为什么IO比NIO处理数据库的速度要快。标准IO在使用时就像是一个水龙头,一滴一滴的滴水,每次滴一滴,就处理一滴;而NIO则是我打开水龙头,“块(缓冲区)”就像个水桶,我快速的接满一桶水,然后对这这桶水进行处理。
1.5 NIO新特性Blocking IO、Non Blocking IO、Selectors 是对网络编程的IO来说的
1.5.1 面向流(Stream Oriented)和面向缓冲区(Buff Oriented) 1.5.2 阻塞(Blocking IO)和非阻塞(Non-Blocking IO)当进程执行时,需要的数据还未就绪时,是否要处在等待状态。如果是一直在等待数据,数据不就绪,我就一直等,那么这就是阻塞(Blocking IO);如果进程不是等待,还是在数据准备的这段时间去忙别的去了,那么就是非阻塞(Non-Blocking IO)。
例如:
NIO可以使用异步非阻塞的模式,所以加入了Selector。下面单独讲
1.6 NIO的核心组件- 管道(Channel)
- 缓冲区(Buffer)
- 选择器(Selector):多路复用器
在上面我们说到NIO不是使用流的方式处理数据,而是以buffer缓冲区和Channel管道配合使用来处理数据。
Selector选择器则是因为NIO可以使用异步的非阻塞模式才加入的东西
buffer缓冲区和Channel管道是怎么配合使用并且完成处理的呢?与面向流的操作有什么区别呢?
1.面向流的IO,程序读取文件首先创建一个输入流(new FileInputSteam(“d://你好.jpg”)),然后就是对流进行一个处理,写入文件首先会创建一个输出流(new FileOutPutSteam(“d://你不好.jph”)),然后在进行写入。在面向流的操作中,就是一个单向的操作,要么去读,要么去写。
2.面向缓冲区的NIO,在对数据做处理时,就像是在中国的魔都上海和京都北京之间输送货物,我们把数据比作货物,把buffer缓冲区比作拉货的火车,Channel管道就是火车轨道了,我们把要运输的货物(数据)放在火车(buffer缓冲区)里,通过连接京都和魔都的火车轨道(Channel管道)进行运输,这样的话火车(buffer缓冲区)是可以在火车轨道(Channel管道)中来回走的,所以我们说这是一个双向的操作。
总结:
- NIO中,对数据的处理,是通过以buffer缓冲区和Channel管道配合使用来处理数据。
- Channel管道:不与数据进行交互,作用就是负责运算buffer缓冲区。搞运输的
- buffer缓冲区:与数据直接进行交互。所有对数据的存取操作都是在对buffer缓冲区进行操作。搞数据的
相对于标准IO的流来说,对数据的操作就是单向的,要么读要么写。在NIO中基于Channel管道这么一个概念,对数据的读写就都是双向的。
2 核心组件 2.1 缓冲区(buffer) 2.1.1 概念在上面提到的运输货物的例子中,缓冲区(buffer)被比作是火车,也就是装货物的。在Java里,buffef就是用来存放具体要被传输的数据,比如文件、socket等,将数据放入buffer内,然后在放入channel管道中运输。
在Java和各种开发语言中,一般都是用列表、数组等存放数据。Buffer缓冲区就是一个数组,它用来存放各类的数据。根据Java中的八种基本类型(byte、short、int、long、float、double、char、String、boolean),除了boolean类型除外(boolean只是存true和false),其他的都有相应的缓冲区,它们都继承自Buffer抽象类
- ByteBuffer:存储字节数据到缓冲区
- ShortBuffer:存储字符串数据到缓冲区
- CharBuffer: 存储字符数据到缓冲区
- IntBuffer:存储整数数据到缓冲区
- LongBuffer:存储长整型数据到缓冲区
- DoubleBuffer:存储小数到缓冲区
- FloatBuffer:存储小数到缓冲区
其中用的最多的就是ByteBuffer类(二进制数据)
以下对于Buffer的方法和属性,都使用ByteBuffer类来进行举例
2.1.2 创建缓冲区的三种方式代码演示:
import java.nio.ByteBuffer;
public class CreateBufferTest {
public static void main(String[] args) {
// 1.(常用,建议)在堆中创建缓冲区:allocate(int capacity) capacity:容量
ByteBuffer byteBuffer1 = ByteBuffer.allocate(10);
// 2.(了解)在系统内存中创建缓冲区:allocateDirect(int capacity)
ByteBuffer byteBuffer2 = ByteBuffer.allocateDirect(10);
// 3.(了解)通过普通数组创建缓冲区:wrap(byte[] arr)
byte[] arr = {1,2,3};
ByteBuffer byteBuffer3 = ByteBuffer.wrap(arr);
}
}
2.1.3 缓冲区常用方法
核心方法有两个
put(byte b):给缓冲区数组添加元素
get(index) or get(byte[] dst):根据索引获取缓冲区数组中的元素
import java.nio.ByteBuffer;
import java.util.Arrays;
public class BufferMethodsTest {
public static void main(String[] args) {
// 1.创建一个ByteBuffer缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
// 2.向缓冲区里添加元素 put(byte b)
byteBuffer.put((byte)1);
byteBuffer.put((byte)2);
byteBuffer.put((byte)3);
// 3.打印ByteBuffer缓冲区内所有数据 byte[] byteArr = byteBuffer.array()
System.out.println(Arrays.toString(byteBuffer.array()));
// 4.通过下标索引获取缓冲区内的数据 get(index)
byte b = byteBuffer.get(1);
System.out.println("byteBuffer[1] = " + b);
}
}
输出结果:
2.1.4 核心属性[1, 2, 3, 0, 0, 0, 0, 0, 0, 0]
byteBuffer[1] = 2
在Buffer抽象类中有四个属性变量,我们进去Buffer的代码中可以看到,这4个核心变量属性提供了关于其所包含的数组的信息
- capacity: 容量,也就是缓冲区能够容纳数据元素的最大数量,容量在缓冲区创建时被设定(allocate(int capacity)),并且永远不能被改变。(不能被改变的原因也很简单,底层是数组嘛)
- limit: 界限(限制),它规定了缓冲区内可以被操作的数据的大小,代表着当前缓冲区内有多少数据是可以被操作的,从limit开始,后面的数据无法被操作。
- position: 位置,下一个要被读或写的位置。该属性会随着get()和put()方法的使用而改变
- mark: 标记,用于记录上一次被读写的位置
接下来我们从代码中来学习属性中的一些问题:(这里主要介绍了当对缓冲区进行操作时,缓冲区属性值的变换,以及 flip() 方法和 clear() 方法的使用,对缓冲区读还是写状态的切换。)
注:为了方便自己学习,代码中写了很多注释,基本的知识点都在里面了,但是看起来太多了就很烦,所以在看的时候,主要是看属性值的改变,以及flip() 和 clear()的使用
import java.nio.ByteBuffer;
public class BufferAttributeTest {
public static void main(String[] args) {
// 1.创建一个ByteBuffer缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
// 2.输出查看buffer属性值
System.out.println("初始--容量capacity = " + byteBuffer.capacity()); // 10
System.out.println("初始--界限limit = " + byteBuffer.limit()); // 10
System.out.println("初始--位置position = " + byteBuffer.position()); // 0
System.out.println("初始--标记mark = " + byteBuffer.mark()); // java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
System.out.println("=====================================================================");
// 3.问题一:当我们进行put操作时,buffer属性值有什么变化
// // 使用put添加一些数据元素
String str = "puffHub";
byteBuffer.put(str.getBytes());
// // 输出查看使用put操作后的属性值,发现 position 和 mark 的值发生改变
// // limit的值并没有发生改变,但这并不意味着其他的方法在操作时limit的值不会发生改变 它也是会改变的
System.out.println("put后--容量capacity = " + byteBuffer.capacity()); // 10
System.out.println("put后--界限limit = " + byteBuffer.limit()); // 10
System.out.println("put后--位置position = " + byteBuffer.position()); // 7
System.out.println("put后--标记mark = " + byteBuffer.mark()); // java.nio.HeapByteBuffer[pos=7 lim=10 cap=10]
System.out.println("=====================================================================");
// 4.问题二:上面的操作是写入操作,且position的值已经为 7 了,也就是说下一次操作无论是读还是写,
// // 都是要从 7 这个位置开始了,但是我们并不想这样 因为 7 以后是没有值的,我们想从 0 的位置开始读,
// // 并且读取的内容是"puffHub",该怎么去读呢?
// // 从上面的叙述中可以得出对缓冲区的操作都是从position到limit,所以我们只需要改变position和limit的值就好了。
// // 这里我们用到 NIO 中给的flip()方法
byteBuffer.flip(); // 切换成“读”模式
// // flip之后,缓冲区中limit和position的值发生了相应的改变
// // 变化一:limit变成position的值 变化二:position的值变成 0
System.out.println("flip后--容量capacity = " + byteBuffer.capacity()); // 10
System.out.println("flip后--界限limit = " + byteBuffer.limit()); // 7
System.out.println("flip后--位置position = " + byteBuffer.position()); // 0
System.out.println("flip后--标记mark = " + byteBuffer.mark()); // java.nio.HeapByteBuffer[pos=0 lim=7 cap=10]
// 5.数据的读取
// // 创建一个byte数组,长度为limit(因为我要在缓冲区里读的数据就是这么长,所以就用这个)
byte[] bytes = new byte[byteBuffer.limit()];
// // 将读出的数据放在字节数组中 get(byte[] dst)
byteBuffer.get(bytes);
// // 打印
System.out.println(new String(bytes, 0, bytes.length));
System.out.println("=====================================================================");
// // get方法也会改变position的值
System.out.println("get后--容量capacity = " + byteBuffer.capacity()); // 10
System.out.println("get后--界限limit = " + byteBuffer.limit()); // 7
System.out.println("get后--位置position = " + byteBuffer.position()); // 7
System.out.println("get后--标记mark = " + byteBuffer.mark()); // java.nio.HeapByteBuffer[pos=7 lim=7 cap=10]
System.out.println("=====================================================================");
// 6.读取到数据后,如果还想要返回一个写的状态,我们可以调用clear()方法,清空缓冲区
// // clear()方法的作用是重置缓冲区,核心属性回归到写的模式,重置核心属性回到创建时的值
// // !!而且!!缓冲区内的值还是存在的!!但是是被遗忘的,就是说无法通过get方法读取到。
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
System.out.println("clear后--标记mark = " + byteBuffer.mark()); // java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
// // 这里我们可以确定 原有的值确实是存在的,只不过读取出来的是原有值的ASCII码
byte b = byteBuffer.get(2);
System.out.println(b); // 102 ==> f
}
}
2.2 Channel管道
2.2.1 概念
由 java.nio.channels 包定义的 Channel管道,类似传统IO中的“流(Stream)”,但又和流有很大的区别。
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中(白话: 就是数据传输用的通道,作用是打开到IO设备的连接,文件、套接字都行)
| 区别 | Channel | stream |
|---|---|---|
| 是否支持异步 | 支持 | 不支持 |
| 是否可双向传输数据 | 可以,既可以从通道读取数据,也可以向通道写入数据 | 不能,只能单向 |
| 是否结合Buffer使用 | 必须结合Buffer使用 | 不需要 |
| 性能 | 较高 | 较低 |
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过 UDP 读写网络中的数据通道。
- SocketChannel:通过 TCP 读写网络中的数据。 (常用)
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来 的连接都会创建一个 SocketChannel。
FileChannel主要是对本地资源的处理,其余三个 是对网络资源的处理
2.2.3 FIleChannel的基本使用import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTest {
public static void main(String[] args) throws IOException {
// 结合标准IO,通道依赖于IO流.创建文件输入输出流
FileInputStream fileInputStream = new FileInputStream("D:\img\image-20210304173004802.png");
FileOutputStream fileOutputStream = new FileOutputStream("D:\\img\副本.png");
// 通过IO流获取Channel通道
FileChannel fileChannel1 = fileInputStream.getChannel();
FileChannel fileChannel2 = fileOutputStream.getChannel();
// 创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (fileChannel1.read(byteBuffer) != -1){
// 切换读写状态(更改buffer核心元素数据,指针位置)
byteBuffer.flip();
// 输出(这里存在两个问题)
// 第一个:输出一次后,position位置会移到最后,我们知道,无论读还是写,操作的都是从position到limit中间的数据元素
// // 循环第一次输出后position位置移到最后,那接下来的循环就无法读取到数据,且条件永远不会等于 -1 就会造成死循环
// // 所以我们需要通过clear方法重置position的位置。
// 第二个:在我们最后一次输出数据的时候,因为我们给定缓冲区的大小是固定的,但是文件的大小有区别,所以当最后一次输出时
// // 1024个字节的大小不一定会刚刚好全部占满,这时候,只是用Clear重置位置会导致最后一次输出的时候,输出一些空的字节
// // 所以我们需要在每次输出缓冲区内的数据前,调用flip()方法(前面的代码讲到了该方法的作用以及对核心元素的影响)
fileChannel2.write(byteBuffer);
// 还原指针位置(更改buffer核心元素数据,指针位置)
byteBuffer.clear();
}
// 关闭流
fileOutputStream.close();
fileInputStream.close();
}
}
2.2.4 网络编程中NIO Channel管道的使用
- 客户端
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws IOException {
// 1.创建对象
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",9999));
// 2.创建字节缓冲区 并 设置数据 输出
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("你好,世界".getBytes());
// 一定要重视flip()方法的使用,这里如果没有该方法,服务器端就会读取到很多空字节
byteBuffer.flip();
socketChannel.write(byteBuffer);
socketChannel.close();
}
}
- 服务端
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1.创建服务器端对象,监听对应的端口,并绑定端口
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9999));
// 2.连接客户端,目前还是阻塞状态
SocketChannel socketChannel = serverSocketChannel.accept();
// 3.读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(byteBuffer);
// 这里再服务端进行读取的时候,并没有用到flip()方法或者是clear()方法,但是却没有读取到空字节
// 原因是,len 是 读取到的数据长度 客户端发送多少,len就是多少
System.out.println(new String(byteBuffer.array(),0,len));
}
}
2.2.5 accept阻塞问题的解决
设置非阻塞的方法 ServerSocketChannel.configureBlocking(false)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NonBlockingIOServer {
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.连接客户端 , accept不会处于阻塞状态
// 如果连接成功就是sc对象,如果没有连接就是sc = null( 解决空指针异常 )
SocketChannel socketChannel = serverSocketChannel.accept();
if(serverSocketChannel != null){
// 3.读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(),0,len));
break;
} else {
// 没有客户访问
System.out.println("因为处于非阻塞状态,此时可以去忙别的事去了");
Thread.sleep(3000);
}
}
}
}
4 Selector选择器
Selector选择器,又被称为多路复用器
4.1 多路复用器的概念一个选择器可以同时监听多个服务器端口, 帮多个服务器端口同时等待客户端的访问
非多路复用: 服务器如果想同时监听多个客户端,需要开启相同数量的线程去监听每一个客户端。比较占用系统资源,线程之间的切换会对操作系统造成很高的代价。
多路复用:一个线程可以同时监听多个端口(通过Select多路复用器来实现)。节省系统资源。
Selector选择器 是 channel通道 的 多路复用器,可以通过Selector同时监听多个通道IO(输入输出)的情况
他们之间是 注册 的关系
Selector的作用是什么?
负责监听事件和选择事件的对应通道。
选择器提供选择执行已经就绪的任务的能力。从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。可以只用一个线程来回切换,处理所有的通道,这样会大量的减少线程之间上下文切换的开销。
注意:并不是所有的通道Channel都会被Select复用,比如FileChannel。
Java定义了一个SelectableChannel的抽象类,只有继承了这个抽象类的Channel,才会被Select复用
SelectableChannel抽象类,提供了实现通道的可选择性所需要的公共方法
在这里要注意的是,通道和选择器并不是绑定一 一对应的,他们并不是一对一的关系,同一个通道可以被多个选择器选择,但对于选择器而言,同一个选择器只能被注册一次。
4.4 Channel如何注册到Selector通道和选择器之间的关系,使用注册的方式完成。SelectableChannel可以被注册到Selector对象上,在注册的时候,需要指定通道的哪些操作,是Selector感兴趣的。
使用Channel.register(Selector sel,int ops)方法,将一个通道注册到一个选择器时。
第一个参数Selector sel:指定通道要注册的选择器是谁(一个Channel可以注册到多个选择器上,所以要指定选择器)
第二个参数int ops:指定选择器需要查询的通道操作 (选择器对通道的那些就绪行为感兴趣)
可以供选择器查询的通道操作,从类型来分,包括以下四种:
(1)可读 : SelectionKey.OP_READ
(读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了)
(2)可写 : SelectionKey.OP_WRITE
(写就绪事件,表示已经可以向通道写数据了)
(3)连接 : SelectionKey.OP_CONNECT
(连接就绪事件,表示客户端与服务器的连接已经建立成功)
(4)接收 : SelectionKey.OP_ACCEPT
(接收连接进行事件,表示服务器监听到了客户连接,服务器就可以接收这个连接了)
如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;
在调用register方法的时候,可以做到注册到Selector选择器,也可以指定通道操作, 但是对于注册的这个selector选择器来说,它是怎么知道我们指定的事件什么时候会处于就绪状态呢? 这里有个问题是,虽然我们指定了监听事件,但这并不意味着selector选择器就可以自动的监听了,也就是说 selector选择器并不会去自动的监听指定的事件,而是selector要去调用方法去查询它感兴趣的事件有没有发生。4.5 选择键(SelectionKey)
Channel和Selector的关系确定好后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到。这个工作,使用选择器Selector的select()方法完成。select方法的作用,对感兴趣的通道操作,进行就绪状态的查询。
Selector可以不断的查询Channel中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是Selector感兴趣的操作,就会被Selector选中,放入选择键集合中。
select() :选择器等待客户端连接的方法 阻塞问题: 1.在开始没有客户访问的时候是阻塞的 2.在有客户来访问的时候方法会变成非阻塞的 3.如果客户的访问被处理结束之后,又会恢复成阻塞的 selectedKeys() :选择器会把被连接的服务端对象放在Set集合中,这个方法就是返回一个Set集合4.6 Selector的使用流程 4.6.1 创建Selector
Selector对象是通过调用静态工厂方法open()来实例化的,如下:
// 1、获取Selector选择器
Selector selector = Selector.open();
4.6.2 将Channel注册到Selector
要实现Selector管理Channel,需要将channel注册到相应的Selector上,如下:
// 2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4、绑定连接
serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));
// 5、将通道注册到选择器上,并制定监听事件为:“接收”事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
上面通过调用通道的register()方法会将它注册到一个选择器上。
首先需要注意的是:
与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException
4.6.3 轮询查询就绪操作万事俱备,下一步是查询就绪的操作。
通过Selector的 select() 方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,包存在一个元素是SelectionKey对象的Set集合中。
select()方法返回的int值,表示有多少通道已经就绪
而一旦调用select()方法,并且返回值不为0时,下一步该干啥?
通过调用Selector的selectedKeys()方法来访问已选择键集合,然后迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作:
整个代码的使用(乱糟糟的 不推荐看,可以看再看后面的代码):
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 SelectorServer {
public static void main(String[] args) throws IOException {
// 小目标:通道注册到选择器上
// 1. 获取selector选择器
Selector selector = Selector.open();
// 2.获取通道对象
ServerSocketChannel serverSocketChannel1 = ServerSocketChannel.open();
ServerSocketChannel serverSocketChannel2 = ServerSocketChannel.open();
ServerSocketChannel serverSocketChannel3 = ServerSocketChannel.open();
serverSocketChannel1.bind(new InetSocketAddress(9999));
serverSocketChannel2.bind(new InetSocketAddress(8888));
serverSocketChannel3.bind(new InetSocketAddress(7777));
// 3.※设置为非阻塞,(与selector同时使用时,channel必须处在非阻塞状态下,不然会抛出异常)
serverSocketChannel1.configureBlocking(false);
serverSocketChannel2.configureBlocking(false);
serverSocketChannel3.configureBlocking(false);
// 4.完成注册操作;并且指定选择器需要查询的通道操作,也就是制定 监听事件 为 “接收事件”
serverSocketChannel1.register(selector, SelectionKey.OP_ACCEPT);
serverSocketChannel2.register(selector, SelectionKey.OP_ACCEPT);
serverSocketChannel3.register(selector, SelectionKey.OP_ACCEPT);
// 5.select() 查询已经就绪的通道操作 返回值:表示有多少通道已经就绪 0:就是没有就绪的通道操作
// 调用后会进入阻塞状态,至少有一个通道上的操作就绪了,才会解除阻塞状态
// int select = selector.select();
// // 没有就绪的通道,就会处于阻塞状态,下面的代码就不会执行
// System.out.println(select);
// 6.通过轮询查询已经就绪的通道操作(SelectionKey)
while (selector.select() > 0){
// 集合中就是所有已经准备就绪的操作(事件)
Set keySet = selector.selectedKeys();
// 遍历上面集合,去判断SelectionKey的类型是哪一种(READ WRITE ACCEPT CONNECT),然后分别进行处理。
Iterator selectionKeys = keySet.iterator();
while (selectionKeys.hasNext()){
// 已经准备就绪的事件
SelectionKey selectionKey = selectionKeys.next();
// 判断SelectorKey的类型
if(selectionKey.isAcceptable()){
// 如果为ACCEPT类型
// SelectableChannel channel = selectionKey.channel();
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(),0,len));
// 资源的关闭
socketChannel.close();
}else if(selectionKey.isConnectable()){
return;
}else if(selectionKey.isReadable()){
return;
}else {//selectionKey.isWritable()
return;
}
}
// 移除选择键
selectionKeys.remove();
}
serverSocketChannel3.close();
serverSocketChannel2.close();
serverSocketChannel1.close();
}
}
5 拉勾教育课件里的代码 ---- NIO编程实例
代码不要看上边那个 还是看这个吧,这个更清楚一些
客户端:
public static void main(String[] args) throws IOException {
//创建客户端
SocketChannel sc = SocketChannel.open();
//指定要连接的服务器ip和端口
sc.connect(new InetSocketAddress("127.0.0.1",9000));
//创建缓冲输出
ByteBuffer buffer = ByteBuffer.allocate(1024);
//给数组添加数据
buffer.put("拉勾教育".getBytes());
//切换
buffer.flip();
//输出数据
sc.write(buffer);
//关闭资源
sc.close();
}
服务端
package com.lagou.selector;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Set;
public class Demo服务端 {
public static void main(String[] args) throws IOException {
//创建服务端对象
ServerSocketChannel ssc1 = ServerSocketChannel.open();
ssc1.bind(new InetSocketAddress(8000));
//设置非阻塞
ssc1.configureBlocking(false);
//创建服务端对象
ServerSocketChannel ssc2 = ServerSocketChannel.open();
ssc2.bind(new InetSocketAddress(9000));
ssc2.configureBlocking(false);
//创建服务端对象
ServerSocketChannel ssc3 = ServerSocketChannel.open();
ssc3.bind(new InetSocketAddress(10001));
ssc3.configureBlocking(false);
//创建选择器对象
Selector s = Selector.open();
//两个服务器都要交给选择器来管理
ssc1.register(s, SelectionKey.OP_ACCEPT);
ssc2.register(s, SelectionKey.OP_ACCEPT);
ssc3.register(s, SelectionKey.OP_ACCEPT);
//获取集合
//selectedKeys() :返回集合,集合作用存放的是被连接的服务对象的key
Set set = s.selectedKeys();
System.out.println("集合中元素的个数: " + set.size()); //0(没有服务端被访问的时候显示0)
//select():这是选择器连接客户端的方法
s.select();
System.out.println("集合中元素的个数: " + set.size()); //1(有一个服务端被访问的时候显示1)
}
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class Selector服务端 {
public static void main(String[] args) throws IOException {
// 1、获取Selector选择器
Selector selector = Selector.open();
// 2、获取通道
ServerSocketChannel ssc1 = ServerSocketChannel.open();
ServerSocketChannel ssc2 = ServerSocketChannel.open();
ServerSocketChannel ssc3 = ServerSocketChannel.open();
// 3.设置为非阻塞
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
// 4、绑定连接
ssc1.bind(new InetSocketAddress(8000));
ssc2.bind(new InetSocketAddress(9000));
ssc3.bind(new InetSocketAddress(10000));
// 5、将通道注册到选择器上,并注册的操作为:"接收"操作
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector, SelectionKey.OP_ACCEPT);
ssc3.register(selector, SelectionKey.OP_ACCEPT);
// 6、采用轮询的方式,查询获取"准备就绪"的注册过的操作
while (selector.select() > 0) {
// 7、获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
Iterator selectedKeys = selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
// 8、获取"准备就绪"的事件
SelectionKey selectedKey = selectedKeys.next();
// 9、获取ServerSocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectedKey.channel();
// 10、接受客户端发来的数据
SocketChannel socketChannel = serverSocketChannel.accept();
// 11、读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int length = 0;
while ((length = socketChannel.read(byteBuffer)) != -1) {
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, length));
byteBuffer.clear();
}
socketChannel.close();
}
// 12、移除选择键
selectedKeys.remove();
}
// 13、关闭连接
ssc1.close();
ssc2.close();
ssc3.close();
}
}



