NIO学习笔记(三) 甚欢篇

403 阅读6分钟

与君初相识,犹如故人归。 天涯明月新,朝暮最相思。-- 唐.杜牧《会友》 最近很喜欢这首诗。

浅谈常见的I/O模型

我们再重新梳理一下网络通信模型,或者说是I/O模型更为准确一些,因为网络也可以算作一种I/O。一般来说一个典型的读写流程是这样的:

我们这里假设是进程发起了读操作,又知道进程在创建时,操作系统会分配给它内存和必要的资源,我们姑且可以将操作系统分配给进程的内存称之为用户空间。当进程发起读操作,事实上会调用操作系统的函数,我们这里要再次强调一下,我们写的程序是无法直接接触到文件的,他们是通过调用操作系统向外暴露的接口来实现读写的。读操作大致上分成两个阶段:

  • 等待数据就绪.
  • 数据就绪,将数据从内核空间(简单的可以理解为操作系统所占有的空间)拷贝进入用户空间(粗略的说,就是进程被分配的空间)。

当数据从内核空间拷贝到用户空间完毕,程序就获取到了所要读取的数据,假设在数据没有到达用户空间之前,进程一直处于阻塞状态,我们就称程序的I/O方式是blocking I / O,我们并不希望进程在处理一个文件或者连接陷入阻塞状态,所以我们可以在进程里开线程或者子进程去处理一个连接或者文件,但是连接多了,消耗又太多。

于是Non—Blocking I/O应需而生,当数据还未到达用户空间时,系统I/O函数会告诉进程,数据还没准备好,你再等等吧。事实上NIO也是在不断的询问操作系统: 数据好了没,数据好了没啊。但是这是被进程所负责的,连接过多或读取的文件过多的时候,会大幅度提升CPU的使用率。一般操作系统提供了更为高效的监测数据准备好了的接口供进程所使用,也就是I/O多路复用。

I/O多路复用,粗略的说就是以前是进程主动的询问操作系统数据好没好,现在是操作系统会跟你讲数据好没好,但每一个Socket操作还是非阻塞的,所以从概念上来讲I/O多路复用和Non-Blocking I/O 非常相似。这也跟编程语言有关系,如果使用的是比较贴近底层的高级语言,像C、C++,大概理解这两个的区别是不困难的,但是对于Java这种跨平台的语言来说,这两个概念姑且可以混为一谈,JDK做了大量的封装,表面上是NIO,Java内部这么称呼,事实上是两种混合在一起的,也就是NIO+多路复用。

I/O多路复用在《NIO 学习笔记(二)相识篇》Socket简介和I/O多路复用,已经有过不少讨论,这里我们系统的总结一下,当用户调用操作系统对应的多路复用函数时,Linux下是select(),不同的操作系统会有不同的实现,我们就暂时用select()来称呼这个操作系统提供的接口,该接口会启动一个进程会侦测服务端对应的所有客户端的行为,如果发现某个客户端的数据就绪,服务端此时应该做读处理,并根据发来的数据来决定是否发送数据,如果要发送数据,就需要等待select进程通知写事件就绪。

这与事件响应模型有几分类似,但是这个模型仍然有一些弊端,服务端程序根据不同的事件来采取对应的行为,我们用伪码大致来表示。

   while(true){
        status = select();
        if(status == 可读){ //

        }
        if (status == 可写){

        }
    }

假设可读大量的出现,那么客户端收到服务端的响应就会有一定的延迟。所以从这里来讲BIO也不是一无是处。我在刚学Java的时候,总是以为NIO比BIO先进,现在来看还是需要根据不同的场景来选择不同的I/O。 不过上面说的问题,一些高级语言库已经解决了。

人类的社会在发展,人类的社会在进步,asynchronous I/O出来了,Linux 内核2.6版本引入了,我们先来简单的看一下它的流程,用户进程在通过异步I/O接口发起读操作之后,首先它会立刻返回,某种意义上你可以理解为这个返回是一个假的结果,在操作系统内核完成数据从内核空间到用户数据的迁移之后,也就是数据已经可以读了之后,会通知用户进程。

java中的AIO(asynchronous I/O)

Java 当中的AIO(asynchronous I/O)简单的说就是在原有NIO基础上,增加了几种类型的channel。 我们再来回忆一下Java中NIO的几个概念: channel、buffer、selector。 channel是通道,buffer是缓冲区,就像是水管和水池的关系。NIO是如果水池中有水了,来通知我一下,我现在用通道将水池的水取出来。而AIO则更进一步,通道已经获取了水池里的水了,来通知一下。 新增的channel都由AsynchronousChannel接口派生而出。 通知的方式大致有两种:

  • future接口
  • 回调函数(很像Ajax,服务端成功响应调用一个success函数,失败调用一个函数) 下面是用asynchronous channel操作文件的例子,我们可以大致通过例子来感受一下异步,读取完了怎么通知呢,事实上也是通过一个线程来做的: Future接口示例:
 AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("E:\\学习资料\\测试.txt"));
        ByteBuffer buffer = ByteBuffer.allocate(21 * 1024);
        Future<Integer> future = channel.read(buffer, 0);
        while (!future.isDone()){
            System.out.println("正在执行的是:"+Thread.currentThread().getName());
        }
        // 阻塞式方法,直到线程执行完毕
        Integer result = future.get();
        buffer.flip();
        System.out.println(result);
        System.out.println(new String(buffer.array(),0,buffer.limit()));

回调函数示例:

AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("E:\\学习资料\\测试.txt"));
        ByteBuffer buffer = ByteBuffer.allocate(21 * 1024);
        channel.read(buffer, 0, null, new CompletionHandler<Integer, Object>() {
            @Override
            public void completed(Integer result, Object attachment) {
                buffer.flip();
                System.out.println(new String(buffer.array(),0,buffer.limit()));
                System.out.println("读取完毕");
            }

            @Override
            public void failed(Throwable exc, Object attachment) {

            }
        });
        while (true){
            // 这里是为了验证系统开辟了一个线程,所做的打印。
            System.out.println("正在执行的是:"+Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(5);
        }

Java NIO存在的问题

  • 写起来太繁琐了

参看《NIO 学习笔记(二)相识篇》,一个聊天室就写了那么多,而且还有坑。

  • NIO != 高性能

我早期的时候是这么觉得的,NIO性能非常强大,甩BIO十条街。现在来看,还是要看并发情况的,当并发数不高,连接数 < 1000, NIO并没有明显的优势

  • JDK NIO存在bug

Java nio 空轮询bug到底是什么 , 以我目前来看,JDK并没有解决这个BUG,这个bug是被Java领域的网络编程框架mina、netty解决了,用netty做网络编程相对来说写起来更简单清晰,假如你想做网络编程,推荐使用Java领域内的网络编程框架,不推荐直接使用原生的NIO,很可能一不小心就踩坑了,这个我深有体会。

参考资料: