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

前言

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

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

回顾五种I/O模型

由上图可知,所有的系统I/O都分为两个阶段:等待就绪操作

举一个例子,当我们要读某块网卡的时候,分为等待系统可读和真正读操作;同理,当我们要output数据的时候,分为等待某个文件/网卡 可写和真正的写操作。

理解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("服务器成功打印日志\n");
                bw.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}

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

对应上述图片,readLine()操作又有如下两个阶段(等待可读和实际读操作):

这个模型严格的来说效率是最快的,注意,我说的是效率。但是这种模型有一个缺点就是每当一个客户端发送请求的时候,服务器就会为其创建一个线程,在活动连接数不是特别高(小于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并没有显著的性能优势。

原文地址:https://www.cnblogs.com/zhxiansheng/p/10795050.html

时间: 2024-09-29 19:23:55

Netty基础系列(3) --彻底理解NIO的相关文章

c#基础系列3---深入理解ref 和out

"大菜":源于自己刚踏入猿途混沌时起,自我感觉不是一般的菜,因而得名"大菜",于自身共勉. 扩展阅读 c#基础系列1---深入理解 值类型和引用类型 c#基础系列2---深入理解 String 在上篇文章深入理解值类型和引用类型的时候,有的小伙伴就推荐说一说ref和out 关键字,昨天晚上彻夜难眠在想是否要谈一下呢,因为可谈的不是太多,也可能是我理解的不够深刻. 应用场景 out 修饰函数参数,以传递引用的方式向函数传递参数. out 关键字也可与泛型类型参数结合使

c#基础系列2---深入理解 String

"大菜":源于自己刚踏入猿途混沌时起,自我感觉不是一般的菜,因而得名"大菜",于自身共勉. 扩展阅读:深入理解值类型和引用类型 基本概念 string(严格来说应该是System.String) 类型是我们日常coding中用的最多的类型之一.那什么是String呢?^ ~ ^ String是一个不可变的连续16位的Unicode代码值的集合,它直接派生自System.Object类型. 与之对应的还有一个不常用的安全字符串类型System.Security.Sec

Android OpenGL ES零基础系列(一):理解GLSurfaceView,GLSurfaceView.Render的基本用法

转载请注明出处 前言 OpenGL ES是OpenGL的一个子集,是针对手机.PDA和游戏主机等嵌入式设备而设计的.该API由Khronos集团定义推广,Khronos是一个图形软硬件行业协会,该协会主要关注图形和多媒体方面的开放标准. 因此OpenGL ES作为第三方库被应用在android中. 到目前为止,OpenGL ES已经发展有了3个版本,OpenGL ES 1.0 , OpenGL ES 2.0 , OpenGL ES 3.0.其中OpenGL ES 1.0 是以OpenGL 1.3

夯实Java基础系列9:深入理解Class类和Object类

目录 Java中Class类及用法 Class类原理 如何获得一个Class类对象 使用Class类的对象来生成目标类的实例 Object类 类构造器public Object(); registerNatives()方法; Clone()方法实现浅拷贝 getClass()方法 equals()方法 hashCode()方法; toString()方法 wait() notify() notifAll() finalize()方法 CLass类和Object类的关系 参考文章 微信公众号 Ja

夯实Java基础系列10:深入理解Java中的异常体系

目录 为什么要使用异常 异常基本定义 异常体系 初识异常 异常和错误 异常的处理方式 "不负责任"的throws 纠结的finally throw : JRE也使用的关键字 异常调用链 自定义异常 异常的注意事项 当finally遇上return JAVA异常常见面试题 参考文章 微信公众号 Java技术江湖 个人公众号:黄小斜 - Java异常 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.c

夯实Java基础系列13:深入理解Java中的泛型

目录 泛型概述 一个栗子 特性 泛型的使用方式 泛型类 泛型接口 泛型通配符 泛型方法 泛型方法的基本用法 类中的泛型方法 泛型方法与可变参数 静态方法与泛型 泛型方法总结 泛型上下边界 泛型常见面试题 参考文章 微信公众号 Java技术江湖 个人公众号:黄小斜 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下Star.Fork.Watch三连哈,感谢你的

Java基础系列1:深入理解Java数据类型

Java基础系列1:深入理解Java数据类型 当初学习计算机的时候,教科书中对程序的定义是:程序=数据结构+算法,Java基础系列第一篇就聊聊Java中的数据类型. 本篇聊Java数据类型主要包括四个内容: Java基本类型 Java封装类型 自动装箱和拆箱 封装类型缓存机制 Java基本类型 Java基本类型分类.大小及表示范围 Java的基本数据类型总共有8种,包括三类:数值型,字符型,布尔型,其中 数值型: 整数类型:byte.short.int.long 浮点类型:float.doubl

Scrum入门基础系列之Scrum工件

Scrum入门基础系列之Scrum工件 3条回复 Scrum工件主要包含一下3种: 产品Backlog Sprint Backlog 产品增量 产品Backlog 在Scrum中,主要由产品负责人[参见Scrum入门基础系列之Scrum角色]整理和维护产品Backlog.产品Backlog是Scrum中维护需求的主要工件,也是做好Scrum的第一步.一个好的产品Backlog,是要符合DEEP原则的,即,产品Backlog是详略得当的(Detailed Appropriate),涌现的(Emer

Scrum入门基础系列之Scrum角色

Scrum入门基础系列之Scrum角色 5条回复 Scrum中定义有三个角色 产品负责人 ScrumMaster 开发团队 另外还会提到两个常见角色(经理和项目经理)在Scrum当中的职责. 产品负责人 职责 产品负责人最大的职责是为产品的投入产出比(ROI)负责,即最大化团队的投入产出比.在Scrum当中,由于Sprint是时间盒(即时间是固定的),且成本(软件开发中人力成本是最大的成本,其他忽略不计)也是固定的,那么最大化投入产出比就是如何做出最有价值的产品增量. 创建产品愿景 创建与维护产