NIO简介

Java NIO(New IO Non Blocking IO)可以理解为新IO非阻塞IO,是从Java1.4开始引入的一个新的IO API,与原来的IO有同样的作用和目的,但是使用方式完全不同

NIO支持面向缓冲区、基于通道的IO操作。简单说,NIO将以更高效的方式进行文件的读写

传统IO与NIO的区别?

传统IO是面向流的,且是单向操作的。分别需要输入流跟输出流,流直接面对其中的数据,来进行数据的传输(即把文件通过byte[]数组进行传输),可以简单的将传统的IO理解为日常生活中的水流

NIO中建立连接的是通道,可以将通道简单的理解为日常生活中的铁路,但是铁路本身并不具备传输功能,我们需要通过缓冲区来进行传输数据,缓冲区可以对等的理解为火车。所以我们可以得知,NIO是面向缓冲区的,且是双向操作的

  • 传统IO:面向流,直接传输数据,单向操作,阻塞IO(水流在水管中单向流动)
  • NIO:面向缓冲区,通道只做连接,传输依靠缓冲区,双向操作,非阻塞IO(火车在铁路上来回运输)

通道与缓冲区

NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)

通道:表示打开到IO设备(文件等)的连接,使用NIO之前,一定要获取连接IO设备的通道【铁路】

缓冲区:就是数组,负责不同数据类型的数据存储【火车】

可以简单的理解为:Channel负责传输,Buffer负责存储

缓冲区的使用

根据数据类型不同,提供对应类型的缓冲区(boolean除外)

包含如下:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer

上述缓冲区的管理方式几乎一致,通过allocate()获取缓冲区

缓冲区存取数据的两个核心方法:

  • put():存入数据到缓冲区中
  • get():获取缓冲区中的数据

缓冲区中四个核心属性:

  • capacity:容量,表示缓冲区中最大存储数据的容量,一旦声明不能改变(因为底层是数组)
  • limit:界限,还是缓冲区中可以操作的数据的大小(limit后数据不能进行读写)
  • position:位置,表示缓冲区中正在操作数据的位置
  • mark:标记,表示记录当前position的位置,可以通过reset()恢复到mark位置

以上参数的限制:0 <= mark <= position <= limit <= capacity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 1、分配一个大小为1024的缓冲区
ByteBuffer bf = ByteBuffer.allocate(1024);
System.out.println("-----------allocate()-----------");
System.out.println("当前位置:" + bf.position());
System.out.println("当前界限:" + bf.limit());
System.out.println("当前容量:" + bf.capacity());
// 2、利用put()存入数据到缓冲区中
System.out.println("-----------put()-----------");
bf.put("abcde".getBytes());
System.out.println("当前位置:" + bf.position());
System.out.println("当前界限:" + bf.limit());
System.out.println("当前容量:" + bf.capacity());
// 3、切换读数据模式
System.out.println("-----------flip()-----------");
bf.flip();
System.out.println("当前位置:" + bf.position());
System.out.println("当前界限:" + bf.limit());
System.out.println("当前容量:" + bf.capacity());
// 4、利用get()读取缓冲区中的数据
System.out.println("-----------get()-----------");
byte[] dst = new byte[2];
bf.get(dst);
System.out.println("读取数据:" + new String(dst, 0, dst.length));
System.out.println("当前位置:" + bf.position());
System.out.println("当前界限:" + bf.limit());
System.out.println("当前容量:" + bf.capacity());
// 4、利用get()读取缓冲区中的数据
System.out.println("-----------get()-----------");
byte[] dst1 = new byte[3];
bf.get(dst1);
System.out.println("读取数据:" + new String(dst1, 0, dst1.length));
System.out.println("当前位置:" + bf.position());
System.out.println("当前界限:" + bf.limit());
System.out.println("当前容量:" + bf.capacity());
// 5、利用rewind()重复读取数据 回到flip()后的位置
System.out.println("-----------rewind()-----------");
bf.rewind();
System.out.println("当前位置:" + bf.position());
System.out.println("当前界限:" + bf.limit());
System.out.println("当前容量:" + bf.capacity());
// 6、利用clear()清空缓冲区 数据依然存在 只是位置界限恢复初始位置 处于被遗忘状态
System.out.println("-----------clear()-----------");
bf.clear();
System.out.println("当前位置:" + bf.position());
System.out.println("当前界限:" + bf.limit());
System.out.println("当前容量:" + bf.capacity());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
控制台打印信息

