只有光头才能变强
回顾前面:html
原本我预想是先来回顾一下传统的IO模式的,将传统的IO模式的相关类理清楚(由于IO的类不少)。java
可是,发如今整理的过程已经有不少优秀的文章了,而我本身来整理的话可能达不到他们的水平。而且传统的IO估计你们都会用,而NIO就不必定了。编程
下面我就贴几张我认为整理比较优秀的思惟导图(下面会给出图片来源地址,你们可前往阅读):小程序
按操做方式分类结构图:微信小程序
字节流的输入和输出对照图:api
字符流的输入和输出对照图:数组
按操做对象分类结构图:缓存
上述图片原文地址,知乎做者@小明:安全
还有阅读传统IO源码的优秀文章:服务器
相信你们看完上面两个给出的连接+理解了包装模式就是这么简单啦,传统的IO应该就没什么事啦~~
而NIO对于我来讲能够说是挺陌生的,在当初学的时候是接触过的。可是一直没有用它,因此停留认知:nio是jdk1.4开始有的,比传统IO高级。
相信不少初学者都跟我同样,对NIO是不太了解的。而咱们如今jdk10都已经发布了,jdk1.4的nio都不知道,这有点说不过去了。
因此我花了几天去了解NIO的核心知识点,期间看了《Java 编程思想》和《疯狂Java 讲义》的nio模块。可是,会发现看完了以后仍是很迷,不知道NIO这是干吗用的,而网上的资料与书上的知识点没有很好地对应。
同步/异步/阻塞/非阻塞/多路复用
,而不一样的人又有不一样的理解方式。select/epoll/poll/pselect
,fd
这些关键字,没有相关基础的人看起来简直是天书我在找资料的过程当中也收藏了好多讲解NIO的资料,这篇文章就是以初学的角度来理解NIO。也算是我这两天看NIO的一个总结吧。
那么接下来就开始吧,若是文章有错误的地方请你们多多包涵,不吝在评论区指正哦~
声明:本文使用JDK1.8
JDK 1.4中的java.nio.*包
中引入新的Java I/O库,其目的是提升速度。实际上,“旧”的I/O包已经使用NIO从新实现过,即便咱们不显式的使用NIO编程,也能从中受益。
在《Java编程思想》读到“即便咱们不显式的使用NIO编程,也能从中受益”的时候,我是挺在乎的,因此:咱们测试一下使用NIO复制文件和传统IO复制文件的性能:
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时间"); } }
我分别测试了文件大小为13M,40M,200M的:
能够看到使用过NIO从新实现过的传统IO根本不虚,在大文件下效果还比NIO要好(固然了,我的几回的测试,或许不是很准)
那这意味着咱们能够不使用/学习NIO了吗?
答案是否认的,IO操做每每在两个场景下会用到:
NIO的魅力:在网络中使用IO就能够体现出来了!
首先咱们来看看IO和NIO的区别:
可简单认为:IO是面向流的处理,NIO是面向块(缓冲区)的处理
NIO主要有三个核心部分组成:
在NIO中并非以流的方式来处理数据的,而是以buffer缓冲区和Channel管道配合使用来处理数据。
简单理解一下:
而咱们的NIO就是经过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理!
要时刻记住:Channel不与数据打交道,它只负责运输数据。与数据打交道的是Buffer缓冲区
相对于传统IO而言,流是单向的。对于NIO而言,有了Channel管道这个概念,咱们的读写都是双向的(铁路上的火车能从广州去北京、天然就能从北京返还到广州)!
咱们来看看Buffer缓冲区有什么值得咱们注意的地方。
Buffer是缓冲区的抽象类:
其中ByteBuffer是用得最多的实现类(在管道中读写字节数据)。
拿到一个缓冲区咱们每每会作什么?很简单,就是读取缓冲区的数据/写数据到缓冲区中。因此,缓冲区的核心方法就是:
put()
get()
Buffer类维护了4个核心变量属性来提供关于其所包含的数组的信息。它们是:
容量Capacity
上界Limit
位置Position
get( )
和 put( )
函数更新。标记Mark
首先展现一下是如何建立缓冲区的,核心变量的值是怎么变化的。
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()); }
运行结果:
如今我想要从缓存区拿数据,怎么拿呀??NIO给了咱们一个flip()
方法。这个方法能够改动position和limit的位置!
仍是上面的代码,咱们flip()
一下后,再看看4个核心属性的值会发生什么变化:
很明显的是:
看到这里的同窗可能就会想到了:当调用完filp()
时:limit是限制读到哪里,而position是从哪里读
通常咱们称filp()
为“切换成读模式”
filp()
“切换成读模式”。切换成读模式以后,咱们就能够读取缓冲区的数据了:
// 建立一个limit()大小的字节数组(由于就只有limit这么多个数据可读) byte[] bytes = new byte[byteBuffer.limit()]; // 将读取的数据装进咱们的字节数组中 byteBuffer.get(bytes); // 输出数据 System.out.println(new String(bytes, 0, bytes.length));
随后输出一下核心变量的值看看:
读完咱们还想写数据到缓冲区,那就使用clear()
函数,这个函数会“清空”缓冲区:
Channel通道只负责传输数据、不直接操做数据的。操做数据都是经过Buffer缓冲区来进行操做!
// 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);
使用FileChannel配合缓冲区实现文件复制的功能:
使用内存映射文件的方式实现文件复制的功能(直接操做缓冲区):
通道之间经过transfer()
实现数据的传输(直接操做缓冲区):
使用直接缓冲区有两种方式:
map()
方法,将文件直接映射到内存中建立这个知识点我感受用得挺少的,不过不少教程都有说这个知识点,我也拿过来讲说吧:
分散读取
汇集写入
字符集(只要编码格式和解码格式一致,就没问题了)
文件的IO就告一段落了,咱们来学习网络中的IO~~~为了更好地理解NIO,咱们先来学习一下IO的模型~
根据UNIX网络编程对I/O模型的分类,在UNIX能够概括成5种I/O模型:
Linux 的内核将全部外部设备都看作一个文件来操做,对一个文件的读写操做会调用内核提供的系统命令(api),返回一个file descriptor
(fd,文件描述符)。而对一个socket的读写也会有响应的描述符,称为socket fd
(socket文件描述符),描述符就是一个数字,指向内核中的一个结构体(文件路径,数据区等一些属性)。
为了保证用户进程不能直接操做内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分
咱们来看看IO在系统中的运行是怎么样的(咱们以read为例)
能够发现的是:当应用程序调用read方法时,是须要等待的--->从内核空间中找数据,再将内核空间的数据拷贝到用户空间的。
下面只讲解用得最多的3个I/0模型:
在进程(用户)空间中调用recvfrom
,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间一直等待。
recvfrom
从应用层到内核的时候,若是没有数据就直接返回一个EWOULDBLOCK错误,通常都对非阻塞I/O模型进行轮询检查这个状态,看内核是否是有数据到来。
前面也已经说了:在Linux下对文件的操做是利用文件描述符(file descriptor)来实现的。
在Linux下它是这样子实现I/O复用模型的:
select/poll/epoll/pselect
其中一个函数,传入多个文件描述符,若是有一个文件描述符就绪,则返回,不然阻塞直到超时。好比poll()
函数是这样子的:int poll(struct pollfd *fds,nfds_t nfds, int timeout);
其中 pollfd
结构定义以下:
struct pollfd { int fd; /* 文件描述符 */ short events; /* 等待的事件 */ short revents; /* 实际发生了的事件 */ };
select/epoll的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。
正经的描述都在上面给出了,不知道你们理解了没有。下面我举几个例子总结一下这三种模型:
阻塞I/O:
Java3y跟女友去买喜茶,排了好久的队终于能够点饮料了。我要绿研,谢谢。但是喜茶不是点了单就能当即拿,因而我在喜茶门口等了一小时才拿到绿研。
非阻塞I/O:
Java3y跟女友去买一点点,排了好久的队终于能够点饮料了。我要波霸奶茶,谢谢。但是一点点不是点了单就能当即拿,同时服务员告诉我:你大概要等半小时哦。大家先去逛逛吧~因而Java3y跟女友去玩了几把斗地主,感受时间差很少了。因而又去一点点问:请问到我了吗?个人单号是xxx。服务员告诉Java3y:还没到呢,如今的单号是XXX,你还要等一会,能够去附近耍耍。问了好几回后,终于拿到个人波霸奶茶了。
I/O复用模型:
Java3y跟女友去麦当劳吃汉堡包,如今就厉害了可使用微信小程序点餐了。因而跟女友找了个地方坐下就用小程序点餐了。点餐了以后玩玩斗地主、聊聊天什么的。时不时听到广播在复述XXX请取餐,反正个人单号还没到,就继续玩呗。~~等听到广播的时候再取餐就是了。时间过得挺快的,此时传来:Java3y请过来取餐。因而我就能拿到个人麦辣鸡翅汉堡了。
回到咱们最开始的图:
NIO被叫为 no-blocking io
,实际上是在网络这个层次中理解的,对于FileChannel来讲同样是阻塞。
咱们前面也仅仅讲解了FileChannel,对于咱们网络通讯是还有几个Channel的~
因此说:咱们一般使用NIO是在网络中使用的,网上大部分讨论NIO都是在网络通讯的基础之上的!说NIO是非阻塞的NIO也是网络中体现的!
从上面的图咱们能够发现还有一个Selector
选择器这么一个东东。从一开始咱们就说过了,nio的核心要素有:
咱们在网络中使用NIO每每是I/O模型的多路复用模型!
为了更好地理解,咱们先来写一下NIO在网络中是阻塞的状态代码,随后看看非阻塞是怎么写的就更容易理解了。
客户端:
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(); } }
服务端:
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(); } }
结果就能够将客户端传递过来的图片保存在本地了:
此时服务端保存完图片想要告诉客户端已经收到图片啦:
客户端接收服务端带过来的数据:
若是仅仅是上面的代码是不行的!这个程序会阻塞起来!
因而客户端在写完数据给服务端时,显式告诉服务端已经发完数据了!
若是使用非阻塞模式的话,那么咱们就能够不显式告诉服务器已经发完数据了。咱们下面来看看怎么写:
客户端:
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(); } }
服务端:
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(); } } } }
仍是刚才的需求:服务端保存了图片之后,告诉客户端已经收到图片了。
在服务端上只要在后面写些数据给客户端就行了:
在客户端上要想获取获得服务端的数据,也须要注册在register上(监听读事件)!
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(); } } } }
测试结果:
下面就简单总结一下使用NIO时的要点:
这里我就再也不讲述了,最难的TCP都讲了,UDP就很简单了。
UDP:
管道:
总的来讲NIO也是一个比较重要的知识点,由于它是学习netty的基础~
想以一篇来彻底讲解NIO显然是不可能的啦,想要更加深刻了解NIO能够往下面的连接继续学习~
参考资料:
若是文章有错的地方欢迎指正,你们互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同窗,能够 关注微信公众号:Java3y。为了你们方便,刚新建了一下 qq群:742919422,你们也能够去交流交流。谢谢支持了!但愿能多介绍给其余有须要的朋友
文章的目录导航: