一、简介:
JDK从1.4开始引进的Java NIO。Java NIO 由以下几个核心部分组成:
●Channel
●Buffer
●Selector
①IO vs NIO
●IO 基于流(Stream oriented),而 NIO 基于 Buffer(Buffer oriented)。
●IO 操作是阻塞的,而 NIO 操作是非阻塞的。
●IO 没有 selector,而 NIO 有 selector。
②基于流 vs 基于 Buffer
传统的 I/O 是面向字节流或字符流的,而在 NIO 中,抛弃了传统的 I/O 流,而是引入了 Channel 和 Buffer 的概念。在 NIO 中,只能从 Channel 中读取数据到 Buffer 中或将数据从 Buffer 中写入到 Channel。
基于流:在一般的 Java I/O 操作中,以流式的方式顺序地从一个 Stream 中读取一个或多个字节,因此也就不能随意改变读取指针的位置。
基于 Buffer:首先需要从 Channel 中读取数据到 Buffer 中,当 Buffer 中有数据后,就可以对这些数据进行操作了。不像 I/O 那样是顺序操作,NIO 中可以随意地读取任意位置的数据。
③selector
selector 是 NIO 中才有的概念,它是 Java NIO 之所以可以非阻塞地进行 I/O 操作的关键。
通过 Selector,一个线程可以监听多个 Channel 的 I/O 事件,当向一个 Selector 中注册了 Channel 后,Selector 内部的机制就可以自动地不断地查询(select)这些注册的 Channel 是否有已就绪的 I/O 事件(例如:可读、可写、网络连接完成等)。通过这样的 Selector 机制,就可以很简单地使用一个线程高效地管理多个 Channel 了。
更多 I/O 模式相关内容可以参考:Linux I/O模式
二、Channel:
通常来说,所有的 NIO 的 I/O 操作都是从 Channel 开始的。一个 channel 类似于一个 stream。
Stream vs Channel:
●可以在同一个 Channel 中执行读和写操作,然而同一个 Stream 仅仅支持读或写。
●Channel 可以支持异步地读写,而 Stream 是阻塞的同步读写。
●Channel 总是从 Buffer 中读取数据,或将数据写入到 Buffer 中。
Channel 类型有以下几种:
●FileChannel(文件操作)
●DatagramChannel(UDP 操作)
●SocketChannel(TCP 操作)
●ServerSocketChannel(TCP 操作,使用在服务器端)
这些 Channel 涵盖了 UDP 和 TCP 网络 I/O 以及文件 I/O。
①FileChannel
FileChannel 是操作文件的 Channel,可以通过 FileChannel 从一个文件中读取数据,也可以将数据写入到文件中。
注意:FileChannel 不能设置为非阻塞模式。
1、打开 FileChannel
RandomAccessFile file = new RandomAccessFile("test.txt", "rw");
FileChannel channel = file.getChannel();
2、从 FileChannel 中读取数据
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = channel.read(buf);
3、写入数据
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
4、关闭
当对 FileChannel 的操作完成后,必须将其关闭。
channel.close();
5、设置 position
long pos = channel.position();
channel.position(pos + 123);
6、文件大小
可以通过 channel.size() 获取关联到这个 Channel 中的文件的大小。注意,这里返回的是文件的大小,而不是 Channel 中剩余的元素个数。
7、截断文件
channel.truncate(1024);
将文件的大小截断为1024字节。
8、强制写入
可以强制将缓存的未写入的数据写入到文件中。
channel.force(true);
②SocketChannel
SocketChannel 是一个客户端用来进行 TCP 连接的 Channel。
创建一个 SocketChannel 的方法有两种:
●打开一个 SocketChannel,然后将其连接到某个服务器中。
●当一个 ServerSocketChannel 接受到连接请求时,会返回一个 SocketChannel 对象。
1、打开 SocketChannel
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("http://test.com", 80));
2、关闭
channel.close();
3、读取数据
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = channel.read(buf);
如果 read()返回 -1,那么表示连接中断了。
4、写入数据
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
5、非阻塞模式
可以设置 SocketChannel 为异步模式,这样 connect、read、write 都是异步的了。
ⅰ、连接
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("http://test.com", 80));
while (!channel.finishConnect()) {
// wait, or do something else...
}
在异步模式中,可能连接还没有建立,connect 方法就返回了,因此需要检查当前是否是连接到了主机,因此通过一个 while 循环来判断。
ⅱ、读写
在异步模式下,读写的方式是一样的。在读取时,因为是异步的,因此必须检查 read 的返回值,以此来判断当前是否读取到了数据。
③ServerSocketChannel
ServerSocketChannel 是用在服务器为端的,可以监听客户端的 TCP 连接。
1、打开、关闭
ServerSocketChannel channel = ServerSocketChannel.open();
channel.close();
2、监听连接
可以使用 ServerSocketChannel.accept() 方法来监听客户端的 TCP 连接请求,accept() 方法会阻塞,直到有连接到来。当有连接时,这个方法会返回一个 SocketChannel 对象:
while (true) {
SocketChannel socketChannel = channel.accept();
// do something
}
3、非阻塞模式
在非阻塞模式下,accept() 是非阻塞的,因此如果此时没有连接到来,那么 accept() 方法会返回null:
ServerSocketChannel channel = ServerSocketChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
channel.configureBlocking(false);
while (true) {
SocketChannel socketChannel = channel.accept();
if (socketChannel != null) {
// do something
}
}
④DatagramChannel
DatagramChannel 是用来处理 UDP 连接的。
1、打开
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
2、读取数据
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
3、发送数据
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("test.com", 80));
4、连接到指定地址
因为 UDP 是非连接的,因此这个的 connect 并不是向 TCP 一样真正意义上的连接,而是它会将 DatagramChannel 锁住。因此仅仅可以从指定的地址中读取或写入数据。
channel.connect(new InetSocketAddress("test.com", 80));
三、Buffer:
当需要与 NIO Channel 进行交互时,就需要使用到 NIO Buffer,即数据从 Buffer 读取到 Channel 中,或者从 Channel 中写入到 Buffer 中。
实际上,一个 Buffer 其实就是一块内存区域。可以在这个内存区域中进行数据的读写。NIO Buffer 其实是这样的内存块的一个封装,并提供了一些操作方法方便进行数据的读写。
Buffer 的类型有:
●ByteBuffer
●CharBuffer
●DoubleBuffer
●FloatBuffer
●IntBuffer
●LongBuffer
●ShortBuffer
这些 Buffer 覆盖了能从 I/O 中传输的所有的 Java 基本数据类型。
①Buffer 的使用流程
使用 Buffer 的步骤如下:
●将数据写入到 Buffer 中。
●调用 Buffer.flip() 方法, 将 Buffer 转换为读模式。
●从 Buffer 中读取数据。
●调用 Buffer.clear() 或 Buffer.compact() 方法, 将 Buffer 转换为写模式。
简单看一个例子:
IntBuffer intBuffer = IntBuffer.allocate(2);
intBuffer.put(123);
intBuffer.put(456);
intBuffer.flip();
System.err.println(intBuffer.get());
System.err.println(intBuffer.get());
代码中,分配了两个单位大小的 IntBuffer,因此可以写入两个 int 值。
使用 put() 方法将 int 值写入,然后使用 flip() 方法将 Buffer 转换为读模式,然后连续使用 get() 方法从 Buffer 中获取这两个 int 值。
每当调用一次 get() 方法读取数据时,Buffer 的读指针都会向前移动一个单位长度(在这里是一个 int 长度)。
②Buffer 的属性
一个 Buffer 主要有三个属性:
●capacity
●position
●limit
其中 position 和 limit 的含义与 Buffer 所处的读写模式有关,而 capacity 的含义与 Buffer 所处的模式无关。
Capacity
每一个内存块都会有一个固定的大小,即容量(capacity),最多写入 capacity 个单位的数据到 Buffer 中。
例如一个 DoubleBuffer,如果其 capacity 是100,那么最多可以写入100个 double 数据。
Position
position 表示了读写操作的位置指针。
当向一个 Buffer 中写入数据时,是从 Buffer 的一个确定的位置(position)开始写入的。在初始状态时,position 的值是0。每当写入了一个单位的数据后,position 就会递增1。
当从 Buffer 中读取数据时,也是从某个特定的位置开始读取的。当调用了 filp() 方法将 Buffer 从写模式转换到读模式时,position 的值会自动被设置为0,每当读取一个单位的数据后,position 的值递增1。
Limit
limit - position 表示此时还可以写入/读取多少个单位的数据。
例如在写模式时,如果此时 limit 是10,position 是2,则表示已经写入了2个单位的数据,还可以写入 10 - 2 = 8 个单位的数据。
简单看一个例子:
IntBuffer intBuffer = IntBuffer.allocate(10);
intBuffer.put(123);
intBuffer.put(456);
System.err.println("Write mode now: ");
System.err.println("Capacity is: " + intBuffer.capacity());
System.err.println("Position is: " + intBuffer.position());
System.err.println("Limit is: " + intBuffer.limit());
intBuffer.flip();
System.err.println("Read mode now: ");
System.err.println("Capacity is: " + intBuffer.capacity());
System.err.println("Position is: " + intBuffer.position());
System.err.println("Limit is: " + intBuffer.limit());
代码设置容量为10,先写入两个值。
当写模式时,capacity = 10,position = 2,limit = 10。
当读模式时,capacity = 10,position = 0,limit = 2。
③Buffer 的分配
为了获取一个 Buffer 对象,首先需要分配内存空间。每个类型的 Buffer 都有一个 allocate() 方法,可以通过这个方法分配 Buffer,比如:
ByteBuffer buf = ByteBuffer.allocate(16);
这里分配了16 * sizeof(Byte)字节的内存空间。
再比如:
CharBuffer buf = CharBuffer.allocate(1024);
这里分配了大小为1024个字符的 Buffer,即这个 Buffer 可以存储1024个 Char,其大小为 1024 * 2 个字节。
④Direct Buffer vs Non-Direct Buffer
Direct Buffer:
●所分配的内存不在 JVM 堆上,不受 GC 的管理。(但是 Direct Buffer 的 Java 对象是由 GC 管理的,因此当发生 GC 对象被回收时,Direct Buffer 也会被释放)
●因为 Direct Buffer 不在 JVM 堆上分配,因此 Direct Buffer 对应用程序的内存占用的影响就不那么明显。(JVM 不方便统计到非 JVM 管理的内存)
●申请和释放 Direct Buffer 的开销比较大。因此正确的使用 Direct Buffer 的方式是在初始化时申请一个 Buffer,然后不断复用此 Buffer,在程序结束后才释放此 buffer。
●使用 Direct Buffer 时,当进行一些底层的系统 I/O 操作时,效率会比较高,因为此时 JVM 不需要拷贝 Buffer 中的内存到中间临时缓冲区中。
Non-Direct Buffer:
●直接在 JVM 堆上进行内存的分配,本质上是 byte[] 数组的封装。
●因为 Non-Direct Buffer 在 JVM 堆中,因此当进行操作系统底层 I/O 操作中时,会将此 Buffer 的内存复制到中间临时缓冲区中。因此 Non-Direct Buffer 的效率就较低。
⑤Buffer 的数据读写
写入数据到 Buffer,有两种方式:
●读取 Channel 写到 Buffer。
●通过 put() 方法写到 Buffer 里。
// 从Channel写到Buffer
int bytesRead = inChannel.read(buf);
// 通过put方法写Buffer
intBuffer.put(456);
从 Buffer 中读取数据,有两种方式:
●从 Buffer 读取数据写入到 Channel。
●使用 get() 方法从 Buffer 中读取数据。
// 从Buffer读取数据到Channel
int bytesWritten = inChannel.write(buf);
// 使用get()方法从Buffer中读取数据
int i = intBuffer.get();
⑥重置 position
Buffer.rewind() 方法可以重置 position 的值为0,可以用来重新读取/写入 Buffer。
如果是读模式,则重置的是读模式的 position;如果是写模式,则重置的是写模式的 position。
其实,rewind() 主要用于读模式。在读模式时,读取到 limit 后,调用 rewind() 方法,可以将读 position 置为0。(重新从头开始读)
⑦暂存/恢复 position
可以通过调用 Buffer.mark() 将当前的 position 的值保存起来,随后可以通过调用 Buffer.reset() 方法将 position 的值恢复回来。
mark 值也是 Buffer 的属性之一,默认为-1,与其他值的关系为:mark <= position <= limit <= capacity。
⑧flip() vs rewind() vs clear()
flip()
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
flip() 方法用于从写模式到读模式的切换。
Buffer 的读/写模式共用 position 和 limit 变量。当从写模式变为读模式时,原先的写 position 就变成了读模式的 limit。
rewind()
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
rewind() 方法仅仅重置 position 的值为0。
clear()
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
clear()方法 将 positin 设置为0,将 limit 设置为 capacity。
使用场景如下:
●在一个已经写满数据的 Buffer 中,调用 clear() 方法,可以从头读取 Buffer 的数据。(可能读到未存入的初始值数据)
●为了将一个 Buffer 填充满数据,可以调用 clear() 方法,然后一直写入,直到达到 limit。
⑨equals() vs compareTo()
可以通过 equals() 或 compareTo() 方法比较两个 Buffer。
当且仅当如下条件满足时,两个 Buffer 是相等的:
●两个 Buffer 是相同类型的。
●两个 Buffer 的剩余数据个数是相同的。
●两个 Buffer 的剩余数据都是相同的。
通过上述条件可以发现,判断两个 Buffer 相等时,并不是 Buffer 中的每个元素都进行比较,而是比较 Buffer 中剩余的元素。
当满足如下条件时,认为一个 Buffer 小于另一个 Buffer:
●第一个不相等的元素小于另一个 Buffer 中对应的元素。
●所有元素都相等,但第一个 Buffer 比另一个先耗尽(第一个 Buffer 的元素个数比另一个少)。
四、Selector:
Selector 允许一个单一的线程来操作多个 Channel。如果应用程序中使用了多个 Channel,那么使用 Selector 可以很方便的操作它们,但是因为在一个线程中使用了多个 Channel,因此也会造成了每个 Channel 传输效率的降低。
要想使用 Selector,首先需要将 Channel 注册到 Selector 中,随后调用 Selector 的 select() 方法。这个方法会阻塞,直到注册在 Selector 中的 Channel 发送可读写事件。当这个方法返回后,当前的这个线程就可以处理 Channel 的事件了。
①创建选择器
可以通过 Selector.open() 方法创建一个选择器:
Selector selector = Selector.open();
②将 Channel 注册到选择器中
为了使用选择器管理 Channel,需要将 Channel 注册到选择器中:
channel.configureBlocking(false);
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_READ);
注意:如果一个 Channel 要注册到 Selector 中,那么这个 Channel 必须是非阻塞的(即channel.configureBlocking(false))。因此 FileChannel 是不能够使用选择器的,因为 FileChannel 都是阻塞的。
在使用 Channel.register() 方法时,第二个参数指定了后续对 Channel 的什么类型的事件感兴趣,这些事件有:
●Connect:即连接事件(TCP 连接),对应于SelectionKey.OP_CONNECT(1 << 3)。
●Accept:即确认事件,对应于SelectionKey.OP_ACCEPT(1 << 4)。
●Read:即读事件,对应于SelectionKey.OP_READ(1 << 0),表示 Buffer 可读。
●Write:即写事件,对应于SelectionKey.OP_WRITE(1 << 2),表示 Buffer 可写。
一个 Channel 发出一个事件也可以称为对于某个事件 Channel 准备好了。因此一个 Channel 成功连接到了另一个服务器也可以被称为 connect ready。
可以使用或运算|来组合多个事件,例如:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
注意:一个 Channel 仅仅可以被注册到一个 Selector 一次。如果将 Channel 注册到 Selector 多次,那么其实就是相当于更新 SelectionKey 的 interest set。例如:
channel.register(selector, SelectionKey.OP_READ);
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
上面的 channel 注册到同一个 Selector 两次了,那么第二次的注册其实就是相当于更新这个 Channel 的 interest set 为 SelectionKey.OP_READ | SelectionKey.OP_WRITE。
③SelectionKey
当使用 register 注册一个 Channel 时,会返回一个 SelectionKey 对象,这个对象包含了如下内容:
●interest set:即后续感兴趣的事件集,即在调用 register 注册 channel 时所设置的 interest set。
●ready set:代表了 Channel 所准备好了的操作。
●channel。
●selector。
●attached object:可选的附加对象。
interest set
可以通过如下方式获取 interest set:
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
ready set
可以像判断 interest set 一样操作 ready set,但是还可以使用如下方法进行判断:
int readySet = selectionKey.readyOps();
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel 和 Selector
可以通过 SelectionKey 获取相对应的 Channel 和 Selector:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
Attaching Object
可以在selectionKey中附加一个对象:
selectionKey.attach(someObject);
Object attachedObj = selectionKey.attachment();
或者在注册时直接附加:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, someObject);
④通过 Selector 选择 Channel
可以通过 Selector.select() 方法获取对某件事件准备好了的 Channel。即如果在注册 Channel 时,对其的可写事件感兴趣,那么当 select() 返回时,就可以获取 Channel 了。此时 select() 方法返回的值表示有多少个 Channel 可操作。
⑤获取可操作的 Channel
如果 select() 方法返回值表示有多个 Channel 准备好了,此时可以通过 Selected key set 访问这个 Channel:
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();
}
注意:在遍历时,每次都应该调用 remove() 方法将这个 key 从迭代器中删除。因为 select() 方法仅仅是简单地将就绪的 I/O 操作放到 selectedKeys 集合中,因此如果从 selectedKeys 获取到一个 key,但是没有将它删除,那么下一次 select 时,这个 key 所对应的 I/O 事件还在 selectedKeys 中。
注意:可以动态更改 sekectedKeys 中的 key 的 interest set。例如在 OP_ACCEPT 中,可以将 interest set 更新为 OP_READ,这样 Selector 就会将这个 Channel 的 读 I/O 就绪事件包含进来了。
⑥Selector 的基本使用流程
1、通过 Selector.open() 打开一个 Selector。
2、将 Channel 注册到 Selector 中,并设置需要监听的事件(interest set)。
3、循环执行以下步骤:
○调用 select() 方法。
○调用 selector.selectedKeys() 获取 selected keys。
○遍历每个 selectedKey:
◗从 selectedKey 中获取对应的 Channel 和附加信息(如果有的话)。
◗判断哪些 I/O 事件已经就绪了,然后处理它们。如果是 OP_ACCEPT 事件,则调用 SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept() 获取 SocketChannel,并将它设置为非阻塞的,然后将这个 Channel 注册到 Selector 中。
◗根据需要更改 selectedKey 的监听事件。
◗将已经处理过的 key 从 selectedKeys 集合中删除。
⑦关闭 Selector
当调用 Selector.close()方法时,其实是关闭了 Selector 本身并且将所有的 SelectionKey 失效,但是并不会关闭 Channel。
⑧整体使用示例
private static final int BUF_SIZE = 256;
private static final int TIMEOUT = 3000;
// 打开服务端Socket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 打开Selector
Selector selector = Selector.open();
// 服务端Socket监听8080端口,并配置为非阻塞模式
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
// 将channel注册到selector中
// 通常是先注册一个OP_ACCEPT事件,然后在OP_ACCEPT到来时,再将这个channel的OP_READ注册到Selector 中.
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 通过调用select方法,阻塞地等待channel I/O可操作
if (selector.select(TIMEOUT) == 0) {
System.out.print(".");
continue;
}
// 获取I/O操作就绪的SelectionKey,通过SelectionKey可以知道哪些Channel的哪类I/O操作已经就绪
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 当获取一个SelectionKey后,就要将它删除,表示已经对这个I/O事件进行了处理
keyIterator.remove();
if (key.isAcceptable()) {
// 当OP_ACCEPT事件到来时,就从ServerSocketChannel中获取一个SocketChannel,代表客户端的连接
// 注意:在OP_ACCEPT事件中,从key.channel()返回的Channel是ServerSocketChannel
// 而在OP_WRITE和OP_READ中,从key.channel()返回的是SocketChannel
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
// 在OP_ACCEPT到来时,再将这个Channel的OP_READ注册到Selector中
// 注意:这里如果没有设置OP_READ的话(即interest set仍然是OP_CONNECT的话),那么select方法会一直直接返回
clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(BUF_SIZE));
}
if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long bytesRead = clientChannel.read(buf);
if (bytesRead == -1) {
clientChannel.close();
} else if (bytesRead > 0) {
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
System.out.println("Get data length: " + bytesRead);
}
}
if (key.isValid() && key.isWritable()) {
ByteBuffer buf = (ByteBuffer) key.attachment();
buf.flip();
SocketChannel clientChannel = (SocketChannel) key.channel();
clientChannel.write(buf);
if (!buf.hasRemaining()) {
key.interestOps(SelectionKey.OP_READ);
}
buf.compact();
}
}
}