-----------allocate()-----------
当前位置:0
当前界限:1024
当前容量:1024
-----------put()-----------
当前位置:5
当前界限:1024
当前容量:1024
-----------flip()-----------
当前位置:0
当前界限:5
当前容量:1024
-----------get()-----------
读取数据:ab
当前位置:2
当前界限:5
当前容量:1024
-----------get()-----------
读取数据:cde
当前位置:5
当前界限:5
当前容量:1024
-----------rewind()-----------
当前位置:0
当前界限:5
当前容量:1024
-----------clear()-----------
当前位置:0
当前界限:1024
当前容量:1024

这里我相信大家肯定跟我同样有一个疑问,到底什么时候使用flip()进行切换呢?

下面我们看一下flip()这个方法里面都干了什么,你就会明白什么时候使用了

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Flips this buffer. The limit is set to the current position and then
* the position is set to zero. If the mark is defined then it is discarded.
* 会将缓冲区的当前位置赋值给缓冲区界限,将当前位置修改为0,如果设置了标记也会一并清空
* 简单点说,如果我们使用了put()方法后,如果需要使用get()进行读取数据的话,我们要执行一次flip()
* 这样我们的position才会从头开始进行读取,读取的最大长度是我们之前写入的最长位置,即当前的界限大小
*/
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}

下面我们重新存储一下数据 进行mark()reset()的演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定义要放入缓冲区中的字符串
String str = "abcdef";
// 初始化一个1024大小的缓冲区
ByteBuffer bf = ByteBuffer.allocate(1024);
// 使用put() 将字符串放入到缓冲区中
bf.put(str.getBytes());
// 使用flip() 切换读写模式
bf.flip();
// 定义一个与缓冲区界限一样大的字节数组用于存放数据
byte[] b = new byte[bf.limit()];
// 使用get() 读取前两个字符
bf.get(b, 0, 2);
System.out.println("读取到的字符:" + new String(b, 0, 2) + " 当前位置:" + bf.position());
// 使用mark() 进行标记
bf.mark();
System.out.println("使用mark进行标记");
// 继续读取两个
bf.get(b, 2, 2);
System.out.println("读取到的字符:" + new String(b, 2, 2) + " 当前位置:" + bf.position());
// 使用reset() 恢复到mark位置
bf.reset();
System.out.println("已使用reset恢复 当前位置:" + bf.position());
// 判断是否还有可读取的数据
if (bf.hasRemaining()) {
// 使用remaining() 获取剩余可读取的数量
System.out.println("剩余可读取的数量:" + bf.remaining());
}
1
2
3
4
5
6
7
控制台打印信息

读取到的字符:ab 当前位置:2
使用mark进行标记
读取到的字符:cd 当前位置:4
已使用reset恢复 当前位置:2
剩余可读取的数量:4

直接缓冲区&非直接缓冲区

非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中

直接缓冲区:通过allocateDirect()方法或通过FileChannelmap()方法,直接将缓冲区建立在物理内存中,且只有ByteBuffer支持直接缓冲区,两种方法原理相同仅获取方式不同

1
2
3
4
5
6
7
// 创建直接缓冲区
ByteBuffer bf1 = ByteBuffer.allocateDirect(1024);
// 创建非直接缓冲区
ByteBuffer bf2 = ByteBuffer.allocate(1024);
// 通过isDirect() 判断是否是直接缓冲区
System.out.println("allocateDirect:" + bf1.isDirect());
System.out.println("allocate:" + bf2.isDirect());
1
2
3
4
控制台打印信息

allocateDirect:true
allocate:false

建议:一般情况下,直接缓冲区分配和取消所需要消耗的成本都高于非直接缓冲区,所以除非直接缓冲区能在程序性能方面带来明显好处时才进行使用

通道Channel

用于源节点与目标节点的连接,在Java NIO中负责缓冲区中数据的传输

Channel本身不存储数据,因此需要配合缓冲区进行传输

通道的主要实现类

java.nio.channels.Channel接口:

  • 本地:
    • FileChannel
  • 网络:
    • SocketChannel
    • ServerSocketChannel
    • DatagramChannel

获取通道Channel三种方法

