Java NIO 的前生今世 之四 NIO Selector 详解
1. BIO
的问题
BIO是阻塞IO,那么阻塞到底发生在哪里?
① ServserSocket.accept()
是阻塞的
② 所有输入流和输出流都是阻塞的,接收方等待发送方发送消息的时候是阻塞等待的,如果发送方一直不发送消息,那么接收方就要一直阻塞等待干不了其他事
③ 在BIO多人聊天室的案例中,由于BIO输出输出的阻塞,我们不得不为他们分配不同的线程
2. NIO
非阻塞式IO
--使用Channel代替Stream
--使用Selector监控多条Channel,由于Channel的读的方法可以是非阻塞的,所以我一直监控Channel的数据是否准备好
--可以在一个线程处理Channel I/O
3. Channle
与Buffer
对应BIO来说,完成程序与磁盘的读写需要在磁盘到程序建立一个管道,就像现实生活中的水管,这样源节点的字节数据就像水流一样直接流到程序
而NIO的Channel
替代BIO的流,我们可以通过Channel
读写,但是真正完成读写我们需要一个Buffer
,他对应的是内存中一块可以读写的区域,也就是说Channel
就是程序和磁盘中间的通道,我们可以把他理解为生活当作的铁路,只是用于连接,铁路自己本身不能完成运输,想完成运输要依赖于火车,也就是NIO中的缓冲区了,把数据装到缓冲区从铁路传输缓冲区到目的地,channel
是双向的,Buffer
可以双向移动(可读可写)
3.1 Buffer
Buffer
在Java NIO 中负责数据的存取,缓冲区低层就是数组,用于存储不同数据类型的数据,根据数据类型的不同(boolean除外),提供了相应类型的缓冲区ByteBuffer/ CharBuffer/ ShortBuffer/ IntBuffer/ LongBuffer/ FloatBuffer/ DoubleBuffer
,缓冲区的管理方式几乎一致,通过allocate()获取缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
① 缓冲区的四个核心属性
capacity
: 容量,表示缓冲区中最大存储数据的容量,一但声明不能改变。(因为底层是数组,数组一但被创建就不能被改变)
limit
: 界限,表示缓冲区中可以操作数据的大小。(limit后数据不能进行读写)
position
: 位置,表示缓冲区中正在操作数据的位置position <= limit <= capacity
mark
: 标记,表示记录当前position的位置,可以通过reset()恢复到mark的位置
② 使用方法
allocate()
:分配缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
put()
:将数据存入缓冲区
byte[] data = new byte[] {'H','E','L','L','O'};
byteBuffer.put(data);
由于Buffer是双向的,可以读可以写,所以他有读写两种模式
flip():
切换到读取数据的模式
byteBuffer.flip();
get()
:读取数据
byte[] data1 = new byte[3];
byteBuffer.get(data1);
读的最远位置就是limit的位置,保证了读到的数据就是刚刚写入的数据
clear()
:清空缓冲区,但是缓冲区中的数据依然存在,只是处于一种“被遗忘“的状态,所以的指针回到了最初的位置
compact()
:如果我上一次没有读完,读到一半就切换到了写模式,我想把没读的那一部分不清除,而是保存起来
compact
会把未读的数据拷贝到缓冲区的头部,position
指向移动后未读取的数据的后一个位置,然后再他后面进行写
rewind()
:重复读,使position归0
mark()
:标记。mark会记录当前的position之后使用reset
将此缓冲区的位置重置为先前标记的位置
reset()
:position恢复到mark记录的位置
hasRemaining(),
判断缓冲区是不是还要没读的,告诉当前位置和极限之间是否存在任何元素
remaining()
,还有几个没读的,返回当前位置和限制之间的元素数
3.2 Channel
Channel
用于源节点与目标节点的连接。在Java NIO中负责缓冲区中数据的传输。Channel
本身并不存储数据,因此需要配合Buffer
一起使用,每一个Channel
也可以向另一个Channel
进行数据交换
-
我们可以在同一个
Channel
中执行读和写操作, 然而同一个Stream
仅仅支持读或写. -
Channel
可以异步地读写, 而Stream
是阻塞的同步读写. -
Channel
总是从Buffer
中读取数据, 或将数据写入到Buffer
中
java.nio.channels.Channel接口:
用于本地数据传输:
|-- FileChannel
用于网络数据传输:
|-- SocketChannel //TCP 操作
|-- ServerSocketChannel //TCP 操作, 使用在服务器端
|-- DatagramChannel //UDP 操作
① FileChannel
打开Channel
Java 针对支持通道的类提供了一个 getChannel()
方法
FileChannel fin = null;//与源文件之间的通道
fin = new FileInputStream(source).getChannel();
文件大小
我们可以通过 channel.size()
获取关联到这个 Channel
中的文件的大小. 注意, 这里返回的是文件的大小, 而不是 Channel
中剩余的元素个数.
channel.size()
从 FileChannel 中读取数据
因为Buffer
是在管道运输,所以从FileChannel
读实际上是把读的数据写到缓冲区
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);//如果 read()返回 -1表示读完了
写入数据
写入实际上是从buffer
中读
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);
}
使用NIO完成文件的拷贝
public void nioBufferCopy(File source, File target){
//通道
FileChannel fin = null;//与源文件之间的通道
FileChannel fout = null;//与目标文件之间的通道
try {
fin = new FileInputStream(source).getChannel();
fout = new FileOutputStream(target).getChannel();
//一个Buffer又读又写
ByteBuffer buffer = ByteBuffer.allocate(1024);//最大容量为1024
//fin-->buffer-->fout
while (fin.read(buffer)!=-1){//写到buffer中
//buffer转换为读模式
buffer.flip();
while(buffer.hasRemaining()){//只要buffer里面有可以读的就一直读
fout.write(buffer);
}
//清空转换为写模式
buffer.clear();
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
fin.close();
fout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用NIO直接让两个Channel
交互完成文件的拷贝
public void nioTransferCopy(File source, File target){
//通道
FileChannel fin = null;//与源文件之间的通道
FileChannel fout = null;//与目标文件之间的通道
try {
fin = new FileInputStream(source).getChannel();
fout = new FileOutputStream(target).getChannel();
//直接让两个Channel交互完成文件的拷贝
long transfer = 0;
long size = fin.size();
while (transfer!=size) {
transfer += fin.transferTo(0, fin.size(), fout);//返回结果是这次一共传输了多少字节
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
fin.close();
fout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
对比所以的文件拷贝方法,包括以前的流的方法,使用缓冲区的效率要远远大于不使用缓冲区,NIO的性能实际上和传统IO差不多,因为在JDK1.4以后,传统IO的底层也进行了改进,所以性能和NIO差不多
② SocketChannel
SocketChannel
是一个客户端用来进行 TCP 连接的 Channel
.
创建一个 SocketChannel
的方法有两种:
-
打开一个
SocketChannel
, 然后将其连接到某个服务器中 -
当一个
ServerSocketChannel
接受到连接请求时, 会返回一个SocketChannel
对象.
打开 SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
读取数据
如果 read()返回 -1, 那么表示连接中断了.
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
写入数据
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);
}
非阻塞模式
我们可以设置 SocketChannel
为异步模式, 这样我们的 connect, read, write
都是异步的了
连接
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://example.com", 80));
while(! socketChannel.finishConnect() ){
//wait, or do something else...
}
在异步模式中, 或许连接还没有建立, connect
方法就返回了, 因此我们需要检查当前是否是连接到了主机, 因此通过一个 while
循环来判断.
读写
在异步模式下, 读写的方式是一样的.
在读取时, 因为是异步的, 因此我们必须检查 read 的返回值, 来判断当前是否读取到了数据
③ ServerSocketChannel
ServerSocketChannel 顾名思义, 是用在服务器为端的, 可以监听客户端的 TCP 连接, 例如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//do something with socketChannel...
}
非阻塞模式
在非阻塞模式下, accept()
是非阻塞的, 因此如果此时没有连接到来, 那么 accept()
方法会返回null:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
if(socketChannel != null){
//do something with socketChannel...
}
}
4. Channle
与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 key = channel.register(selector, SelectionKey.OP_READ);
注意, 如果一个 Channel
要注册到 Selector 中, 那么这个 Channel
必须是非阻塞的, 即channel.configureBlocking(false);
因为 Channel
必须要是非阻塞的, 因此 FileChannel
是不能够使用选择器的, 因为 FileChannel
都是阻塞的.
注意到, 在使用 Channel.register()
方法时, 第二个参数指定了我们对 Channel
的什么类型的事件感兴趣,也就是我们让Selector
监听Channel
的什么状态,这些事件有:
Connect, 即连接事件(TCP 连接), 对应于SelectionKey.OP_CONNECT
Accept, 即确认事件, 对应于SelectionKey.OP_ACCEPT
Read, 即读事件, 对应于SelectionKey.OP_READ, 表示 buffer 可读.
Write, 即写事件, 对应于SelectionKey.OP_WRITE, 表示 buffer 可写.
可以使用或运算|来组合多个事件
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey
如上所示, 当我们使用 register
注册一个 Channel
时, 会返回一个 SelectionKey
对象, 这个对象包含了如下内容:
interest set:即我们感兴趣的事件集, 即在调用 register 注册 channel 时所设置的 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:代表了 Channel 所准备好了的操作
int readySet = selectionKey.readyOps();
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
--------------------------------------------------------------------------------------
channel: 获取相对应的 Channel
Channel channel = selectionKey.channel();
--------------------------------------------------------------------------------------
selector:获取相对应的 Selector:
Selector selector = selectionKey.selector();
--------------------------------------------------------------------------------------
attached object, 可以在selectionKey中附加一个对象:
通过 Selector
选择 Channel
我们可以通过 Selector.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();
}
注意, 在每次迭代时, 我们都调用 "keyIterator.remove()"
将这个 key 从迭代器中删除, 因为 select()
方法仅仅是简单地将就绪的 IO 操作放到 selectedKeys
集合中, 因此如果我们从 selectedKeys
获取到一个 key, 但是没有将它删除, 那么下一次 select 时, 这个 key 所对应的 IO 事件还在 selectedKeys
中.
例如此时我们收到 OP_ACCEPT
通知, 然后我们进行相关处理, 但是并没有将这个 Key 从 SelectedKeys
中删除, 那么下一次 select()
返回时 我们还可以在 SelectedKeys
中获取到 OP_ACCEPT
的 key.
注意, 我们可以动态更改 SekectedKeys
中的 key
的 interest set
. 例如在 OP_ACCEPT
中, 我们可以将 interest set
更新为 OP_READ
, 这样 Selector
就会将这个 Channel
的 读 IO 就绪事件包含进来了
Selector 的基本使用流程
通过 Selector.open() 打开一个 Selector.
将 Channel 注册到 Selector 中, 并设置需要监听的事件(interest set)
不断重复以下步骤
① 调用 select()
方法
② 调用 selector.selectedKeys()
获取 selected keys
③ 迭代每个 selected key:
④ 从 selected key
中获取 对应的 Channel
和附加信息(如果有的话)
⑤ 判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT
事件, 则调用 "SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()"
获取 SocketChannel
, 并将它设置为 非阻塞的, 然后将这个 Channel
注册到 Selector
中.
⑥ 根据需要更改 selected key
的监听事件.
⑦ 将已经处理过的 key
从 selected keys
集合中删除
关闭 Selector
:当调用了 Selector.close()
方法时, 我们其实是关闭了 Selector
本身并且将所有的 SelectionKey
失效, 但是并不会关闭 Channel
public class NioEchoServer {
private static final int BUF_SIZE = 256;
private static final int TIMEOUT = 3000;
public static void main(String args[]) throws Exception {
// 打开服务端 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 后, 就要将它删除, 表示我们已经对这个 IO 事件进行了处理.
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(), 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(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(OP_READ);
}
buf.compact();
}
}
}
}
}