不管是磁盘IO还是网络IO,数据在写入OutputStream或者从InputStream读取时都有可能会阻塞,线程将会失去CPU的使用权,这在当前大规模访问量和有性能要求的情况下是不能被接受的。虽然当前的网络IO有一些解决办法。如一个客户端一个出来线程,出现阻塞时只是一个线程阻塞而不会影响其他线程工作,为了减少系统线程开销,采用线程池的办法来减少线程创建和回收成本。
但是,当需要大量HTTP长连接时,一些情况下服务端需要同时保持几百万的HTTP连接,但并不是每时每刻这些连接都在传输数据,同时创建这么多线程来保持连接是不可能的。另外一种情况是,每个客户端在请求服务端时可能会访问一些竞争资源,因为这些客户端在不同线程当中,必须通过同步才能实现不同线程对同一资源的同时访问。同步操作的复杂性会降低应用程序的稳定性。
NIO的出现很好的解决了这一个问题,NIO引入了Channel、Buffer和Selector这三个关键类,通过这些类把原本通过Socket传递的信息具体化,Channel就像是高铁,Selector像车站,Buffer是高铁上的座位,旅客是数据,他们各自有明确的分工,而Socket就是一个抽象的管道,当数据少的时候,管道因为其简洁可能比高铁快,但是当大量数据需要传输时,管道就会阻塞,高铁因为其具体化分工协作就会发挥优势。
理解这些概念后,下面就是一段实现NIO的代码:
import java.io.IOException;
import java.net.InetSocketAddress;
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.Iterator;
import java.util.Set;
public class NIOSample {
public void selector() throws IOException{
ByteBuffer buffer=ByteBuffer.allocate(1024);
Selector selector=Selector.open();
ServerSocketChannel ssc=ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true)
{
Set selectedKeys=selector.selectedKeys();
Iterator it=selectedKeys.iterator();
while(it.hasNext())
{
SelectionKey key=(SelectionKey)it.next();
if((key.readyOps()&SelectionKey.OP_ACCEPT)==SelectionKey.OP_ACCEPT)
{
ServerSocketChannel ssChannel=(ServerSocketChannel)key.channel();
SocketChannel sc=ssChannel.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_ACCEPT);
it.remove();
}else if((key.readyOps()&SelectionKey.OP_READ)==SelectionKey.OP_READ)
{
SocketChannel sc=(SocketChannel)key.channel();
while(true)
{
buffer.clear();
int n=sc.read(buffer);
if(n<0)break;
buffer.flip();
}
it.remove();
}
}
}
}
}
代码分析:调用Selector的静态工厂创建一个选择器,然后创建一个服务端的Channel,绑定到一个Socket对象,并把这个通信信道注册到选择器上,把这个通信信道设置为非阻塞模式。接着调用Selector的selectedKeys方法来检查已经注册在这个选择器上的所有通信信道是否有需要的事件发生,如果有某个事件发生,将返回所有的SelectorKeys,通过这个对象的channel方法就能取得这个通信信道的对象,从而可以读取通信的数据,而这里读取的数据是Buffer,这个Buffer是我们可以控制的缓冲器。
上面这段程序中,我将Server端的监听连接请求的事件和处理请求的事件放在同一个线程中,但是在实际应用中通常会把它们放在两个线程中:一个线程专门负责监听客户端的连接请求,而且以阻塞方式进行;另一个线程负责处理请求,这个专门的处理请求才会真正采用NIO形式,相关应用实例可看Tomcat和Jetty。