这里需要注意的是,只是获取通道的方式不同,与直接缓冲区或非直接缓冲区无关

  1. Java针对支持通道的类提供了getChannel()方法
  • 本地IO:
    • FileInputStream/FileOutputStream
    • RandomAccessFile
  • 网络IO:
    • Socket
    • ServerSocket
    • DatagramSocket
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 获取输入输出流
FileInputStream fis = new FileInputStream("d:\\xx.mp4");
FileOutputStream fos = new FileOutputStream("d:\\yy.mp4");
// 获取通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
// 分配指定大小缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将inChannel通道中的数据存入缓冲区(可以理解为 本次火车卖1024张车票)
while (inChannel.read(buffer) != -1) {
// 切换读数据模式
buffer.flip();
// 将缓冲区中的数据存入到outChannel通道中(可以理解为 1024张车票的乘客都已坐上火车)
outChannel.write(buffer);
// 清空缓冲区(可以理解为 乘客终点站都下车了 只有没人了才能卖1024张返程车票)
buffer.clear();
}
// 关闭通道
outChannel.close();
inChannel.close();
// 关闭流
fos.close();
fis.close();
  1. JDK1.7中的NIO.2针对各个通道提供了静态方法open()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 通过通道的静态方法open()获取通道 并指定模式(读、写、新建不覆盖、新建覆盖)
FileChannel inChannel = FileChannel.open(Paths.get("D:\\xx.mp4"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("D:\\yy.mp4"), StandardOpenOption.WRITE,StandardOpenOption.READ, StandardOpenOption.CREATE);
// 使用FileChannel的map()方法建立直接缓冲区
MappedByteBuffer inMapBuffer = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
// 这种写法的好处是 省略了通道读写的操作 直接对缓冲区进行读写操作
// 定义一个与缓冲区界限一样大的字节数组用于存放数据
byte[] dst = new byte[inMapBuffer.limit()];
inMapBuffer.get(dst);
outMapBuffer.put(dst);
// 关闭通道
inChannel.close();
outChannel.close();

StandardOpenOption.CREATE_NEW:如果文件不存在则创建 存在则报错

StandardOpenOption.CREATE:如果文件不存在则创建 存在则覆盖

注意:因为MapMode.READ_WRITE只有读写方式,所以StandardOpenOption必须同时指定READWRITE两种模式,否则会出现报错

  1. JDK1.7中的NIO.2Files工具类的newByteChannel()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 通过Files工具类的newByteChannel()获取通道
SeekableByteChannel inChannel = Files.newByteChannel(Paths.get("D:\\xx.mp4"), StandardOpenOption.READ);
SeekableByteChannel outChannel = Files.newByteChannel(Paths.get("D:\\yy.mp4"), StandardOpenOption.WRITE,StandardOpenOption.READ, StandardOpenOption.CREATE);
// 创建指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 将inChannel通道中的数据存入缓冲区
while (inChannel.read(buf) != -1) {
// 切换缓冲区读取数据模式
buf.flip();
// 将缓冲区中的数据存入outChannel通道中
outChannel.write(buf);
// 清空缓冲区来存放下次数据
buf.clear();
}
// 关闭通道和流
outChannel.close();
inChannel.close();

通道之间的数据传输

使用transferFrom()transferTo()对数据进行直接传输(采用直接缓冲区方式)

读数据通道使用transferTo(),写数据通道使用transferFrom(),任选一种方式即可

1
2
3
4
5
6
7
8
9
10
// 获取读数据通道
FileChannel inChannel = FileChannel.open(Paths.get("d:\\xx.mp4"), StandardOpenOption.READ);
// 获取写数据通道
FileChannel outChannel = FileChannel.open(Paths.get("d:\\yy.mp4"), StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// 使用transferTo或transferFrom 直接进行数据的传输 任选一个即可
// inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel, 0, inChannel.size());
// 关闭通道
inChannel.close();
outChannel.close();

分散(Scatter)与聚集(Gather)

分散读取:将通道中的数据分散到多个缓冲区中,按照缓冲区顺序,从通道中读取的数据依次将缓冲区填满

聚集写入:将多个缓冲区中的数据聚集到通道中,按照缓冲区顺序,写入positionlimit间的数据到通道中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 获取文件
RandomAccessFile read = new RandomAccessFile(new File("d:\\11.txt"), "rw");
// 获取读数据通道
FileChannel inChannel = read.getChannel();
// 分配指定大小缓冲区
ByteBuffer buffer1 = ByteBuffer.allocate(100);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
// 将所有缓冲区存入数组中
ByteBuffer[] bufs = { buffer1, buffer2 };
// 分散读取
inChannel.read(bufs);
// 聚集写入
RandomAccessFile write = new RandomAccessFile(new File("d:\\22.txt"), "rw");
// 获取写数据通道
FileChannel outChannel = write.getChannel();
// 将所有的缓冲区都遍历切换为读取数据模式
for (ByteBuffer buffer : bufs) {
buffer.flip();
}
// 将数据写入
outChannel.write(bufs);
// 关闭流
outChannel.close();
inChannel.close();

字符集Charset

编码:字符串 -> 字节数组

解码:字节数组 -> 字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 指定字符集
Charset charset = Charset.forName("GBK");
// 根据字符集获取编码器
CharsetEncoder encoder = charset.newEncoder();
// 根据字符集获取解码器
CharsetDecoder decoder = charset.newDecoder();
// 定义指定大小的缓冲区
CharBuffer charBuffer = CharBuffer.allocate(1024);
// 存入数据
charBuffer.put("一给我哩giao");
charBuffer.flip();
// 对存入的数据进行编码 因为要读出你存的数据 所以之前一定要flip()
ByteBuffer byteBuffer = encoder.encode(charBuffer);
// 对存入的数据进行解码
CharBuffer result = decoder.decode(byteBuffer);
// 打印解码后的结果
System.out.println("解码后的数据:" + result.toString());

那么为什么encode()编码decode()解码后不需要用flip()呢?

这里我们需要注意三点:

第一点是flip()的含义,flip()是将我们现在移动的位置position当作界限limit,然后将position重置为0,这就意味着我们只有在flip()后才可以进行get(),否则我们获取的位置是新的位置,并不是我们已经放入数据的位置

第二点为什么我们编码或解码前,需要进行flip(),因为在编码或解码的过程中,是需要读取数据的,否则怎么对你已经put()存入的数据进行读取呢?所以我们需要保证传入的缓冲区已经执行了flip()方法,确保编码跟解码的时候可以读取到数据

第三点我们可以查看encode()decode()源码发现,在原代码最后返回之前,都已经替我们执行了一次flip(),所以我们无论是编码还是解码后,都可以直接读取里面的数据而无需flip()

阻塞式网络通信

接下来我们用代码模拟阻塞式网络通信,传输一张图片

  • 客户端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 获取网络通道
SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8888));
// 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 读取本地文件 并发送到服务端
FileChannel inChannel = FileChannel.open(Paths.get("d:\\xx.jpg"),StandardOpenOption.READ);
// 将读取到的图片保存到缓冲区中
while (inChannel.read(buf) != -1) {
// 切换缓冲区读写模式
buf.flip();
// 将读取到的数据存入网络通道中
clientChannel.write(buf);
// 清空缓冲区
buf.clear();
}
// 客户端写完数据了
clientChannel.shutdownOutput();
// 定义读取的数据长度
int len = 0;
// 接收服务端的反馈
while ((len = clientChannel.read(byteBuffer)) != -1) {
// 切换缓冲区读写模式
byteBuffer.flip();
// 接收到多少就打印多少
System.out.println(new String(byteBuffer.array(), 0, len));
// 清空缓冲区
byteBuffer.clear();
}
// 关闭通道
inChannel.close();
clientChannel.close();
  • 服务端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 获取网络通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 绑定端口号
