回顾传统
有必要了解传统的I/O操作的方式。以网络应用为例,传统方式需要监听一个ServerSocket,接受请求的连接为其提供服务(服务通常包括了处理请求并发送响应),下图是服务器的生命周期图,其中标有粗黑线条的部分表明会发生I/O阻塞。
?
?
ServerSocket server=new ServerSocket(10000); //接受新的连接请求 Socket newConnection=server.accept(); //对于accept方法的调用将造成阻塞,直到ServerSocket接受到一个连接请求为止。一旦连接请求被接受,服务器可以读客户socket中的请求。 InputStream in = newConnection.getInputStream(); InputStreamReader reader = new InputStreamReader(in); BufferedReader buffer = new BufferedReader(reader); Request request = new Request(); while(!request.isComplete()) { String line = buffer.readLine(); request.addLine(line); } //这样的操作有两个问题,首先BufferedReader类的readLine()方法在其缓冲区未满时会造成线程阻塞,只有一定数据填满了缓冲区或者客户关闭了套接字,方法才会返回。其次,它回产生大量的垃圾,BufferedReader创建了缓冲区来从客户套接字读入数据,但是同样创建了一些字符串存储这些数据。虽然BufferedReader内部提供了StringBuffer处理这一问题,但是所有的String很快变成了垃圾需要回收。 //同样的问题在发送响应代码中也存在 Response response = request.generateResponse(); OutputStream out = newConnection.getOutputStream(); InputStream in = response.getInputStream(); int ch; while(-1 != (ch = in.read())) { out.write(ch); } newConnection.close(); //类似的,读写操作被阻塞而且向流中一次写入一个字符会造成效率低下,所以应该使用缓冲区,但是一旦使用缓冲,流又会产生更多的垃圾。
??
?? 传统的解决方法
? 通常在Java中处理阻塞I/O要用到线程(大量的线程)。一般是实现一个线程池用来处理请求,如下图:
?
??
? 线程使得服务器可以处理多个连接,但是它们也同样引发了许多问题。每个线程拥有自己的栈空间并且占用一些CPU时间,耗费很大,而且很多时间是浪费在阻塞的I/O操作上,没有有效的利用CPU。
? 选择NIO
? NIO包(java.nio.*)引入了四个关键的抽象数据类型,它们共同解决传统的I/O类中的一些问题。
1. Buffer:它是包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。
2. Charset:它提供Unicode字符串影射到字节序列以及逆影射的操作。
3. Channels:包含socket,file和pipe三种管道,它实际上是双向交流的通道,你可能注意到现有的java.io类中没有一个能够读写Buffer类型,所以NIO中提供了Channel类来读写Buffer。channel就是一个读写的管道,通过管道的读写来完成IO操作。channel分为ServerSocketChannel和SocketChannel,前者用于监听,获得客户端的连接,后者直接用于操作IO,来看看Channel如何进行Socket操作
4. Selector:它将多元异步I/O操作集中到一个或多个线程中,在过去的阻塞I/O中,我们一般知道什么时候可以向stream中读或写,因为方法调用直到stream准备好时返回。但是使用非阻塞通道,我们需要一些方法来知道什么时候通道准备好了。在NIO包中,设计Selector就是为了这个目的。SelectableChannel可以注册特定的事件,而不是在事件发生时通知应用,通道跟踪事件。然后,当应用调用Selector上的任意一个selection方法时,它查看注册了的通道看是否有任何感兴趣的事件发生
?
String host = 127.0.0.1; InetSocketAddress socketAddress = new InetSocketAddress(host, 80); //默认情况下,所有channel(包括ServerSocketChannel, SocketChannel)的工作模式是阻塞 ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.connect(socketAddress); //阻塞监听客户端,获得客户端的连接channel SocketChannel sc = ssc.accept(); Charset charset = Charset.forName("ISO-8859-1"); CharsetEncoder encoder = charset.newEncoder(); String request = "GET / \r\n\r\n"; //阻塞写 sc.write(encoder.encode(CharBuffer.wrap(request))); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); //阻塞读 while(sc.read(buffer) != -1){ //do something }
??
NIO通过channel来操作IO,概念上更加清晰。但发现:它仍然是阻塞的操作模式,本质上于传统Socket IO来比,工作模式并没有本质上的变化呀,还不是要分配线程。的确,如果NIO只带来了这些,那么NIO也没有什么优势。但是NIO带来的不只是这些,这些channel可以配置成no-blocking模式,借助于selector,NIO带来了一种新的socket编写模式:
?
String host = 127.0.0.1; InetSocketAddress socketAddress = new InetSocketAddress(host, 80); ServerSocketChannel ssc = ServerSocketChannel.open(); //配置channel的阻塞模式 ssc.configureBlocking(false); ssc.connect(socketAddress); Selector selector = Selector.open(); //将ServerSocketChannel注册到selector上,selector可以检测多路channel ssc.register(selctor, SelectionKey.OP_ACCEPT); while(true){ //阻塞等待事件响应 selector.select(); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> it = selectionKeys.iterator(); //获得多路的channel,这些channel此时都已准备就绪,工作在非阻塞模式,可以非阻塞读写 while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); if (key.isAcceptable()) { ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); //此时的accept是非阻塞的,立马返回 SocketChannel channel = serverSocketChannel.accept(); channel.configureBlocking(false); //可以不断得将这些channel注册成不同的类型,使之即可读,又可写 channel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel channel = (SocketChannel) key.channel(); //非阻塞读,立马返回数据 channel.read(buffer); //... SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_WRITE); selectionKey.attach(new HandleClient(clientName)); } else if (key.isWritable()) { SocketChannel channel = (SocketChannel) key.channel(); HandleClient handleClient = (HandleClient) key.attachment(); ByteBuffer buffer = handleClient.readBlock(); } } }
?
在这种模式下,原来负责端口监听的accept()方法换成了select()方法,两者都是阻塞的,本质上没有分别.区别在于select()之后返回的所有channel都是非阻塞的,都是可以马上读写的。而accept()之后的channel则是阻塞的,不能保证此时返回的channel的读写能够马上返回。因此,NIO的非阻塞方式就可以设置比较少的线程,因为这些线程拿到的channel都是立马可以读写的,这些线程的工作都是满负荷的,效率高。反之,阻塞方式需要创建同样较多的线程,因为这些线程很多都处于阻塞休眠状态,大家都不是满负荷在工作。这样NIO的优点就很明显了。