上文我们描述了五中IO类型。第一种同步阻塞模型我们我们称之为BIO(Blocking IO),
第三种IO复用模型我们称之为NIO(Nonblocking IO)。
上图我们可以很容易的发现 BIO会为每个socket请求创建一个线程,而NIO可以通过一个线程处理多个请求。当然,我们可以为BIO构建一个线程池,这是一种伪异步的BIO模型。BIO和NIO最大的区别还是在阻塞上面。
阻塞主要有两方面
- 等待网络可读写
server.accept()
- 读写阻塞
通过观察InputStream的Api我们可以了解到,只有在下面三种情况下,BIO才会解除阻塞
1.有数据可读 2.可用数据已读取完毕 3.发送空指针或者I/O异常
所以,假如我们使用BIO进行网络消息传递,在网络不稳定的情况下,一次消息的传递需要花费30s,那这个bio的线程就需要阻塞30秒,假如所有的线程都阻塞30s,那系统基本就不可用了。
基于上述的问题,java推出了NIO。我们先用一段代码看看NIO的编程
public static void main(String[] args) throws Exception { // 打开一个ServerSocketChannel ServerSocketChannel socketChannel = ServerSocketChannel.open(); socketChannel.configureBlocking(Boolean.FALSE); // 获取ServerSocketChannel绑定的Socket ServerSocket socket = socketChannel.socket(); // 设置ServerSocket监听的端口 socket.bind(new InetSocketAddress(PORT)); System.out.println("开始等待客户端连接"); // 打开一个选择器 Selector selector = Selector.open(); // 将ServerSocketChannel注册到选择器上去并监听accept事件 socketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // 这里会发生阻塞,等待就绪的通道 int select = selector.select(); // 没有就绪的通道则什么也不做 if (select == 0) { continue; } // 获取SelectionKeys上已经就绪的通道的集合 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); // 遍历每一个Key while (iterator.hasNext()){ SelectionKey next = iterator.next(); if (next.isAcceptable()){ ServerSocketChannel channel = (ServerSocketChannel) next.channel(); SocketChannel socketChannel1 = channel.accept(); socketChannel1.register(selector,SelectionKey.OP_READ); }else if (next.isReadable()){ readDataFromSocket(next); } iterator.remove(); } } } private static ByteBuffer bb = ByteBuffer.allocate(1024); private static void readDataFromSocket(SelectionKey next) throws IOException { SocketChannel sc = (SocketChannel)next.channel(); bb.clear(); while (sc.read(bb)>0){ bb.flip();// //告知在当前位置和限制之间是否有元素 while (bb.hasRemaining()){ System.out.println((char) bb.get()); } System.out.println(); bb.clear(); } }
java为NIO提供了全新的API,大致有以下三种
- 缓冲区 Buffer
一个缓冲区对象是固定数量的数据的容器,其作用是一个存储器,或者分段运输区,在这里数据可被存储并在之后用于检索。从数据结构而言,缓冲区就是一个数组,通常是一个字节数组即ByteBuffer。每一种java基本类型都有对应的缓冲区
- Channel
与socket类和SeverSocket类似。NIO提供了SocketChannel和ServerSocketChannel ,这两个新增的通道都支持阻塞和非阻塞模式,阻塞模式使用简单,但是性能和可靠性都不好。非阻塞模式则相反。Channel可以自由的设置阻塞对Java来说意义非常重大。试想下之前的BIO网络编程为什么一个连接必须要对应一个线程。由于NIO的channel可以设置非阻塞模式,我们完全可以通过一个线程接受多个socket请求。
有两点需要我们注意:
1.文件通道总是阻塞的,不能设置成非阻塞模式
2.Channel只能往Buffer中写入
- Selector
选择器的作用是协调管理多个channel,selector定义了4种channel事件,每次channel注册的时候都必须定义好自己关心的是哪一种事件。注册完成后selector会一直阻塞,直到某些事件就绪。
public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4;
在了解上述三个api之后,我们再简单分析下上述代码
1.创建ServerSocketChannel 2.设置ServerSocketChannel为非阻塞状态 3.监听端口 4.将ServerSocketChannel 注册到一个Selector 5.等待选择接受就绪事件,一旦接收到 即可做出相应的操作
NIO的阻塞
如上图所示,NIO其实是有阻塞的环节的。那为什么我们仍然称NIO是同步非阻塞IO呢。这里主要涉及到一次完整的io请求是怎么进行读写的。
所有的系统I/O都分为两个阶段:
等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在"干活",而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。
对于BIO而言,如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,再阻塞的读到的数据。
对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
所以,socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。这部分的阻塞相对于BIO而言,是可以忽略不计的。所以我们可以认为NIO是非阻塞的。