serverChannel.bind(new InetSocketAddress(8888));
// 获取客户端连接的通道
SocketChannel clientChannel = serverChannel.accept();
// 接收客户端的数据 将文件保存到本地
FileChannel outChannel = FileChannel.open(Paths.get("d:\\yy.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
// 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 循环读取客户端数据
while (clientChannel.read(buf) != -1) {
// 切换缓冲区读写模式
buf.flip();
// 将获取到的数据写入到磁盘
outChannel.write(buf);
// 清空缓冲区
buf.clear();
}
// 将服务端反馈数据写入
byteBuffer.put("服务端数据接收完成!".getBytes());
// 切换读取数据模式
byteBuffer.flip();
// 发送反馈给客户端
clientChannel.write(byteBuffer);
// 关闭通道
outChannel.close();
clientChannel.close();
serverChannel.close();

非阻塞式网络通信

当调用register(Selector sel,int ops)将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定

可以监听的事件类型(可以使用SelectionKey的四个常量表示):

  • 读:SelectionKey.OP_READ
  • 写:SelectionKey.OP_WRITE
  • 连接:SelectionKey.OP_CONNECT
  • 接收:SelectionKey.OP_ACCEPT
1
2
// 若注册时不止监听一个事件,则可以使用`"位或"`操作符连接
int i = SelectionKey.OP_READ|SelectionKey.OP_WRITE;

接下来我们用代码模拟非阻塞式网络通信,完成简易聊天室的效果

  • 客户端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取客户端通道
SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));
// 切换非阻塞模式
clientChannel.configureBlocking(false);
// 定义指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 创建键盘录入
Scanner scanner = new Scanner(System.in);
// 遍历录入数据
while (scanner.hasNext()) {
// 将数据存入缓冲区
byteBuffer.put((new Date().toString() + "\n" + scanner.next()).getBytes());
// 切换读写模式
byteBuffer.flip();
// 将缓冲区写入客户端通道
clientChannel.write(byteBuffer);
// 清空缓冲区
byteBuffer.clear();
}
// 关闭通道
clientChannel.close();
  • 服务端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 获取服务端通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 切换非阻塞模式
