本文章主要讨论以下几个问题:
- 什么是NIO?NIO与IO的区别与联系?
- 为什么要使用NIO,它有哪些优点?
- NIO中的关键类,例如
Buffer
、Channel
、Selector
的介绍? - NIO的具体使用。
什么是NIO?NIO与普通IO的区别与联系?
- NIO指的是Non-Block IO,也即非阻塞的IO。传统的IO是堵塞的,也就是说当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。
- Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。
- 面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。
- 一个 面向缓冲区 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
- Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(
channel
)。
为什么要使用NIO,它有哪些优点?
- NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。
- 第二个使用NIO的原因是它可以实现异步的IO模式。通过向 Selector进行注册事件,如监听、读操作等,这样的话当一个事件到来时,Selector就会通知该Channel来处理该事件。
异步 I/O 是一种 没有阻塞地 读写数据的方法。通常,在代码进行 read() 调用时,代码会阻塞直至有可供读取的数据。同样, write() 调用将会阻塞直至数据能够写入。
另一方面,异步 I/O 调用不会阻塞。相反,您将注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接,等等,而在发生这样的事件时,系统将会告诉您。
异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。使用异步 I/O,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程。
NIO中的关键类,例如Buffer、Channel、Selector的介绍
NIO是面向块的,也就是说写操作和读操作都是需要经过缓冲区(Buffer)。例如从一个文件中读取数据,需要首先获取到文件的Channel
,然后将文件中的数据读入Buffer
,最后再对数据进行处理;向一个文件中进行写的操作,先把数据写入缓冲区,然后将缓冲区的数据write到文件中。
Buffer对象
Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer
对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream
对象中。
缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
Buffer的类型
最常用的就是ByteBuffer,他按照字节来处理数据。当然对于特定的数据类型也有不同的Buffer,但是其主要操作还是一样的。如CharBuffer
、ShortBuffer
、IntBuffer
、LongBuffer
、FloatBuffer
、DoubleBuffer。
Buffer中的三个属性position、limit以及capacity
- capacity。这个Buffer的容量,是一个不变的值。
- limit。这个值代表的是可读可写的范围。对于读或者写操作,范围就是[position, limit]。
- position。当前操作的位置。
三者之间的关系,具体可以看下面这段代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14 // 分配一个缓存区,此时position=0,limit = capacity = 100;
ByteBuffer buffer = ByteBuffer.allocate(100);
// 往buffer里面放了两个int,每个4字节
//因此,position = 8, limit = capacity = 100
buffer.putInt(1);
buffer.putInt(2);
//flip操作就是将buffer由写模式转为读模式
//limit 置位position = 8, 然后position = 0,capacity = 100
buffer.flip();
while(buffer.hasRemaining()){
buffer.getInt();
}
// clear操作,将postion置为0, limit = capacity = 100,这样就可以继续往这个buffer中写数据了
buffer.clear();
Channel对象
- Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是
InputStream
或者OutputStream
的子类), 而 通道 可以用于读、写或者同时用于读写。 - 一个Channel 总是 往一个Buffer里面读(read to) ,或者从一个buffer里面写(write from)。
Channel的种类
FileChannel 。从一个文件中进行读写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 创建一个FileChannel, 使用getChannel方法。
FileChannel inChannel = new FileInputStream("hello.txt").getChannel();
FileChannel outChannel = new FileOutputStream("world.txt").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true)
{
buffer.clear();
int count = inchannel.read(buffer);
if (count < 0)
break;
buffer.flip();
outChannel.write(buffer);
}DatagramChannel 。通过UDP协议进行读写的操作。
1
2
3
4
5
6
7
8
9DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));//在9999监听UDP连接
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
// 接收数据并存到buf中
channel.receive(buf);
// 发送数据
int bytesSent = channel.send(buf, new InetSocketAddress(host, 80));SocketChannel。通过TCP协议进行读写的操作。
1
2
3SocketChannel socketChannel = SocketChannel.open();
// 连接到服务器
socketChannel.connect(new InetSocketAddress(host, 80));ServerSocketChannel。允许你监听TCP连接,并且针对每个连接创建一个SocketChannel。
1
2
3
4
5
6
7
8
9ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 在9999建立监听
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//do something with socketChannel...
}
Selector
Selector 是一个Java NIO组件,它可以检查一个或多个NIO通道,并确定哪些通道已经准备好了,例如读或写。通过这种方式,单个线程可以管理多个通道,从而实现多个网络连接。、
为什么使用Selector
使用单个线程来处理多个通道的优点是,您需要较少的线程来处理通道。实际上,您可以使用一个线程来处理所有的通道。对于操作系统来说,在线程之间切换是很昂贵的,而且每个线程在操作系统中也会占用一些资源(内存)。因此,使用的线程越少越好。
创建一个Selector
1 | Selector selector = Selector.open(); |
向Selector注册Channel
为了使用选择器的通道,您必须使用选择器注册通道。这是通过使用SelectableChannel.register()
方法,如下:1
2
3channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
FileChannel
是不可以注册的,因为它不能切换为 non-block
模式。
NIO的具体使用
下面这个例子展示的是Selector的使用,首先开启Selector,然后channel向Selector注册,Selector负责监听。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//1. 开启一个Selector
Selector selector = Selector.open();
//2. 将channel设置为非阻塞模式
channel.configureBlocking(false);
//3. channel向Selector进行注册,它将处理读事件
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
//4. select() 方法将会阻塞,直到有准备好的channel
int readyChannels = selector.select();
if(readyChannels == 0) continue;
//5. 进行相应的处理
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
}