非阻塞I/O
Java NIO的一个非常重要的特点就是,引入了非阻塞的I/O。其中Selector类是非阻塞I/O的核心类。Selector是一个能够检查多个Channel,并发现哪些Channel已经准备好了数据传送的NIO组件。这样可以在一个线程去管理多个Channel,例如多个网络连接。Selector与 Windows 消息循环类似,它从不同客户机捕获各种事件并将它们分派到相应的事件处理程序。
一个常见的网络 IO 通讯流程如下 :
图 2 网络 IO 通讯流程
在通讯过程中若连接还没到来,那么accept会阻塞,程序运行到这里不得不挂起,CPU 转而执行其他线程。同样,read 会一样也会阻塞。
阻塞式网络 IO 的特点:多线程处理多个连接,例如10000个连接就需要10000个线程(消耗资源),并且阻塞的结果就是会带来大量的线程频繁地进行上下文切换(消耗时间)。
非阻塞式IO的出现的目的就是为了解决这个瓶颈。而非阻塞式IO是怎么实现的呢?非阻塞IO处理连接的线程数和连接数没有联系,也就是说处理10000个连接非阻塞IO不需要10000个线程,你可以用1000个也可以用2000个线程来处理。因为非阻塞IO处理连接是异步的。当某个连接发送请求到服务器,服务器把这个连接请求当作一个请求“事件”,并把这个"事件"分配给相应的函数处理。我们可以把这个处理函数放到线程中去执行,执行完就把线程归还。这样一个线程就可以异步的处理多个事件。而阻塞式IO的线程的大部分时间都浪费在等待请求上了。
图 3 Java NIO: A Thread uses a Selector to handle 3 Channel's
下面是一个非阻塞式网络通信的例子,分为服务器端和客户端程序。
NonBlockingServer.java:
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Set;
public class NonBlockingServer {
public static void main(String[] args) throws Exception {
// Define a set of ports.
int ports[] = { 8000, 8001, 8002, 8005 };
// Creating a Selector.
Selector selector = Selector.open();
// Open some ServerSocketChannels and register in the selector.
for (int i : ports) {
ServerSocketChannel initServerSocketChannel = ServerSocketChannel
.open();
// The Channel must be in non-blocking mode to be used with a
// Selector.
initServerSocketChannel.configureBlocking(false);
ServerSocket initServerSocket = initServerSocketChannel.socket();
initServerSocket.bind(new InetSocketAddress("127.0.0.1", i));
// Registering Channels with the Selector.
initServerSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server is listenning on port " + i);
}
// Once you have register one or more channels with a Selector you can
// call one of the select()
// methods. These methods return the channels that are "ready" for the
// events you are interested in
// (connect, accept, read or write). In other words, if you are
// interested in channels that are ready for
// reading, you will receive the channels that are ready for reading
// from the select() methods.
// Here are the select() methods:
// int select()
// int select(long timeout)
// int selectNow()
// select() blocks until at least one channel is ready for the events
// you registered for.
// select(long timeout) does the same as select() except it blocks for a
// maximum of timeout milliseconds (the parameter).
// selectNow() doesn't block at all. It returns immediately with
// whatever channels are ready.
// The int returned by the select() methods tells how many channels are
// ready. That is, how many channels that became ready since last time
// you called select(). If you call select() and it returns 1 because
// one channel has become ready, and you call select() one more time,
// and one more channel has become ready, it will return 1 again. If you
// have done nothing with the first channel that was ready, you now have
// 2 ready channels, but only one channel had become ready between each
// select() call.
while (selector.select() > 0) {
Set<SelectionKey> selectedKeys = selector.selectedKeys();
for (SelectionKey key : selectedKeys) {
if (key.isAcceptable()) {
// Accessing the channel from the SelectionKey.
ServerSocketChannel server = (ServerSocketChannel) key
.channel();
SocketChannel client = server.accept();
System.out.println("Accepted");
client.configureBlocking(false);
ByteBuffer outBuf = ByteBuffer.allocate(1024);
outBuf.put(("Current Time is " + new Date()).getBytes());
outBuf.flip();
client.write(outBuf);
client.close();
}
}
selectedKeys.clear();
}
}
}
Client.java:
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Set;
public class Client {
public static void main(String[] args) throws Exception {
Selector selector = Selector.open();
SocketChannel initClientSocketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8001));
// You must set the Channel to non-blocking.
initClientSocketChannel.configureBlocking(false);
initClientSocketChannel.register(selector, SelectionKey.OP_READ);
while(selector.select()>0){
Set<SelectionKey> selectedKeys = selector.selectedKeys();
for(SelectionKey key : selectedKeys) {
if(key.isReadable()) {
SocketChannel clientSocketChannel = (SocketChannel) key.channel();
ByteBuffer inBuf = ByteBuffer.allocate(1024);
clientSocketChannel.read(inBuf);
inBuf.flip();
Charset charset = Charset.forName( "utf-8" );
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(inBuf);
System.out.println(charBuffer);
}
}
}
}
}
事实上,客观地说,NIO的性能跟传统线程池性能的比较孰优孰劣有待考证,笔者能力有限,暂时没有写出可以供测试的模型出来,如果读者有兴趣可以参考下图写一个服务器程序出来。
图 4 NIO服务器模型
参考资料:
http://g.oswego.edu/dl/cpjslides/nio.pdf