serverChannel.configureBlocking(false);
// 绑定端口号
serverChannel.bind(new InetSocketAddress(9999));
// 获取选择器
Selector selector = Selector.open();
// 将通道注册到选择器中 并指定监听"接收事件"
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 轮询式的获取选择器上已经"准备就绪"的事件
while (selector.select() > 0) {
// 获取该选择器中注册的"选择键"(已就绪的监听事件)
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
// 遍历迭代器 判断当前是哪种状态
while (it.hasNext()) {
// 获取当前选择键
SelectionKey key = it.next();
// 如果是接收就绪状态
if (key.isAcceptable()) {
// 获取客户端连接
SocketChannel clientChannel = serverChannel.accept();
// 切换非阻塞模式
clientChannel.configureBlocking(false);
// 将通道注册到选择器中 并指定读就绪状态
clientChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 获取当前选择键中"读就绪"状态的通道
SocketChannel clientChannel = (SocketChannel) key.channel();
// 定义指定大小缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 定义读取长度
int len = 0;
while ((len = clientChannel.read(byteBuffer)) > 0) {
// 切换读数据模式
byteBuffer.flip();
// 将数据打印
System.out.println(new String(byteBuffer.array(), 0, len));
// 清空缓冲区
byteBuffer.clear();
}
}
// 取消选择键SelectionKey
it.remove();
}
}

DatagramChannel

Java NIO中的DatagramChannel是一个能收发UDP包的通道

操作步骤:

  • 打开DatagramChannel
  • 接收/发送数据

同样,我们使用这种方式实现一个简易的聊天室

  • 发送端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取发送端通道
DatagramChannel sendChannel = DatagramChannel.open();
// 切换非阻塞模式
sendChannel.configureBlocking(false);
// 定义指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 获取键盘录入
Scanner scanner = new Scanner(System.in);
// 遍历录入数据
while (scanner.hasNext()) {
// 将数据存入缓冲区
byteBuffer.put((new Date().toString() + "\n" + scanner.next()).getBytes());
// 切换读写模式
byteBuffer.flip();
// 将缓冲区数据发送
sendChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 9999));
// 清空缓冲区
byteBuffer.clear();
}
// 关闭通道
sendChannel.close();
  • 接收端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 获取接收端通道
DatagramChannel receiveChannel = DatagramChannel.open();
// 切换非阻塞模式
receiveChannel.configureBlocking(false);
// 绑定端口
receiveChannel.bind(new InetSocketAddress(9999));
// 获取选择器
Selector selector = Selector.open();
// 将通道注册到选择器中 并指定"读就绪"模式
receiveChannel.register(selector, SelectionKey.OP_READ);
// 轮询式的获取选择器上已经"准备就绪"的事件
while (selector.select() > 0) {
// 获取该选择器中注册的"选择键"(已就绪的监听事件)
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
// 遍历迭代器 判断当前是哪种状态
while (it.hasNext()) {
// 获取当前选择键
SelectionKey key = it.next();
// 如果是"读就绪"状态
if (key.isReadable()) {
// 定义指定大小缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 接收数据
receiveChannel.receive(byteBuffer);
// 切换读数据模式
byteBuffer.flip();
// 将接收到的数据进行打印
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
// 清空缓冲区
byteBuffer.clear();
}
}
// 取消选择键
it.remove();
}

管道Pipe

Java NIO管道是2个线程之间的单向数据连接

Pipe有一个source通道和一个sink通道,数据会被写到sink通道,从source通道读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 获取管道
Pipe pipe = Pipe.open();
// 定义指定大小缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 获取sink通道
SinkChannel sinkChannel = pipe.sink();
// 将数据存入缓冲区
byteBuffer.put("通过单向管道发送数据".getBytes());
// 切换读写模式
byteBuffer.flip();
// 将缓冲区中的数据写入管道
sinkChannel.write(byteBuffer);

// 上方写数据 下方读数据 以后可以分开两个线程分别执行

// 获取source通道
SourceChannel sourceChannel = pipe.source();
// 因为之前执行了write方法 需要再次切换读数据模式
byteBuffer.flip();
// 读取缓冲区中的数据
int len = sourceChannel.read(byteBuffer);
// 打印读取到的数据
System.out.println(new String(byteBuffer.array(), 0, len));
// 关闭通道
sourceChannel.close();
sinkChannel.close();