• Netty基础系列(3) --彻底理解NIO


    前言

    上一节中我们提到了同步异步与阻塞非阻塞的区别,知道了同步并不等于阻塞。而本节的主角NIO是一种同步非阻塞的I/O模型,并且是I/O多路复用模型。NIO在java中被称为 New I/O。它并不能提高I/O处理的效率,注意我这里说的是效率,而从根本上解决的是I/O处理的并发问题。

    那么NIO的本质是什么样的呢?它是怎样与事件模型结合来解放线程、提高系统吞吐的呢?

    回顾五种I/O模型

    在这里插入图片描述

    由上图可知,所有的系统I/O都分为两个阶段:等待数据将数据从内核态复制到用户态

    举一个例子,传统的BIO中,当我们要读某块网卡接受到的网络数据的时候,程序会一直阻塞直到有数据到来,在此阶段cpu空转不干活。当监听到有数据的时候,就将数据从内核缓存区copy到用户缓存区,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。

    理解I/O的这两个阶段实际意义尤其的重要。下面讲NIO之前,我们先来深入剖析一下传统同步阻塞式BIO。

    同步阻塞式BIO

    下面这个伪代码是一个传统BIO模型,它的作用是打印客户端发来的数据并返回数据。

    public class SocketServer {
        public static void main(String args[]) {
            ExecutorService executor = Executors.newFixedThreadPool(100);//线程池
    
            try {
                ServerSocket ss = new ServerSocket(8888);
                System.out.println("启动服务器....");
                while (true) {
                    //阻塞等待接受客户端连接。
                    Socket socket = ss.accept();
                    System.out.println("客户端:" + socket.getInetAddress().getLocalHost() + "已连接到服务器");
                    executor.submit(new DataHandler(socket));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        static class DataHandler implements Runnable {
            Socket socket;
    
            public DataHandler(Socket socket) {
                this.socket = socket;
            }
            @Override
            public void run() {
                //阻塞操作
                try {
                    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    String mess = br.readLine();
                    System.out.println("客户端发来的数据:" + mess);
                    //返回数据
                    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                    bw.write("服务器成功打印日志
    ");
                    bw.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
    
            }
        }
    }
    

    上诉代码中,总共有三处地方发生了阻塞,第一处是等待客户端连接,第二处是input操作,第三处是output操作。所以该模型必须使用多线程来操作,如果是单线程,系统必将挂死在那里。

    对应上述图片,readLine()操作又有如下两个阶段(等待数据和将数据copy到用户缓存中):
    在这里插入图片描述

    这个模型严格的来说效率是最快的,注意,我说的是效率。但是这种模型有一个缺点就是每当一个客户端发送请求的时候,服务器就会为其创建一个线程,在活动连接数不是特别高(小于1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。

    但是这个方式的缺点就是,一旦有客户端访问,都创建一个专属的线程去处理,即便有线程池的存在,当并发访问量上来以后,CPU使用率会迅速上升,导致系统几乎陷入不可用的状态。

    NIO

    接下来我们进入今天的主题:NIO。

    如果是你在开发一个基于BIO模型的服务器,发现哪一天系统无法抗住庞大的并发,那么你有什么手段去优化你的服务器呢?

    没错,如果你看了之前的章节,那么你的脑海一定会出现多路复用模型,在传统BIO模型中,并发量上限的根本原因就是启动了过多的线程。

    对于BIO模型,之所以需要多线程,是因为在进行I/O操作的时候,一是没有办法知道到底能不能写、能不能读,只能”傻等”,即使通过各种估算,算出来操作系统没有能力进行读写,readLine()和write()函数中返回,这两个函数无法进行有效的中断。所以除了多开线程另起炉灶,没有好的办法利用CPU。

    NIO的读写函数则可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(readLine()返回0或者write()返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。

    多路复用器Selector

    当一个客户端请求到来的时候,我们会将其(Channel)注册到Selector上,然后Selector会不断的轮询注册在其上的Channel,如果某个Channel上面发生了读或者写事件,这个Channel就会处于就绪状态,会被Selector轮询出来,然后通过调用方法获取所有就绪Channel的集合,进行后续的操作。

    一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()(Netty基础系列(1)中有介绍)代替传统的select,所以没有数量1024/2048的上限限制。这也就意味着每一个线程负责Seletor的轮询,就可以接入成千上万个客户端,这确实是非常巨大的进步。

    通道Channel

    可以将其想象成一个水管,一个客户端的连接成功,可以想象成这根水管一头插入了服务器,一头插入了客户端,它们之间的通信就靠的这根水管。

    与传统的流不同,流只能在一个方向是移动(如上述代码,input只能写入,output只能写出)。但是Channel是全双工的,意思是能同时支持读写操作。

    缓存区Buffer

    在NIO库类中加入了一个Buffer对象。它区别于传统的流,能写入或者将数据直接读到Stream对象中。NIO所有数据都是基于Buffer处理的,在读取数据的时候直接读取Buffer里的数据,写数据的时候直接往Buffer里写数据。任何时候访问NIO中的数据,都是通过缓冲区进行操作的。

    通常情况下,操作系统的一次写操作分为两步: 1. 将数据从用户空间拷贝到系统空间。 2. 从系统空间往网卡写。同理,读操作也分为两步: ① 将数据从网卡拷贝到系统空间; ② 将数据从系统空间拷贝到用户空间。

    但是值得注意的是,如果使用了DirectByteBuffer(继承Buffer),一般来说可以减少一次系统空间到用户空间的拷贝。但Buffer创建和销毁的成本更高,更不宜维护,通常会用内存池来提高性能。

    如果数据量比较小的中小应用情况下,可以考虑使用heapBuffer;反之可以用directBuffer。

    总结

    本章多个角度的解释了NIO,以及NIO的基本组件。

    NIO编程的代码博主没有过多的解释,因为对于NIO编程博主也是个小菜鸡。但是!Netty将NIO进行了进一步的封装,让我们能使用更简单,更高效的API来完成我们NIO操作。比直接写NIO更轻松,也不必在意操作系统之间的区别。但是有兴趣的小伙伴可以自行学习NIO编程,然后再体会对比一下与Netty编程实现相同功能的难度与代码量。你就会深深感叹,Netty真他么的强大。

    最后再提醒各位一点,使用NIO != 高性能,当连接数<1000,并发程度不高或者局域网环境下NIO并没有显著的性能优势。

  • 相关阅读:
    鼠标点击表格行背景变色
    2006年星座运势全解巨蟹
    去除衣物污渍大本营
    海量数据库的查询优化及分页算法方案[转帖]
    奇怪的Access错误
    深圳易高科技有限公司面试题目
    各大IT公司的起名缘由
    微星横向菜单
    【转】函数参数入栈问题
    堆和栈的区别 (转贴)
  • 原文地址:https://www.cnblogs.com/zhxiansheng/p/10795050.html
Copyright © 2020-2023  润新知