对于上一节中的socket通讯,用以下图来解释NIO的通讯过程:
从图中可以明显的看出NIO主要体现在 Channel,Buffer,Selector 这几个类上
一. Selector(选择器)
针对SelectableChannel
对象的多路复用器。即只针对网络通讯,对文件IO不支持。
可通过调用此类的 open
方法创建选择器,在选择器调用 close
方法关闭选择器之前,它一直保持打开状态。
通过 SelectionKey
对象来表示可选择通道到选择器的注册。
//打开一个选择器。
Selector selector=Selector.open();
//该方法是阻塞的,选择一组键,其相应的通道已为 I/O 操作准备就绪。
selector.select();
//该方法是阻塞的,最多等1s,如果还没有就绪的就返回0。
selector.select(1000);
//该方法是非阻塞的,如果没有准备就绪的连接,直接返回。
selector.selectNow();
//返回此选择器的所有键集。
selector.keys();
//返回已此通道已准备就绪的键集,已选择始终是键集的一个子集。
selector.selectedKeys();
二.SelectionKey(选择键)
表示 SelectableChannel
在 Selector
中的注册的标记。
每次向选择器注册通道时就会创建一个选择键。
通过调用某个键的 cancel
方法、关闭其通道,或者通过关闭其选择器来取消 该键之前,它一直保持有效。
取消某个键不会立即从其选择器中移除它;相反,会将该键添加到选择器的已取消键集,以便在下一次进行选择操作时移除它。可通过调用某个键的isValid
方法来测试其有效性。
选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
interest 集合(关注集合)确定了下一次调用某个选择器的选择方法时,将测试哪类操作的准备就绪信息。创建该键时使用给定的值初始化(channel调用regist方法时) interest 集合;之后可通过interestOps(int)
方法对其进行更改。
ready 集合 即某个键的选择器检测到该键的通道已为此类操作准备就绪。创建该键时 ready 集合被初始化为零;可以在之后的选择操作中通过选择器对其进行更新,但不能直接更新它。可以通过调用键的readyOps( )方法来获取相关的通道的已经就绪的操作。ready集合是interest集合的子集,并且表示了interest集合中从上次调用select( )以来已经就绪的那些操作。
选择键可通过 attach
方法附加对象,然后通过 attachment
方法获取该对象。
选择过程:
选择操作是当三种形式的select( )中的任意一种被调用时,由选择器执行的。不管是哪一种形式的调用,下面步骤将被执行:
1.已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销。这个步骤结束后,已取消的键的集合将是空的。
2.已注册的键的集合中的键的interest集合将被检查。在这个步骤中的检查执行过后,对interest集合的改动不会影响剩余的检查过程。确定每个通道所关心的操作的真实就绪状态,依赖于特定的select( )方法调用,如果没有通道已经准备好,线程可能会在这时阻塞,通常会有一个超时值。
3.步骤2可能会花费很长时间,特别是所激发的线程处于休眠状态时。与该选择器相关的键可能会同时被取消。当步骤2结束时,步骤1将重新执行,以完成任意一个在选择进行的过程中,键已经被取消的通道的注销。
4.select操作返回的值是ready集合在步骤2中被修改的键的数量,而不是已选择的键的集合中的通道的总数。返回值不是已准备好的通道的总数,而是从上一个select( )调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值可能是0。
以下是一个简单的服务端:
public class ServerDemo {
private static ByteBuffer buffer=ByteBuffer.allocate(1024);//1K大小的缓冲池
public static void main(String[] args) {
try {
Selector selector=Selector.open();
ServerSocketChannel serverChannel=ServerSocketChannel.open();
ServerSocket server=serverChannel.socket();
server.bind(new InetSocketAddress(12345));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){
// This may block for a long time. Upon returning, the
// selected set contains keys of the ready channels.
int n=selector.select();
if (n == 0) { // nothing to do
continue;
}
Iterator iterator=selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key=iterator.next();
// Is a new connection coming in?
if(key.isAcceptable()){
//此时的key是,ServerSocketChannel注册的。要获取客户端通道
ServerSocketChannel serverSocketChannel=(ServerSocketChannel)key.channel();
SocketChannel client=serverSocketChannel.accept();
client.configureBlocking(false);//将客户端通道也设置成非阻塞
client.register(selector, SelectionKey.OP_READ);
//想客户端发送消息
buffer.clear();
buffer.put("Hi there!\r\n".getBytes());
buffer.flip();
client.write(buffer);
}
if(key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
int count;
buffer.clear(); // Empty buffer
// Loop while data is available; channel is nonblocking
while ((count = socketChannel.read(buffer)) > 0) {
buffer.flip();
// Make buffer readable
// Send the data; don't assume it goes all at once
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
// WARNING: the above loop is evil. Because
// it's writing back to the same nonblocking
// channel it read the data from, this code can
// potentially spin in a busy loop. In real life
// you'd do something more useful than this.
buffer.clear();
// Empty buffer
}
if (count < 0) {
// Close channel on EOF, invalidates the key
socketChannel.close();
}
}
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}