阻塞IO和非阻塞IO:
阻塞IO:在代码进行 read() 调用时,线程会阻塞直至有可供读取的数据。同样, write()调用时,线程也会阻塞直至数据全部写入。换句话说,当你发了一个请求(或方法调用)之后,必须等待,直到程序返回结果,这段期间不能干其他事情。Everything is in sequence,每件事情都是有先后顺序的。
非阻塞IO就是当你发了一个请求之后,不用去管结果何时返回,而是去干其他的事情,当程序返回结果时,会通知你,例如你用全自动洗衣机洗衣服,你不用在旁边等着洗衣机洗完,而是可以去做饭啊上网什么的,当洗衣机洗完衣服之后,会发出“滴滴滴”的通知声音,当你得知这个消息后,就可以去处理它的结果了(例如晾衣服等)。
假设某时刻有大量的请求同时到达服务端,如果对每一个连接都创建一个线程去处理该请求,那么连接数越多,创建的线程数就越多,但是一台服务器能够产生的线程数是有限的,这样就限制了它的并发处理能力。In addition,如果来一个请求就创建一个线程,处理完之后又销毁掉,那么频繁的创建、销毁线程会带来巨大的系统开销,所以必须引入线程池,维持一个核心线程数和最大线程数。但是,服务端的并发处理能力依然有限,因为处理能力还是与最大线程数相关。如何解决呢?使用NIO。通过一个selector(只需要开启一个线程),每一个客户端连接为一个channel,这些channel都注册到selector上,selector进行事件的监听、选择性处理。
NIO:non-blocking IO,JDK中自带得有NIO的包(java.nio);但是通常基于java nio开发的程序有各种问题,至少要半年甚至一年才会慢慢解决掉,所以只需要了解其中的核心概念(ByteBuffer,Channel,Selector)。通道是对 I/O 包中的流的模拟,到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。Buffer 实质上是一个容器对象,发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。
缓冲区实质上是一个数组,最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 是在其底层字节数组上进行 get/set 操作。通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。
从文件中读取(从外面的文件读取进入内存)
如果使用原来的 I/O,那么我们只需创建一个 FileInputStream 并从它那里读取。而在 NIO 中,情况有所不同:
第一步是获取通道。我们从 FileInputStream 获取通道:
FileInputStream fin = new FileInputStream( "readandshow.txt" ); FileChannel fc = fin.getChannel();
下一步是创建缓冲区:
ByteBuffer buffer = ByteBuffer.allocate( 1024 ); //缓冲区分配
最后,需要将数据从通道读到缓冲区中
fc.read( buffer );
写入文件(从内存写出到外面的文件)
在 NIO 中写入文件,首先从 FileOutputStream 获取一个通道:
FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" ); FileChannel fc = fout.getChannel();
下一步是创建一个缓冲区并在其中放入一些数据
ByteBuffer buffer = ByteBuffer.allocate( 1024 ); for (int i=0; i<message.length; ++i) { buffer.put( message[i] ); } buffer.flip();
最后一步是写入到文件中:
fc.write( buffer );
现在重点研究一下Buffer中的结构(状态变量:position、limit 、capacity,可以理解为三个指针),通过这三个状态变量,Buffer就可以自动管理里面的数据了。capacity始终为字节数组的长度,当从外界读入数据到Buffer中时,limit指向position的位置,positon初始位置为0,每读入一个字节,position++,但是不会超过capacity的位置。
如果要将Buffer中的数据读出到外界去,则需要调用buffer.flip()方法。flip方法会改变position和limit指针的位置,将positon指向0的位置,将limit指向原先的position的位置,这样一来,读取的时候就只会在[0,limit]范围内了,每读取一个字节,position++,直到到达limit,这样就将buffer中的数据全部读走了。
最后,调用buffer.clear()方法,该方法会重置这三个指针,使得其回到最原始的位置,准备新的读入工作。
如何直接操作Buffer中的数据呢?使用put()、get()方法,put往Buffer中放入字节,get从Buffer中读取字节。
Selector:Selector是一个选择器,每一个Channel都会在Selector上进行注册,并指定Selector对该channel感兴趣的事件(返回值为SelectedKey)。Selector上同时注册得有多个channel,当某个事件发生时,Selector就会通知对应的处理程序进行处理。
我们需要做的第一件事就是创建一个 Selector:
Selector selector = Selector.open();
然后,我们将对各个通道对象调用 register() 方法,以便注册我们对这些对象中发生 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector。为了接收连接,我们需要一个 ServerSocketChannel。对于每一个端口,我们打开一个 ServerSocketChannel,如下所示:
ServerSocketChannel ssc = ServerSocketChannel.open(); //创建一个新的 ServerSocketChannel ssc.configureBlocking( false ); //我们必须对每一个要使用的套接字通道调用这个方法,否则异步 I/O 就不能工作。 ServerSocket ss = ssc.socket(); InetSocketAddress address = new InetSocketAddress( ports[i] ); ss.bind( address ); //将它绑定到给定的端口
将新打开的 ServerSocketChannels 注册到 Selector上。为此我们使用 ServerSocketChannel.register() 方法,如下所示:
SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT ); //返回选择键
register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 ACCEPT 事件,也就是在新的连接建立时所发生的事件。请注意对 register() 的调用的返回值,SelectionKey 代表这个通道在此 Selector 上的注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。
接着进入主循环。使用 Selectors 的几乎每个程序都像下面这样使用内部循环:
int num = selector.select(); //该方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量 Set selectedKeys = selector.selectedKeys(); //返回发生了事件的 SelectionKey 对象的集合 Iterator it = selectedKeys.iterator(); while (it.hasNext()) { SelectionKey key = (SelectionKey)it.next(); if ((key.readyOps() & SelectionKey.OP_ACCEPT) //对每个感兴趣事件的处理 == SelectionKey.OP_ACCEPT) { ServerSocketChannel ssc = (ServerSocketChannel)key.channel(); // SocketChannel sc = ssc.accept(); //接受新的连接
sc.configureBlocking( false ); SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ ); //将SocketChannel又注册一次,用于读取来自套接字的数据 } }
我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。
当来自一个套接字的数据到达时,它会触发一个 I/O 事件。这会导致在主循环中调用 Selector.select(),并返回一个或者多个 I/O 事件。这一次, SelectionKey 将被标记为 OP_READ 事件,如下所示:
} else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) { // Read the data SocketChannel sc = (SocketChannel)key.channel(); // ... }
这样Selector就实现了一个线程同时处理多个套接字连接的需求(高并发)。