JDK10都发布了,nio你了解多少?

Hmily25 6年前
   <h2>前言</h2>    <p>只有光头才能变强</p>    <p>回顾前面:</p>    <ul>     <li><a href="https://mp.weixin.qq.com/s?__biz=MzI4Njg5MDA5NA==&mid=2247484222&idx=1&sn=5191aca33f7b331adaef11c5e07df468&chksm=ebd7423fdca0cb29cdc59b4c79afcda9a44b9206806d2212a1b807c9f5879674934c37c250a1#rd" rel="nofollow,noindex">给女朋友讲解什么是代理模式</a></li>     <li><a href="https://mp.weixin.qq.com/s?__biz=MzI4Njg5MDA5NA==&mid=2247484226&idx=1&sn=a354d872978fc5db79a012c27ab5277f&chksm=ebd74243dca0cb5515fcad1ecfeda6a329be38c30a82a1325a5a19e7f62b0fa3c74069eed24d#rd" rel="nofollow,noindex">包装模式就是这么简单啦</a></li>    </ul>    <p>本来我预想是先来回顾一下传统的IO模式的,将传统的IO模式的相关类理清楚(因为IO的类很多)。</p>    <p>但是,发现在整理的过程 <strong>已经有很多优秀的文章</strong> 了,而我自己来整理的话可能达不到他们的水平。并且 <strong>传统的IO估计大家都会用,而NIO就不一定了</strong> 。</p>    <p>下面我就贴几张我认为整理比较优秀的思维导图(下面会给出图片来源地址,大家可前往阅读):</p>    <p>按操作方式分类结构图:</p>    <p><img src="https://simg.open-open.com/show/3abd22f73e146b121e5681fd430ca05a.jpg"></p>    <p>字节流的输入和输出对照图:</p>    <p><img src="https://simg.open-open.com/show/5d274d13ea07e1342f5aa4169b9399df.jpg"></p>    <p>字符流的输入和输出对照图:</p>    <p><img src="https://simg.open-open.com/show/9cf1ec06c707080c2cc10ca7c3de4448.jpg"></p>    <p>按操作对象分类结构图:</p>    <p><img src="https://simg.open-open.com/show/504689f9560695a9c3da3858bdc86e84.jpg"></p>    <p>上述图片原文地址,知乎作者@小明:</p>    <ul>     <li><a href="/misc/goto?guid=4959757835534379268" rel="nofollow,noindex">https://zhuanlan.zhihu.com/p/28286559</a></li>    </ul>    <p>还有 <strong>阅读传统IO源码</strong> 的优秀文章:</p>    <ul>     <li><a href="/misc/goto?guid=4959757835616907120" rel="nofollow,noindex">https://blog.csdn.net/panweiwei1994/article/details/78046000</a></li>    </ul>    <p>相信大家看完上面两个给出的链接+理解了 <a href="https://mp.weixin.qq.com/s?__biz=MzI4Njg5MDA5NA==&mid=2247484226&idx=1&sn=a354d872978fc5db79a012c27ab5277f&chksm=ebd74243dca0cb5515fcad1ecfeda6a329be38c30a82a1325a5a19e7f62b0fa3c74069eed24d#rd" rel="nofollow,noindex">包装模式就是这么简单啦</a> ,传统的IO应该就没什么事啦~~</p>    <p>而NIO对于我来说可以说是挺 <strong>陌生</strong> 的,在当初学的时候是接触过的。 <strong>但是</strong> 一直没有用它,所以停留认知:nio是jdk1.4开始有的,比传统IO高级。</p>    <p>相信很多初学者都跟我一样,对NIO是不太了解的。而我们现在jdk10都已经发布了,jdk1.4的nio都不知道,这有点说不过去了。</p>    <p>所以我花了几天去了解 <strong>NIO的核心知识点</strong> ,期间看了《Java 编程思想》和《疯狂Java 讲义》的nio模块。 <strong>但是</strong> ,会发现看完了之后还是很 <strong>迷</strong> ,不知道NIO这是干嘛用的,而网上的资料与书上的知识点没有很好地对应。</p>    <ul>     <li>网上的资料很多都以IO的五种模型为基础来讲解NIO,而IO这五种模型其中又涉及到了很多概念: 同步/异步/阻塞/非阻塞/多路复用 , <strong>而不同的人又有不同的理解方式</strong> 。</li>     <li>还有涉及到了unix的 select/epoll/poll/pselect , fd 这些关键字,没有相关基础的人看起来简直是天书</li>     <li>这就导致了在初学时认为nio远不可及</li>    </ul>    <p>我在找资料的过程中也收藏了好多讲解NIO的资料,这篇文章就是 <strong>以初学的角度来理解NIO</strong> 。也算是我这两天看NIO的一个总结吧。</p>    <ul>     <li>希望大家可以看了之后知道什么是NIO,NIO的核心知识点是什么,会使用NIO~</li>    </ul>    <p>那么接下来就开始吧,如果文章有错误的地方请大家多多包涵,不吝在评论区指正哦~</p>    <p>声明:本文使用JDK1.8</p>    <h2>一、NIO的概述</h2>    <p>JDK 1.4中的 java.nio.*包 中引入新的Java I/O库,其目的是 <strong>提高速度</strong> 。实际上,“旧”的I/O包已经使用NIO 重新实现过,即使我们不显式的使用NIO编程,也能从中受益 。</p>    <ul>     <li>nio翻译成 no-blocking io 或者 new io 都无所谓啦,都说得通~</li>    </ul>    <p>在《Java编程思想》读到 <strong>“即使我们不显式的使用NIO编程,也能从中受益”</strong> 的时候,我是挺在意的,所以:我们 <strong>测试</strong> 一下使用NIO复制文件和传统IO复制文件的性能:</p>    <pre>  <code class="language-java">import java.io.*;  import java.nio.ByteBuffer;  import java.nio.channels.FileChannel;    public class SimpleFileTransferTest {        private long transferFile(File source, File des) throws IOException {          long startTime = System.currentTimeMillis();            if (!des.exists())              des.createNewFile();            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source));          BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(des));            //将数据源读到的内容写入目的地--使用数组          byte[] bytes = new byte[1024 * 1024];          int len;          while ((len = bis.read(bytes)) != -1) {              bos.write(bytes, 0, len);          }            long endTime = System.currentTimeMillis();          return endTime - startTime;      }        private long transferFileWithNIO(File source, File des) throws IOException {          long startTime = System.currentTimeMillis();            if (!des.exists())              des.createNewFile();            RandomAccessFile read = new RandomAccessFile(source, "rw");          RandomAccessFile write = new RandomAccessFile(des, "rw");            FileChannel readChannel = read.getChannel();          FileChannel writeChannel = write.getChannel();              ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);//1M缓冲区            while (readChannel.read(byteBuffer) > 0) {              byteBuffer.flip();              writeChannel.write(byteBuffer);              byteBuffer.clear();          }            writeChannel.close();          readChannel.close();          long endTime = System.currentTimeMillis();          return endTime - startTime;      }        public static void main(String[] args) throws IOException {          SimpleFileTransferTest simpleFileTransferTest = new SimpleFileTransferTest();          File sourse = new File("F:\\电影\\[电影天堂www.dygod.cn]猜火车-cd1.rmvb");          File des = new File("X:\\Users\\ozc\\Desktop\\io.avi");          File nio = new File("X:\\Users\\ozc\\Desktop\\nio.avi");            long time = simpleFileTransferTest.transferFile(sourse, des);          System.out.println(time + ":普通字节流时间");            long timeNio = simpleFileTransferTest.transferFileWithNIO(sourse, nio);          System.out.println(timeNio + ":NIO时间");          }    }</code></pre>    <p>我分别测试了文件大小为13M,40M,200M的:</p>    <p><img src="https://simg.open-open.com/show/e885f0bd9aa351554434af86601f7497.png"></p>    <p><img src="https://simg.open-open.com/show/ae51deebc4945a6e287a39d2f07dc3f1.png"></p>    <p><img src="https://simg.open-open.com/show/71891c935273babefa53435261edabb8.png"></p>    <h2>1.1为什么要使用NIO</h2>    <p>可以看到使用过NIO重新实现过的 <strong>传统IO根本不虚</strong> ,在大文件下效果还比NIO要好(当然了,个人几次的测试,或许不是很准)</p>    <ul>     <li>而NIO要有一定的学习成本,也没有传统IO那么好理解。</li>    </ul>    <p>那这意味着我们 <strong>可以不使用/学习NIO了吗</strong> ?</p>    <p>答案是 <strong>否定</strong> 的,IO操作往往在 <strong>两个场景</strong> 下会用到:</p>    <ul>     <li>文件IO</li>     <li>网络IO</li>    </ul>    <p>NIO的 <strong>魅力:在网络中使用IO就可以体现出来了</strong> !</p>    <ul>     <li>后面会说到网络中使用NIO,不急哈~</li>    </ul>    <h2>二、NIO快速入门</h2>    <p>首先我们来看看 <strong>IO和NIO的区别</strong> :</p>    <p><img src="https://simg.open-open.com/show/e770a297e39bc8bf51675b2e69fed54d.png"></p>    <ul>     <li>可简单认为: <strong>IO是面向流的处理,NIO是面向块(缓冲区)的处理</strong>      <ul>       <li>面向流的I/O 系统 <strong>一次一个字节地处理数据</strong> 。</li>       <li>一个面向块(缓冲区)的I/O系统 <strong>以块的形式处理数据</strong> 。</li>      </ul> </li>    </ul>    <p>NIO主要有 <strong>三个核心部分组成</strong> :</p>    <ul>     <li><strong>buffer缓冲区</strong></li>     <li><strong>Channel管道</strong></li>     <li><strong>Selector选择器</strong></li>    </ul>    <h2>2.1buffer缓冲区和Channel管道</h2>    <p>在NIO中并不是以流的方式来处理数据的,而是以buffer缓冲区和Channel管道 <strong>配合使用</strong> 来处理数据。</p>    <p>简单理解一下:</p>    <ul>     <li>Channel管道比作成铁路,buffer缓冲区比作成火车(运载着货物)</li>    </ul>    <p>而我们的NIO就是 通过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理 !</p>    <ul>     <li>要时刻记住:Channel不与数据打交道,它只负责运输数据。与数据打交道的是Buffer缓冲区      <ul>       <li><strong>Channel-->运输</strong></li>       <li><strong>Buffer-->数据</strong></li>      </ul> </li>    </ul>    <p>相对于传统IO而言, <strong>流是单向的</strong> 。对于NIO而言,有了Channel管道这个概念,我们的 <strong>读写都是双向</strong> 的(铁路上的火车能从广州去北京、自然就能从北京返还到广州)!</p>    <h3>2.1.1buffer缓冲区核心要点</h3>    <p>我们来看看Buffer缓冲区有什么值得我们注意的地方。</p>    <p>Buffer是缓冲区的抽象类:</p>    <p><img src="https://simg.open-open.com/show/c79582345ff57bf950710d9bfc300c32.png"></p>    <p>其中ByteBuffer是 <strong>用得最多的实现类</strong> (在管道中读写字节数据)。</p>    <p><img src="https://simg.open-open.com/show/4db99a7cd5890579f5de0b99f747dd68.png"></p>    <p>拿到一个缓冲区我们往往会做什么?很简单,就是 <strong>读取缓冲区的数据/写数据到缓冲区中</strong> 。所以,缓冲区的核心方法就是:</p>    <ul>     <li>put()</li>     <li>get()</li>    </ul>    <p><img src="https://simg.open-open.com/show/6feed136dbc0545c24f96b79cc944913.png"></p>    <p><img src="https://simg.open-open.com/show/6ee88003f145a8e4d30d2e38a318e4df.png"></p>    <p>Buffer类维护了4个核心变量属性来提供 <strong>关于其所包含的数组的信息</strong> 。它们是:</p>    <ul>     <li>容量Capacity      <ul>       <li><strong>缓冲区能够容纳的数据元素的最大数量</strong> 。容量在缓冲区创建时被设定,并且永远不能被改变。(不能被改变的原因也很简单,底层是数组嘛)</li>      </ul> </li>     <li>上界Limit      <ul>       <li><strong>缓冲区里的数据的总数</strong> ,代表了当前缓冲区中一共有多少数据。</li>      </ul> </li>     <li>位置Position      <ul>       <li><strong>下一个要被读或写的元素的位置</strong> 。Position会自动由相应的 get( ) 和 put( ) 函数更新。</li>      </ul> </li>     <li>标记Mark      <ul>       <li>一个备忘位置。 <strong>用于记录上一次读写的位置</strong> 。</li>      </ul> </li>    </ul>    <p><img src="https://simg.open-open.com/show/f9d0c54962a4ffd56ea4fd95534e85f1.png"></p>    <h3>2.1.2buffer代码演示</h3>    <p>首先展示一下 <strong>是如何创建缓冲区的,核心变量的值是怎么变化的</strong> 。</p>    <pre>  <code class="language-java">    public static void main(String[] args) {            // 创建一个缓冲区          ByteBuffer byteBuffer = ByteBuffer.allocate(1024);            // 看一下初始时4个核心变量的值          System.out.println("初始时-->limit--->"+byteBuffer.limit());          System.out.println("初始时-->position--->"+byteBuffer.position());          System.out.println("初始时-->capacity--->"+byteBuffer.capacity());          System.out.println("初始时-->mark--->" + byteBuffer.mark());            System.out.println("--------------------------------------");            // 添加一些数据到缓冲区中          String s = "Java3y";          byteBuffer.put(s.getBytes());            // 看一下初始时4个核心变量的值          System.out.println("put完之后-->limit--->"+byteBuffer.limit());          System.out.println("put完之后-->position--->"+byteBuffer.position());          System.out.println("put完之后-->capacity--->"+byteBuffer.capacity());          System.out.println("put完之后-->mark--->" + byteBuffer.mark());      }</code></pre>    <p>运行结果:</p>    <p><img src="https://simg.open-open.com/show/4f04fd464344b577c9381e1361be8b37.png"></p>    <p>现在 <strong>我想要从缓存区拿数据</strong> ,怎么拿呀??NIO给了我们一个 flip() 方法。这个方法可以 <strong>改动position和limit的位置</strong> !</p>    <p>还是上面的代码,我们 flip() 一下后,再看看4个核心属性的值会发生什么变化:</p>    <p><img src="https://simg.open-open.com/show/fcc5241b9fb323b85c1d2481a9895968.png"></p>    <p>很明显的是:</p>    <ul>     <li><strong>limit变成了position的位置了</strong></li>     <li><strong>而position变成了0</strong></li>    </ul>    <p>看到这里的同学可能就会想到了:当调用完 filp() 时: <strong>limit是限制读到哪里,而position是从哪里读</strong></p>    <p>一般我们称 filp() 为 <strong>“切换成读模式”</strong></p>    <ul>     <li>每当要从缓存区的时候读取数据时,就调用 filp() <strong>“切换成读模式”</strong> 。</li>    </ul>    <p><img src="https://simg.open-open.com/show/ec9e6ad2a1e58ea20447004f8c92192c.png"></p>    <p>切换成读模式之后,我们就可以读取缓冲区的数据了:</p>    <pre>  <code class="language-java">        // 创建一个limit()大小的字节数组(因为就只有limit这么多个数据可读)          byte[] bytes = new byte[byteBuffer.limit()];            // 将读取的数据装进我们的字节数组中          byteBuffer.get(bytes);            // 输出数据          System.out.println(new String(bytes, 0, bytes.length));</code></pre>    <p><img src="https://simg.open-open.com/show/cef08bb0d542141ba9ea7e345856ed01.png"></p>    <p>随后输出一下核心变量的值看看:</p>    <p><img src="https://simg.open-open.com/show/65134c55a5d6bc06542eb42cf0bd91bb.png"></p>    <p>读完我们还想写数据到缓冲区,那就使用 clear() 函数,这个函数会“清空”缓冲区:</p>    <ul>     <li>数据没有真正被清空,只是被 <strong>遗忘</strong> 掉了</li>    </ul>    <p><img src="https://simg.open-open.com/show/6711db7114121062998699bb366e43d4.png"></p>    <h3>2.1.3FileChannel通道核心要点</h3>    <p><img src="https://simg.open-open.com/show/a31f80cc8c666a2896867952e143dccf.png"></p>    <p>Channel通道 <strong>只负责传输数据、不直接操作数据的</strong> 。操作数据都是通过Buffer缓冲区来进行操作!</p>    <pre>  <code class="language-java">        // 1. 通过本地IO的方式来获取通道          FileInputStream fileInputStream = new FileInputStream("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是这么简单.md");            // 得到文件的输入通道          FileChannel inchannel = fileInputStream.getChannel();            // 2. jdk1.7后通过静态方法.open()获取通道          FileChannel.open(Paths.get("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是这么简单2.md"), StandardOpenOption.WRITE);</code></pre>    <p>使用 <strong>FileChannel配合缓冲区</strong> 实现文件复制的功能:</p>    <p><img src="https://simg.open-open.com/show/868c22c3068deb47f5fcb240e1709579.png"></p>    <p>使用 <strong>内存映射文件</strong> 的方式实现 <strong>文件复制</strong> 的功能(直接操作缓冲区):</p>    <p><img src="https://simg.open-open.com/show/538b9514891f5e787d4321991e309246.png"></p>    <p>通道之间通过 transfer() 实现数据的传输(直接操作缓冲区):</p>    <p><img src="https://simg.open-open.com/show/82997a013a64d5a83864dd5c77dab3ec.png"></p>    <h3>2.1.4直接与非直接缓冲区</h3>    <ul>     <li>非直接缓冲区是 <strong>需要</strong> 经过一个:copy的阶段的(从内核空间copy到用户空间)</li>     <li>直接缓冲区 <strong>不需要</strong> 经过copy阶段,也可以理解成---> <strong>内存映射文件</strong> ,(上面的图片也有过例子)。</li>    </ul>    <p><img src="https://simg.open-open.com/show/915afa079591f69f0d68f318be994ad1.png"></p>    <p><img src="https://simg.open-open.com/show/adf3284de641d630cfd6d167291b93e9.png"></p>    <p>使用直接缓冲区有两种方式:</p>    <ul>     <li>缓冲区创建的时候分配的是直接缓冲区</li>     <li>在FileChannel上调用 map() 方法,将文件直接映射到内存中创建</li>    </ul>    <p><img src="https://simg.open-open.com/show/d90bce67357534e26b726b61ea48f91a.png"></p>    <h3>2.1.5scatter和gather、字符集</h3>    <p>这个知识点我感觉用得挺少的,不过很多教程都有说这个知识点,我也拿过来说说吧:</p>    <ul>     <li>分散读取(scatter):将一个通道中的数据分散读取到多个缓冲区中</li>     <li>聚集写入(gather):将多个缓冲区中的数据集中写入到一个通道中</li>    </ul>    <p><img src="https://simg.open-open.com/show/949b445b97f9f66d7c8b4f10aee1d3c1.png"></p>    <p><img src="https://simg.open-open.com/show/aa10e39337df16ccd7863c7e1e0854fe.png"></p>    <p>分散读取</p>    <p><img src="https://simg.open-open.com/show/d047494a12430565422e4be4ce417e06.png"></p>    <p>聚集写入</p>    <p><img src="https://simg.open-open.com/show/fb52918d4b3bec44219d58ea34302c48.png"></p>    <p>字符集(只要编码格式和解码格式一致,就没问题了)</p>    <p><img src="https://simg.open-open.com/show/48611918c70f0bf0c8face0488ad7565.png"></p>    <h2>三、IO模型理解</h2>    <p>文件的IO就告一段落了,我们来学习网络中的IO~~~为了更好地理解NIO, <strong>我们先来学习一下IO的模型</strong> ~</p>    <p>根据UNIX网络编程对I/O模型的分类, <strong>在UNIX可以归纳成5种I/O模型</strong> :</p>    <ul>     <li><strong>阻塞I/O</strong></li>     <li><strong>非阻塞I/O</strong></li>     <li><strong>I/O多路复用</strong></li>     <li>信号驱动I/O</li>     <li>异步I/O</li>    </ul>    <h2>3.0学习I/O模型需要的基础</h2>    <h3>3.0.1文件描述符</h3>    <p>Linux 的内核将所有外部设备 <strong>都看做一个文件来操作</strong> ,对一个文件的读写操作会 <strong>调用内核提供的系统命令(api)</strong> ,返回一个 file descriptor (fd,文件描述符)。而对一个socket的读写也会有响应的描述符,称为 socket fd (socket文件描述符),描述符就是一个数字, <strong>指向内核中的一个结构体</strong> (文件路径,数据区等一些属性)。</p>    <ul>     <li>所以说:在Linux下对文件的操作是 <strong>利用文件描述符(file descriptor)来实现的</strong> 。</li>    </ul>    <h3>3.0.2用户空间和内核空间</h3>    <p>为了保证用户进程不能直接操作内核(kernel), <strong>保证内核的安全</strong> ,操心系统将虚拟空间划分为两部分</p>    <ul>     <li><strong>一部分为内核空间</strong> 。</li>     <li><strong>一部分为用户空间</strong> 。</li>    </ul>    <h3>3.0.3I/O运行过程</h3>    <p>我们来看看IO在系统中的运行是怎么样的(我们 <strong>以read为例</strong> )</p>    <p><img src="https://simg.open-open.com/show/e80c878620d9afc2e866b70eb403b383.png"></p>    <p>可以发现的是:当应用程序调用read方法时,是需要 <strong>等待</strong> 的--->从内核空间中找数据,再将内核空间的数据拷贝到用户空间的。</p>    <ul>     <li><strong>这个等待是必要的过程</strong> !</li>    </ul>    <p>下面只讲解用得最多的3个I/0模型:</p>    <ul>     <li><strong>阻塞I/O</strong></li>     <li><strong>非阻塞I/O</strong></li>     <li><strong>I/O多路复用</strong></li>    </ul>    <h2>3.1阻塞I/O模型</h2>    <p>在进程(用户)空间中调用 recvfrom ,其系统调用直到数据包到达且 <strong>被复制到应用进程的缓冲区中或者发生错误时才返回</strong> ,在此期间 <strong>一直等待</strong> 。</p>    <p><img src="https://simg.open-open.com/show/5ad5c83ed696c7c7ef582d8d218c4ac6.png"></p>    <h2>3.2非阻塞I/O模型</h2>    <p>recvfrom 从应用层到内核的时候,如果没有数据就 <strong>直接返回</strong> 一个EWOULDBLOCK错误,一般都对非阻塞I/O模型 <strong>进行轮询检查这个状态</strong> ,看内核是不是有数据到来。</p>    <p><img src="https://simg.open-open.com/show/9f8f30250ba54c200b4354dbd0306d8a.png"></p>    <h2>3.3I/O复用模型</h2>    <p>前面也已经说了:在Linux下对文件的操作是 <strong>利用文件描述符(file descriptor)来实现的</strong> 。</p>    <p>在Linux下它是这样子实现I/O复用模型的:</p>    <ul>     <li>调用 select/poll/epoll/pselect 其中一个函数, <strong>传入多个文件描述符</strong> ,如果有一个文件描述符 <strong>就绪,则返回</strong> ,否则阻塞直到超时。</li>    </ul>    <p>比如 poll() 函数是这样子的: int poll(struct pollfd *fds,nfds_t nfds, int timeout);</p>    <p>其中 pollfd 结构定义如下:</p>    <pre>  <code class="language-java">struct pollfd {      int fd;         /* 文件描述符 */      short events;         /* 等待的事件 */      short revents;       /* 实际发生了的事件 */  };</code></pre>    <p><img src="https://simg.open-open.com/show/1386971a37730af6bf62f45478a4159f.gif"></p>    <p><img src="https://simg.open-open.com/show/335637bead258ec04f20be9f518b98c4.png"></p>    <ul>     <li>(1)当用户进程调用了select,那么整个进程会被block;</li>     <li>(2)而同时,kernel会“监视”所有select负责的socket;</li>     <li>(3)当任何一个socket中的数据准备好了,select就会返回;</li>     <li>(4)这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程(空间)。</li>     <li>所以,I/O 多路复用的特点是 <strong>通过一种机制一个进程能同时等待多个文件描述符</strong> ,而这些文件描述符 <strong>其中的任意一个进入读就绪状态</strong> ,select()函数 <strong>就可以返回</strong> 。</li>    </ul>    <p>select/epoll的优势并不是对于单个连接能处理得更快,而是 <strong>在于能处理更多的连接</strong> 。</p>    <h2>3.4I/O模型总结</h2>    <p>正经的描述都在上面给出了,不知道大家理解了没有。下面我举几个例子总结一下这三种模型:</p>    <p>阻塞I/O:</p>    <ul>     <li>Java3y跟女朋友去买喜茶,排了很久的队终于可以点饮料了。我要绿研,谢谢。可是喜茶不是点了单就能立即拿,于是我 <strong>在喜茶门口等了一小时才拿到</strong> 绿研。      <ul>       <li>在门口干等一小时</li>      </ul> </li>    </ul>    <p>非阻塞I/O:</p>    <ul>     <li>Java3y跟女朋友去买一点点,排了很久的队终于可以点饮料了。我要波霸奶茶,谢谢。可是一点点不是点了单就能立即拿, <strong>同时</strong> 服务员告诉我:你大概要等半小时哦。你们先去逛逛吧~于是Java3y跟女朋友去玩了几把斗地主,感觉时间差不多了。于是 <strong>又去一点点问</strong> :请问到我了吗?我的单号是xxx。服务员告诉Java3y:还没到呢,现在的单号是XXX,你还要等一会,可以去附近耍耍。问了好几次后,终于拿到我的波霸奶茶了。      <ul>       <li>去逛了下街、斗了下地主,时不时问问到我了没有</li>      </ul> </li>    </ul>    <p>I/O复用模型:</p>    <ul>     <li>Java3y跟女朋友去麦当劳吃汉堡包,现在就厉害了可以使用微信小程序点餐了。于是跟女朋友找了个地方坐下就用小程序点餐了。点餐了之后玩玩斗地主、聊聊天什么的。 <strong>时不时听到广播在复述XXX请取餐</strong> ,反正我的单号还没到,就继续玩呗。~~ <strong>等听到广播的时候再取餐就是了</strong> 。时间过得挺快的,此时传来:Java3y请过来取餐。于是我就能拿到我的麦辣鸡翅汉堡了。      <ul>       <li>听广播取餐, <strong>广播不是为我一个人服务</strong> 。广播喊到我了,我过去取就Ok了。</li>      </ul> </li>    </ul>    <h2>四、使用NIO完成网络通信</h2>    <h2>4.1NIO基础继续讲解</h2>    <p>回到我们最开始的图:</p>    <p><img src="https://simg.open-open.com/show/e770a297e39bc8bf51675b2e69fed54d.png"></p>    <p>NIO被叫为 no-blocking io ,其实是在 <strong>网络这个层次中理解的</strong> ,对于 <strong>FileChannel来说一样是阻塞</strong> 。</p>    <p>我们前面也仅仅讲解了FileChannel,对于我们网络通信是还有几个Channel的~</p>    <p><img src="https://simg.open-open.com/show/948c1d5d3fa8ced4da074dc1031dce9d.png"></p>    <p>所以说:我们 <strong>通常</strong> 使用NIO是在网络中使用的,网上大部分讨论NIO都是在 <strong>网络通信的基础之上</strong> 的!说NIO是非阻塞的NIO也是 <strong>网络中体现</strong> 的!</p>    <p>从上面的图我们可以发现还有一个 Selector 选择器这么一个东东。从一开始我们就说过了,nio的 <strong>核心要素</strong> 有:</p>    <ul>     <li>Buffer缓冲区</li>     <li>Channel通道</li>     <li>Selector选择器</li>    </ul>    <p>我们在网络中使用NIO往往是I/O模型的 <strong>多路复用模型</strong> !</p>    <ul>     <li>Selector选择器就可以比喻成麦当劳的 <strong>广播</strong> 。</li>     <li><strong>一个线程能够管理多个Channel的状态</strong></li>    </ul>    <p><img src="https://simg.open-open.com/show/e24735751d397bc66bb3794681a85667.png"></p>    <h2>4.2NIO阻塞形态</h2>    <p>为了更好地理解,我们先来写一下NIO <strong>在网络中是阻塞的状态代码</strong> ,随后看看非阻塞是怎么写的就更容易理解了。</p>    <ul>     <li><strong>是阻塞的就没有Selector选择器了</strong> ,就直接使用Channel和Buffer就完事了。</li>    </ul>    <p>客户端:</p>    <pre>  <code class="language-java">public class BlockClient {        public static void main(String[] args) throws IOException {            // 1. 获取通道          SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));            // 2. 发送一张图片给服务端吧          FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夹\\1.png"), StandardOpenOption.READ);            // 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢          ByteBuffer buffer = ByteBuffer.allocate(1024);            // 4.读取本地文件(图片),发送到服务器          while (fileChannel.read(buffer) != -1) {                // 在读之前都要切换成读模式              buffer.flip();                socketChannel.write(buffer);                // 读完切换成写模式,能让管道继续读取文件的数据              buffer.clear();          }            // 5. 关闭流          fileChannel.close();          socketChannel.close();      }  }</code></pre>    <p>服务端:</p>    <pre>  <code class="language-java">public class BlockServer {        public static void main(String[] args) throws IOException {            // 1.获取通道          ServerSocketChannel server = ServerSocketChannel.open();            // 2.得到文件通道,将客户端传递过来的图片写到本地项目下(写模式、没有则创建)          FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);            // 3. 绑定链接          server.bind(new InetSocketAddress(6666));            // 4. 获取客户端的连接(阻塞的)          SocketChannel client = server.accept();            // 5. 要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢          ByteBuffer buffer = ByteBuffer.allocate(1024);            // 6.将客户端传递过来的图片保存在本地中          while (client.read(buffer) != -1) {                // 在读之前都要切换成读模式              buffer.flip();                outChannel.write(buffer);                // 读完切换成写模式,能让管道继续读取文件的数据              buffer.clear();            }            // 7.关闭通道          outChannel.close();          client.close();          server.close();      }  }</code></pre>    <p>结果就可以将客户端传递过来的图片保存在本地了:</p>    <p><img src="https://simg.open-open.com/show/82dff70ea384dafd57c818e0b76c020f.png"></p>    <p>此时服务端保存完图片想要告诉客户端已经收到图片啦:</p>    <p><img src="https://simg.open-open.com/show/d1fb009e9170e8d1aea63a753dab5436.png"></p>    <p>客户端接收服务端带过来的数据:</p>    <p><img src="https://simg.open-open.com/show/15af62fc52d0b36c05f48d3873696ca5.png"></p>    <p>如果仅仅是上面的代码 <strong>是不行</strong> 的!这个程序会 <strong>阻塞</strong> 起来!</p>    <ul>     <li>因为服务端 <strong>不知道客户端还有没有数据要发过来</strong> (与刚开始不一样,客户端发完数据就将流关闭了,服务端可以知道客户端没数据发过来了),导致服务端一直在读取客户端发过来的数据。</li>     <li>进而导致了阻塞!</li>    </ul>    <p>于是客户端在写完数据给服务端时, <strong>显式告诉服务端已经发完数据</strong> 了!</p>    <p><img src="https://simg.open-open.com/show/59a37dbc5b8e789252a8450328167980.png"></p>    <h2>4.3NIO非阻塞形态</h2>    <p>如果使用非阻塞模式的话,那么我们就可以不显式告诉服务器已经发完数据了。我们下面来看看怎么写:</p>    <p>客户端:</p>    <pre>  <code class="language-java">public class NoBlockClient {        public static void main(String[] args) throws IOException {            // 1. 获取通道          SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));            // 1.1切换成非阻塞模式          socketChannel.configureBlocking(false);            // 2. 发送一张图片给服务端吧          FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夹\\1.png"), StandardOpenOption.READ);            // 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢          ByteBuffer buffer = ByteBuffer.allocate(1024);            // 4.读取本地文件(图片),发送到服务器          while (fileChannel.read(buffer) != -1) {                // 在读之前都要切换成读模式              buffer.flip();                socketChannel.write(buffer);                // 读完切换成写模式,能让管道继续读取文件的数据              buffer.clear();          }            // 5. 关闭流          fileChannel.close();          socketChannel.close();      }  }</code></pre>    <p>服务端:</p>    <pre>  <code class="language-java">public class NoBlockServer {        public static void main(String[] args) throws IOException {            // 1.获取通道          ServerSocketChannel server = ServerSocketChannel.open();            // 2.切换成非阻塞模式          server.configureBlocking(false);            // 3. 绑定连接          server.bind(new InetSocketAddress(6666));            // 4. 获取选择器          Selector selector = Selector.open();            // 4.1将通道注册到选择器上,指定接收“监听通道”事件          server.register(selector, SelectionKey.OP_ACCEPT);            // 5. 轮训地获取选择器上已“就绪”的事件--->只要select()>0,说明已就绪          while (selector.select() > 0) {              // 6. 获取当前选择器所有注册的“选择键”(已就绪的监听事件)              Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();                // 7. 获取已“就绪”的事件,(不同的事件做不同的事)              while (iterator.hasNext()) {                    SelectionKey selectionKey = iterator.next();                    // 接收事件就绪                  if (selectionKey.isAcceptable()) {                        // 8. 获取客户端的链接                      SocketChannel client = server.accept();                        // 8.1 切换成非阻塞状态                      client.configureBlocking(false);                        // 8.2 注册到选择器上-->拿到客户端的连接为了读取通道的数据(监听读就绪事件)                      client.register(selector, SelectionKey.OP_READ);                    } else if (selectionKey.isReadable()) { // 读事件就绪                        // 9. 获取当前选择器读就绪状态的通道                      SocketChannel client = (SocketChannel) selectionKey.channel();                        // 9.1读取数据                      ByteBuffer buffer = ByteBuffer.allocate(1024);                        // 9.2得到文件通道,将客户端传递过来的图片写到本地项目下(写模式、没有则创建)                      FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);                        while (client.read(buffer) > 0) {                          // 在读之前都要切换成读模式                          buffer.flip();                            outChannel.write(buffer);                            // 读完切换成写模式,能让管道继续读取文件的数据                          buffer.clear();                      }                  }                  // 10. 取消选择键(已经处理过的事件,就应该取消掉了)                  iterator.remove();              }          }        }  }</code></pre>    <p>还是刚才的需求: <strong>服务端保存了图片以后,告诉客户端已经收到图片了</strong> 。</p>    <p>在服务端上只要在后面写些数据给客户端就好了:</p>    <p><img src="https://simg.open-open.com/show/2f01c7379b2f8110126a83eb141e98e1.png"></p>    <p>在客户端上要想获取得到服务端的数据,也需要注册在register上(监听读事件)!</p>    <pre>  <code class="language-java">public class NoBlockClient2 {        public static void main(String[] args) throws IOException {            // 1. 获取通道          SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));            // 1.1切换成非阻塞模式          socketChannel.configureBlocking(false);            // 1.2获取选择器          Selector selector = Selector.open();            // 1.3将通道注册到选择器中,获取服务端返回的数据          socketChannel.register(selector, SelectionKey.OP_READ);            // 2. 发送一张图片给服务端吧          FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夹\\1.png"), StandardOpenOption.READ);            // 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢          ByteBuffer buffer = ByteBuffer.allocate(1024);            // 4.读取本地文件(图片),发送到服务器          while (fileChannel.read(buffer) != -1) {                // 在读之前都要切换成读模式              buffer.flip();                socketChannel.write(buffer);                // 读完切换成写模式,能让管道继续读取文件的数据              buffer.clear();          }              // 5. 轮训地获取选择器上已“就绪”的事件--->只要select()>0,说明已就绪          while (selector.select() > 0) {              // 6. 获取当前选择器所有注册的“选择键”(已就绪的监听事件)              Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();                // 7. 获取已“就绪”的事件,(不同的事件做不同的事)              while (iterator.hasNext()) {                    SelectionKey selectionKey = iterator.next();                    // 8. 读事件就绪                  if (selectionKey.isReadable()) {                        // 8.1得到对应的通道                      SocketChannel channel = (SocketChannel) selectionKey.channel();                        ByteBuffer responseBuffer = ByteBuffer.allocate(1024);                        // 9. 知道服务端要返回响应的数据给客户端,客户端在这里接收                      int readBytes = channel.read(responseBuffer);                        if (readBytes > 0) {                          // 切换读模式                          responseBuffer.flip();                          System.out.println(new String(responseBuffer.array(), 0, readBytes));                      }                  }                    // 10. 取消选择键(已经处理过的事件,就应该取消掉了)                  iterator.remove();              }          }      }      }</code></pre>    <p>测试结果:</p>    <p><img src="https://simg.open-open.com/show/d2cb8d570e928672e0e5080b1b0af37d.png"></p>    <p>下面就 <strong>简单总结一下</strong> 使用NIO时的要点:</p>    <ul>     <li>将Socket通道注册到Selector中,监听感兴趣的事件</li>     <li>当感兴趣的时间就绪时,则会进去我们处理的方法进行处理</li>     <li>每处理完一次就绪事件,删除该选择键(因为我们已经处理完了)</li>    </ul>    <h2>4.4管道和DataGramChannel</h2>    <p>这里我就不再讲述了,最难的TCP都讲了,UDP就很简单了。</p>    <p>UDP:</p>    <p><img src="https://simg.open-open.com/show/0f62a949fff3f78e5a7ad36b49a0c503.png"></p>    <p><img src="https://simg.open-open.com/show/8b3b4b872166937e18aad21f4dc2b840.png"></p>    <p>管道:</p>    <p><img src="https://simg.open-open.com/show/337440fb58873994126972b21f624913.png"></p>    <p><img src="https://simg.open-open.com/show/17b510a4dc1fb7478755dff3ca31a851.png"></p>    <h2>五、总结</h2>    <p>总的来说NIO也是一个比较重要的知识点,因为它是学习netty的基础~</p>    <p>想以一篇来完全讲解NIO显然是不可能的啦,想要更加深入了解NIO可以往下面的链接继续学习~</p>    <p>参考资料:</p>    <ul>     <li><a href="/misc/goto?guid=4959757835705345867" rel="nofollow,noindex">https://www.zhihu.com/question/29005375</a> ---如何学习Java的NIO?</li>     <li><a href="/misc/goto?guid=4959546162221366333" rel="nofollow,noindex">http://ifeve.com/java-nio-all/</a> ---Java NIO 系列教程</li>     <li><a href="/misc/goto?guid=4959757835813807720" rel="nofollow,noindex">https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html</a> -----NIO 入门</li>     <li><a href="/misc/goto?guid=4959757835901613535" rel="nofollow,noindex">https://blog.csdn.net/anxpp/article/details/51503329</a> -----Linux 网络 I/O 模型简介(图文)</li>     <li><a href="/misc/goto?guid=4959757835985037979" rel="nofollow,noindex">https://wangjingxin.top/2016/10/21/decoration/</a> -----谈谈java的NIO和AIO</li>     <li><a href="/misc/goto?guid=4959757836070158393" rel="nofollow,noindex">https://www.yiibai.com/java_nio/</a> -----Java NIO教程</li>     <li><a href="/misc/goto?guid=4959757836147937868" rel="nofollow,noindex">https://blog.csdn.net/cowthan/article/details/53563206</a> ------Java 8:Java 的新IO (nio)</li>     <li><a href="/misc/goto?guid=4959757836236177753" rel="nofollow,noindex">https://blog.csdn.net/youyou1543724847/article/details/52748785</a> -------JAVA NIO(1.基本概念,基本类)</li>     <li><a href="/misc/goto?guid=4959757836320976944" rel="nofollow,noindex">https://www.cnblogs.com/zingp/p/6863170.html</a> -----IO模式和IO多路复用</li>     <li><a href="/misc/goto?guid=4959757836390714740" rel="nofollow,noindex">https://www.cnblogs.com/Evsward/p/nio.html</a> ----掌握NIO,程序人生</li>     <li><a href="/misc/goto?guid=4959757836477863247" rel="nofollow,noindex">https://blog.csdn.net/anxpp/article/details/51512200</a> ----Java 网络IO编程总结(BIO、NIO、AIO均含完整实例代码)</li>     <li><a href="/misc/goto?guid=4959757836562564741" rel="nofollow,noindex">https://zhuanlan.zhihu.com/p/24393775?refer=hinus</a> ---进击的Java新人</li>    </ul>    <p>如果文章有错的地方欢迎指正,大家互相交流。</p>    <p>来自:http://zhongfucheng.bitcron.com/post/javaji-chu/jdk10du-fa-bu-liao-nioni-liao-jie-duo-shao</p>    <p> </p>