NIO教程 ——检视阅读java
参考linux
BIO,NIO,AIO 总结程序员
Java NIO浅析sql
Java NIO 教程——极客,蓝本数据库
Java NIO 系列教程 ——并发编程网编程
BIO,NIO——知乎数组
NIO 入门——IBM缓存
Java NIO教程 ——易百服务器
Java NIO Tutorial英文版网络
首先Java中的IO有如下三种: BIO(Blocking IO) 同步式阻塞IO NIO(Non-BlockingIO/New IO) 同步式非阻塞IO JDK1.4提供 AIO(AsynchronousIO) 异步式非阻塞IO JDK1.8提供
略读:
ibm
NIO 的建立目的是为了让 Java 程序员能够实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操做(即填充和提取缓冲区)转移回操做系统,于是能够极大地提升速度。
原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的,原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
通道和 缓冲区是 NIO 中的核心对象,几乎在每个 I/O 操做中都要使用它们。
通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的全部数据都必须经过一个 Channel 对象。一个 Buffer 实质上是一个容器对象。发送给一个通道的全部对象都必须首先放到缓冲区中;一样地,从通道中读取的任何数据都要读到缓冲区中。
Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream 对象中 。
缓冲区实质上是一个数组。一般它是一个字节数组,可是也能够使用其余种类的数组。可是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,并且还能够跟踪系统的读/写进程。
Channel是一个对象,能够经过它读取和写入数据。拿 NIO 与原来的 I/O 作个比较,通道就像是流。
通道与流的不一样之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道能够用于读、写或者同时用于读写。
由于它们是双向的,因此通道能够比流更好地反映底层操做系统的真实状况。特别是在 UNIX 模型中,底层操做系统通道是双向的。
在 NIO 系统中,任什么时候候执行一个读操做,您都是从通道中读取,可是您不是 直接 从通道读取。由于全部数据最终都驻留在缓冲区中,因此您是从通道读到缓冲区中。
所以读取文件涉及三个步骤:(1) 从 FileInputStream 获取 Channel,(2) 建立 Buffer,(3) 将数据从 Channel 读到 Buffer中。
clear() 方法重设缓冲区,使它能够接受读入的数据。 flip() 方法让缓冲区能够将新读入的数据写入另外一个通道。
flip
如今咱们要将数据写到输出通道中。在这以前,咱们必须调用 flip() 方法。这个方法作两件很是重要的事:
clear
最后一步是调用缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。 Clear 作两种很是重要的事情:
read() 和 write() 调用获得了极大的简化,由于许多工做细节都由缓冲区完成了。 clear() 和 flip() 方法用于让缓冲区在读和写之间切换。
建立不一样类型的缓冲区以达到不一样的目的,如可保护数据不被修改的 只读 缓冲区,和直接映射到底层操做系统缓冲区的 直接 缓冲区。
使用静态方法 allocate() 来分配缓冲区:
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
将一个现有的数组转换为缓冲区,以下所示:
byte array[] = new byte[1024];``ByteBuffer buffer = ByteBuffer.wrap( array );
本例使用了 wrap() 方法将一个数组包装为缓冲区。必须很是当心地进行这类操做。一旦完成包装,底层数据就能够经过缓冲区或者直接访问。
建立一个包含槽 3 到槽 6 的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个窗口 。
窗口的起始和结束位置经过设置 position 和 limit 值来指定,而后调用 Buffer 的 slice() 方法:
buffer.position( 3 );buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
片是缓冲区的 子缓冲区。不过, 片断和 缓冲区共享同一个底层数据数组
只读缓冲区很是简单 ― 您能够读取它们,可是不能向它们写入。能够经过调用缓冲区的 asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区彻底相同的缓冲区(并与其共享数据),只不过它是只读的。
只读缓冲区对于保护数据颇有用。在将缓冲区传递给某个对象的方法时,您没法知道这个方法是否会修改缓冲区中的数据。建立一个只读的缓冲区能够 保证 该缓冲区不会被修改。
不能将只读的缓冲区转换为可写的缓冲区。
//直接缓冲区 ByteBuffer buffer = ByteBuffer.allocateDirect( 1024 );
分散/汇集 I/O
通道能够有选择地实现两个新的接口: ScatteringByteChannel 和 GatheringByteChannel。一个 ScatteringByteChannel是一个具备两个附加读方法的通道:
这些 long read() 方法很像标准的 read 方法,只不过它们不是取单个缓冲区而是取一个缓冲区数组。缓冲区数组就像一个大缓冲区。
以socket.read()为例子:
传统的BIO里面socket.read(),若是TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
meituan
对于NIO,若是TCP RecvBuffer有数据,就把数据从网卡读到内存,而且返回给用户;反之则直接返回0,永远不会阻塞。
最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。
换句话说,BIO里用户最关心“我要读”,NIO里用户最关心”我能够读了”,在AIO模型里用户更须要关注的是“读完了”。
NIO一个重要的特色是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操做是同步阻塞的(消耗CPU但性能很是高)。
回忆BIO模型,之因此须要多线程,是由于在进行I/O操做的时候,一是没有办法知道到底能不能写、能不能读,只能”傻等”,即便经过各类估算,算出来操做系统没有能力进行读写,也无法在socket.read()和socket.write()函数中返回,这两个函数没法进行有效的中断。因此除了多开线程另起炉灶,没有好的办法利用CPU。
NIO的读写函数能够马上返回,这就给了咱们不开线程利用CPU的最好机会:若是一个链接不能读写(socket.read()返回0或者socket.write()返回0),咱们能够把这件事记下来,记录的方式一般是在Selector上注册标记位,而后切换到其它就绪的链接(channel)继续进行读写。
NIO的主要事件有几个:读就绪、写就绪、有新链接到来。
仔细分析一下咱们须要的线程,其实主要包括如下几种: 1. 事件分发器,单线程选择就绪的事件。 2. I/O处理器,包括connect、read、write等,这种纯CPU操做,通常开启CPU核心个线程就能够。 3. 业务线程,在处理完I/O后,业务通常还会有本身的业务逻辑,有的还会有其余的阻塞I/O,如DB操做,RPC等。只要有阻塞,就须要单独的线程。
NIO给咱们带来了些什么:
BIO,NIO,AIO 总结
如何区分 “同步/异步 ”和 “阻塞/非阻塞” 呢?
同步/异步是从行为角度描述事物的,而阻塞和非阻塞描述的当前事物的状态(等待调用结果时的状态)。
阻塞模式使用就像传统中的支持同样,比较简单,可是性能和可靠性都很差;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,能够使用同步阻塞I/O来提高开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·能够将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,仍是从流读到缓冲区,而 NIO 倒是直接读到 Buffer 中进行操做。
NIO 经过Channel(通道) 进行读写。
通道是双向的,可读也可写,而流的读写是单向的。不管读写,通道只能和Buffer交互。由于 Buffer,通道能够异步地读写。
NIO有选择器,而IO没有。
选择器用于使用单个线程处理多个通道。所以,它须要较少的线程来处理这些通道。线程之间的切换对于操做系统来讲是昂贵的。 所以,为了提升系统效率选择器是有用的。
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操做以后会直接返回,不会堵塞在那里,当后台处理完成,操做系统会通知相应的线程进行后续的操做。
每当要从缓存区的时候读取数据时,就调用filp()“切换成读模式”。
读完咱们还想写数据到缓冲区,那就使用clear()函数,这个函数会“清空”缓冲区 。
简介
NIO中的N能够理解为Non-blocking ,不单纯是New 。
不一样点:
概览
NIO包含下面3个核心的组件,Channel,Buffer和Selector组成了这个核心的API:
一般来讲NIO中的全部IO都是从Channel开始的。Channel和流有点相似。经过Channel,咱们便可以从Channel把数据写到Buffer中,也能够把数据冲Buffer写入到Channel 。
有不少的Channel,Buffer类型。下面列举了主要的几种:
正如你看到的,这些channel基于于UDP和TCP的网络IO,以及文件IO。 和这些类一块儿的还有其余一些比较有趣的接口,在本节中暂时很少介绍。为了简洁起见,咱们会在必要的时候引入这些概念。 下面是核心的Buffer实现类的列表:
这些Buffer涵盖了能够经过IO操做的基础类型:byte,short,int,long,float,double以及characters. NIO实际上还包含一种MappedBytesBuffer,通常用于和内存映射的文件。
选择器容许单线程操做多个通道。若是你的程序中有大量的连接,同时每一个连接的IO带宽不高的话,这个特性将会很是有帮助。好比聊天服务器。 下面是一个单线程中Slector维护3个Channel的示意图:
要使用Selector的话,咱们必须把Channel注册到Selector上,而后就能够调用Selector的select()方法。这个方法会进入阻塞,直到有一个channel的状态符合条件。当方法返回后,线程能够处理这些事件。
Java NIO Channel通道
Java NIO Channel通道和流很是类似,主要有如下3点区别:
Channel的实现
下面列出Java NIO中最重要的集中Channel的实现:
FileChannel用于文件的数据读写。 DatagramChannel用于UDP的数据读写。 SocketChannel用于TCP的数据读写。 ServerSocketChannel容许咱们监听TCP连接请求,每一个请求会建立会一个SocketChannel.
RandomAccessFile扩展:
RandomAccessFile(随机访问文件)类。该类是Java语言中功能最为丰富的文件访问类 。RandomAccessFile类支持“随机访问”方式,这里“随机”是指能够跳转到文件的任意位置处读写数据。在访问一个文件的时候,没必要把文件从头读到尾,而是但愿像访问一个数据库同样“为所欲为”地访问一个文件的某个部分,这时使用RandomAccessFile类就是最佳选择。
四种模式:R RW RWD RWS
r 以只读的方式打开文本,也就意味着不能用write来操做文件
rw 读操做和写操做都是容许的
rws 每当进行写操做,同步的刷新到磁盘,刷新内容和元数据
rwd 每当进行写操做,同步的刷新到磁盘,刷新内容
RandomAccessFile的用处:
一、大型文本日志类文件的快速定位获取数据:
得益于seek的巧妙设计,我认为咱们能够从超大的文本中快速定位咱们的游标,例如每次存日志的时候,咱们能够创建一个索引缓存,索引是日志的起始日期,value是文本的poiniter 也就是光标,这样咱们能够快速定位某一个时间段的文本内容
二、并发读写
也是得益于seek的设计,我认为多线程能够轮流操做seek控制光标的位置,从未达到不一样线程的并发写操做。
三、更方便的获取二进制文件
经过自带的读写转码(readDouble、writeLong等),我认为能够快速的完成字节码到字符的转换功能,对使用者来讲比较友好。
RandomAccessFile参考
实例:
public class FileChannelTest { public static void main(String[] args) throws IOException { RandomAccessFile file = new RandomAccessFile("D:\\text\\1_loan.sql", "r"); //mode只有4中,若是不是读写的mode或者给的不是4种中的,就会报错。 RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r"); try { FileChannel fileChannel = file.getChannel(); FileChannel copyFileChannel = copyFile.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int read = fileChannel.read(byteBuffer); while (read != -1) { System.out.println("read:" + read); //byteBuffer缓冲区切换为读模式 byteBuffer.flip(); copyFileChannel.write(byteBuffer); //“清空”byteBuffer缓冲区,以知足后续写入操做 byteBuffer.clear(); //注意,每次读时都要返回读后的状态read值赋值给循环判断体read,不然会陷入死循环true read = fileChannel.read(byteBuffer); } } finally { file.close(); copyFile.close(); } } }
报错:
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "w"); //由于没有"w"的mode Exception in thread "main" java.lang.IllegalArgumentException: Illegal mode "w" must be one of "r", "rw", "rws", or "rwd" at java.io.RandomAccessFile.<init>(RandomAccessFile.java:221) RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r"); //由于没有"w"的权限 Exception in thread "main" java.nio.channels.NonWritableChannelException at sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:194) at com.niotest.FileChannelTest.main(FileChannelTest.java:33)
NIO Buffer缓冲区
Java NIO Buffers用于和NIO Channel交互。正如你已经知道的,咱们从channel中读取数据到buffers里,从buffer把数据写入到channels.
buffer本质上就是一块内存区,能够用来写入数据,并在稍后读取出来。这块内存被NIO Buffer包裹起来,对外提供一系列的读写方便开发的接口。
Buffer基本用法
利用Buffer读写数据,一般遵循四个步骤:
当写入数据到buffer中时,buffer会记录已经写入的数据大小。当须要读数据时,经过flip()方法把buffer从写模式调整为读模式;在读模式下,能够读取全部已经写入的数据。
当读取完数据后,须要清空buffer,以知足后续写入操做。清空buffer有两种方式:调用clear()或compact()方法。clear会清空整个buffer,compact则只清空已读取的数据,未被读取的数据会被移动到buffer的开始位置,写入位置则近跟着未读数据以后。
Buffer的容量,位置,上限(Buffer Capacity, Position and Limit)
buffer缓冲区实质上就是一块内存,用于写入数据,也供后续再次读取数据。这块内存被NIO Buffer管理,并提供一系列的方法用于更简单的操做这块内存。
一个Buffer有三个属性是必须掌握的,分别是:
position和limit的具体含义取决于当前buffer的模式。capacity在两种模式下都表示容量。
下面有张示例图,描诉了不一样模式下position和limit的含义:
容量(Capacity)
做为一块内存,buffer有一个固定的大小,叫作capacity容量。也就是最多只能写入容量值的字节,整形等数据。一旦buffer写满了就须要清空已读数据以便下次继续写入新的数据。
位置(Position)
当写入数据到Buffer的时候须要中一个肯定的位置开始,默认初始化时这个位置position为0,一旦写入了数据好比一个字节,整形数据,那么position的值就会指向数据以后的一个单元,position最大能够到capacity-1.
当从Buffer读取数据时,也须要从一个肯定的位置开始。buffer从写入模式变为读取模式时,position会归零,每次读取后,position向后移动。
上限(Limit)
在写模式,limit的含义是咱们所能写入的最大数据量。它等同于buffer的容量。
一旦切换到读模式,limit则表明咱们所能读取的最大数据量,他的值等同于写模式下position的位置。
数据读取的上限时buffer中已有的数据,也就是limit的位置(原写模式下position所指的位置)。
Buffer Types
Java NIO有以下具体的Buffer类型:
正如你看到的,Buffer的类型表明了不一样数据类型,换句话说,Buffer中的数据能够是上述的基本类型;
分配一个Buffer(Allocating a Buffer)
为了获取一个Buffer对象,你必须先分配。每一个Buffer实现类都有一个allocate()方法用于分配内存。
ByteBuffer byteBuffer = ByteBuffer.allocate(1024); CharBuffer charBuffer = CharBuffer.allocate(48);
写入数据到Buffer(Writing Data to a Buffer)
写数据到Buffer有两种方法:
从Channel中写数据到Buffer
手动写数据到Buffer,调用put方法
//从Channel中写数据到Buffer
int read = fileChannel.read(byteBuffer);
//调用put方法写
buf.put(3);
//把数据写到特定的位置
public ByteBuffer put(int i, byte x);
//把一个具体类型数据写入buffer
public ByteBuffer putInt(int x);
flip()——翻转
flip()方法能够把Buffer从写模式切换到读模式。调用flip方法会把position归零,并设置limit为以前的position的值。 也就是说,如今position表明的是读取位置,limit标示的是已写入的数据位置。
从Buffer读取数据(Reading Data from a Buffer)
冲Buffer读数据也有两种方式。
从buffer读数据到channel。
从buffer直接读取数据,调用get方法。
//读取数据到channel的例子:
int bytesWritten = inChannel.write(buf);
//调用get读取数据的例子:
byte aByte = buf.get();
rewind()——倒带
Buffer.rewind()方法将position置为0,这样咱们能够重复读取buffer中的数据。limit保持不变。
clear() and compact()
一旦咱们从buffer中读取完数据,须要复用buffer为下次写数据作准备。只须要调用clear或compact方法。
clear方法会重置position为0,limit为capacity,也就是整个Buffer清空。实际上Buffer中数据并无清空,咱们只是把标记为修改了。(从新写入的时候这些存在的数据就会被新的数据覆盖)
若是Buffer还有一些数据没有读取完,调用clear就会致使这部分数据被“遗忘”,由于咱们没有标记这部分数据未读。
针对这种状况,若是须要保留未读数据,那么能够使用compact()。 所以compact()和clear()的区别就在于对未读数据的处理,是保留这部分数据仍是一块儿清空。
mark() and reset()
经过mark方法能够标记当前的position,经过reset来恢复mark的位置,这个很是像canva的save和restore:
buffer.mark(); //call buffer.get() a couple of times, e.g. during parsing. buffer.reset(); //set position back to mark.
equals() and compareTo()
能够用eqauls和compareTo比较两个buffer
equals()
判断两个buffer相对,需知足:
从上面的三个条件能够看出,equals只比较buffer中的部份内容,并不会去比较每个元素。
compareTo()
compareTo也是比较buffer中的剩余元素,只不过这个方法适用于比较排序的:
NIO Scatter (分散)/ Gather(汇集)
——分散读和汇集写的场景。
Java NIO发布时内置了对scatter / gather的支持。scatter / gather是经过通道读写数据的两个概念。
Scattering read指的是从通道读取的操做能把数据写入多个buffer,也就是scatters表明了数据从一个channel到多个buffer的过程。
gathering write则正好相反,表示的是从多个buffer把数据写入到一个channel中。
Scatter/gather在有些场景下会很是有用,好比须要处理多份分开传输的数据。举例来讲,假设一个消息包含了header和body,咱们可能会把header和body保存在不一样独立buffer中,这种分开处理header与body的作法会使开发更简明。
Scattering Reads
"scattering read"是把数据从单个Channel写入到多个buffer,下面是示意图:
观察代码能够发现,咱们把多个buffer写在了一个数组中,而后把数组传递给channel.read()方法。read()方法内部会负责把数据按顺序写进传入的buffer数组内。一个buffer写满后,接着写到下一个buffer中。
实际上,scattering read内部必须写满一个buffer后才会向后移动到下一个buffer,所以这并不适合消息大小会动态改变的部分,也就是说,若是你有一个header和body,而且header有一个固定的大小(好比128字节),这种情形下能够正常工做。
athering Writes
"gathering write"把多个buffer的数据写入到同一个channel中.
似的传入一个buffer数组给write,内部机会按顺序将数组内的内容写进channel,这里须要注意,写入的时候针对的是buffer中position到limit之间的数据。也就是若是buffer的容量是128字节,但它只包含了58字节数据,那么写入的时候只有58字节会真正写入。所以gathering write是能够适用于可变大小的message的,这和scattering reads不一样。
NIO Channel to Channel Transfers通道传输接口
在Java NIO中若是一个channel是FileChannel类型的,那么他能够直接把数据传输到另外一个channel。这个特性得益于FileChannel包含的transferTo和transferFrom两个方法。
transferFrom()——目标channel用,参数为源数据channel。
transferFrom的参数position和count表示目标文件的写入位置和最多写入的数据量。若是通道源的数据小于count那么就传实际有的数据量。 另外,有些SocketChannel的实如今传输时只会传输哪些处于就绪状态的数据,即便SocketChannel后续会有更多可用数据。所以,这个传输过程可能不会传输整个的数据。
transferTo()——源数据用,参数为目标channel
SocketChannel的问题也存在于transferTo.SocketChannel的实现可能只在发送的buffer填充满后才发送,并结束。
实例:
public class ChannelTransferTest { public static void main(String[] args) throws IOException { RandomAccessFile fromfile = new RandomAccessFile("D:\\text\\1_loan.sql", "rw"); //mode只有4中,若是不是读写的mode或者给的不是4种中的,就会报错。 RandomAccessFile toFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "rw"); FileChannel fromfileChannel = fromfile.getChannel(); FileChannel toFileChannel = toFile.getChannel(); //==========================transferTo================================= //transferTo方法把fromfileChannel数据传输到另外一个toFileChannel //long transferSize = fromfileChannel.transferTo(0, fromfileChannel.size(), toFileChannel); //System.out.println(transferSize); //=============================transferFrom============================== //把数据从通道源传输到toFileChannel,相比经过buffer读写更加的便捷 long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()); //参数position和count表示目标文件的写入位置和最多写入的数据量 //long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()-1000); //若是通道源的数据小于count那么就传实际有的数据量。 //long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()+1000); System.out.println(transferSize1); } }
NIO Selector选择器
Selector是Java NIO中的一个组件,用于检查一个或多个NIO Channel的状态是否处于可读、可写。如此能够实现单线程管理多个channels,也就是能够管理多个网络连接。
为何使用Selector
用单线程处理多个channels的好处是我须要更少的线程来处理channel。实际上,你甚至能够用一个线程来处理全部的channels。从操做系统的角度来看,切换线程开销是比较昂贵的,而且每一个线程都须要占用系统资源,所以暂用线程越少越好。
须要留意的是,现代操做系统和CPU在多任务处理上已经变得愈来愈好,因此多线程带来的影响也愈来愈小。若是一个CPU是多核的,若是不执行多任务反而是浪费了机器的性能。不过这些设计讨论是另外的话题了。简而言之,经过Selector咱们能够实现单线程操做多个channel。
建立Selector
建立一个Selector能够经过Selector.open()方法:
Selector selector = Selector.open();
注册Channel到Selector上
先把Channel注册到Selector上,这个操做使用SelectableChannel的register()。SocketChannel等都有继承此抽象类。
channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Channel必须是非阻塞的。因此FileChannel不适用Selector,由于FileChannel不能切换为非阻塞模式。Socket channel能够正常使用。
注意register的第二个参数,这个参数是一个“关注集合”,表明咱们关注的channel状态,有四种基础类型可供监听:
一个channel触发了一个事件也可视做该事件处于就绪状态。所以当channel与server链接成功后,那么就是“链接就绪”状态。server channel接收请求链接时处于“可链接就绪”状态。channel有数据可读时处于“读就绪”状态。channel能够进行数据写入时处于“写就绪”状态。
上述的四种就绪状态用SelectionKey中的常量表示以下:
若是对多个事件感兴趣可利用位的或运算结合多个常量,好比:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey's
在上一小节中,咱们利用register方法把Channel注册到了Selectors上,这个方法的返回值是SelectionKeys,这个返回的对象包含了一些比较有价值的属性:
Interest Set
这个“关注集合”实际上就是咱们但愿处理的事件的集合,它的值就是注册时传入的参数,咱们能够用按为与运算把每一个事件取出来:
int interestSet = selectionKey.interestOps(); boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
Ready Set
"就绪集合"中的值是当前channel处于就绪的值,通常来讲在调用了select方法后都会须要用到就绪状态
int readySet = selectionKey.readyOps();
从“就绪集合”中取值的操做相似于“关注集合”的操做,固然还有更简单的方法,SelectionKey提供了一系列返回值为boolean的的方法:
selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
Channel + Selector
从SelectionKey操做Channel和Selector很是简单:
Channel channel = selectionKey.channel(); Selector selector = selectionKey.selector();
Attaching Objects
咱们能够给一个SelectionKey附加一个Object,这样作一方面能够方便咱们识别某个特定的channel,同时也增长了channel相关的附加信息。例如,能够把用于channel的buffer附加到SelectionKey上:
selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment();
附加对象的操做也能够在register的时候就执行:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
从Selector中选择channel
一旦咱们向Selector注册了一个或多个channel后,就能够调用select来获取channel。select方法会返回全部处于就绪状态的channel。 select方法具体以下:
select()方法在返回channel以前处于阻塞状态。 select(long timeout)和select作的事同样,不过他的阻塞有一个超时限制。
selectNow()不会阻塞,根据当前状态马上返回合适的channel。
select()方法的返回值是一个int整形,表明有多少channel处于就绪了。也就是自上一次select后有多少channel进入就绪。举例来讲,假设第一次调用select时正好有一个channel就绪,那么返回值是1,而且对这个channel作任何处理,接着再次调用select,此时刚好又有一个新的channel就绪,那么返回值仍是1,如今咱们一共有两个channel处于就绪,可是在每次调用select时只有一个channel是就绪的。
selectedKeys()
在调用select并返回了有channel就绪以后,能够经过选中的key集合来获取channel,这个操做经过调用selectedKeys()方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
还记得在register时的操做吧,咱们register后的返回值就是SelectionKey实例,也就是咱们如今经过selectedKeys()方法所返回的SelectionKey。
遍历这些SelectionKey能够经过以下方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); }
上述循环会迭代key集合,针对每一个key咱们单独判断他是处于何种就绪状态。
注意:keyIterater.remove()方法的调用,Selector自己并不会移除SelectionKey对象,这个操做须要咱们手动执行。当下次channel处于就绪是,Selector任然会吧这些key再次加入进来。
SelectionKey.channel返回的channel实例须要强转为咱们实际使用的具体的channel类型,例如ServerSocketChannel或SocketChannel.
wakeUp()
因为调用select而被阻塞的线程,能够经过调用Selector.wakeup()来唤醒即使此时已然没有channel处于就绪状态。具体操做是,在另一个线程调用wakeup,被阻塞与select方法的线程就会马上返回。
close()
当操做Selector完毕后,须要调用close方法。close的调用会关闭Selector并使相关的SelectionKey都无效。channel自己不会被关闭。
示例:首先打开一个Selector,而后注册channel,最后监听Selector的状态。
public class NIOServer { 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_loan.sql"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); while (client.read(buffer) > 0) { // 在读以前都要切换成读模式 buffer.flip(); outChannel.write(buffer); // 读完切换成写模式,能让管道继续读取文件的数据 buffer.clear(); } ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put("yeah,i know,i got your message!".getBytes()); byteBuffer.flip(); client.write(byteBuffer); } // 10. 取消选择键(已经处理过的事件,就应该取消掉了) iterator.remove(); } } } } public class NIOClientTwo { 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("D:\\text\\1_loan.sql"), 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 FileChannel文件通道
Java NIO中的FileChannel是用于链接文件的通道。经过文件通道能够读、写文件的数据。Java NIO的FileChannel是相对标准Java IO API的可选接口。
FileChannel不能够设置为非阻塞模式,他只能在阻塞模式下运行。
打开文件通道
在使用FileChannel前必须打开通道,打开一个文件通道须要经过输入/输出流或者RandomAccessFile,下面是经过RandomAccessFile打开文件通道的案例:
RandomAccessFile aFile = new RandomAccessFile("D:\text\1_loan.sql", "rw"); FileChannel inChannel = aFile.getChannel();
从文件通道内读取数据
读取文件通道的数据能够经过read方法:
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf);
首先开辟一个Buffer,从通道中读取的数据会写入Buffer内。接着就能够调用read方法,read的返回值表明有多少字节被写入了Buffer,返回-1则表示已经读取到文件结尾了。
向文件通道写入数据
写数据用write方法,入参是Buffer:
String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); while(buf.hasRemaining()) { channel.write(buf); }
注意这里的write调用写在了wihle循环汇总,这是由于write不能保证有多少数据真实被写入,所以须要循环写入直到没有更多数据。
关闭通道
操做完毕后,须要把通道关闭:
channel.close();
FileChannel Position
当操做FileChannel的时候读和写都是基于特定起始位置的(position),获取当前的位置能够用FileChannel的position()方法,设置当前位置能够用带参数的position(long pos)方法。
//获取当前的位置 long position = fileChannel.position(); //设置当前位置为pos +123 fileChannel.position(pos +123);
假设咱们把当前位置设置为文件结尾以后,那么当咱们视图从通道中读取数据时就会发现返回值是-1,表示已经到达文件结尾了。 若是把当前位置设置为文件结尾以后,再向通道中写入数据,文件会自动扩展以便写入数据,可是这样会致使文件中出现相似空洞,即文件的一些位置是没有数据的。
FileChannel Size
size()方法能够返回FileChannel对应的文件的文件大小:
long fileSize = channel.size();
FileChannel Truncate
利用truncate方法能够截取指定长度的文件:
FileChannel truncateFile = fileChannel.truncate(1024);
FileChannel Force
force方法会把全部未写磁盘的数据都强制写入磁盘。这是由于在操做系统中出于性能考虑回把数据放入缓冲区,因此不能保证数据在调用write写入文件通道后就及时写到磁盘上了,除非手动调用force方法。 force方法须要一个布尔参数,表明是否把meta data也一并强制写入。
channel.force(true);
NIO SocketChannel套接字通道
在Java NIO体系中,SocketChannel是用于TCP网络链接的套接字接口,至关于Java网络编程中的Socket套接字接口。建立SocketChannel主要有两种方式,以下:
创建一个SocketChannel链接
打开一个SocketChannel能够这样操做:
SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("http://www.google.com", 80));
关闭一个SocketChannel链接
关闭一个SocketChannel只须要调用他的close方法,以下:
socketChannel.close();
从SocketChannel中读数据
从一个SocketChannel链接中读取数据,能够经过read()方法,以下:
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = socketChannel.read(buf);
首先须要开辟一个Buffer。从SocketChannel中读取的数据将放到Buffer中。
接下来就是调用SocketChannel的read()方法.这个read()会把通道中的数据读到Buffer中。read()方法的返回值是一个int数据,表明这次有多少字节的数据被写入了Buffer中。若是返回的是-1,那么意味着通道内的数据已经读取完毕,到底了(连接关闭)。
向SocketChannel写数据
向SocketChannel中写入数据是经过write()方法,write也须要一个Buffer做为参数。下面看一下具体的示例:
String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); while(buf.hasRemaining()) { channel.write(buf); }
非阻塞模式
咱们能够把SocketChannel设置为non-blocking(非阻塞)模式。这样的话在调用connect(), read(), write()时都是异步的。
socketChannel.configureBlocking(false);
connect()
若是咱们设置了一个SocketChannel是非阻塞的,那么调用connect()后,方法会在连接创建前就直接返回。为了检查当前连接是否创建成功,咱们能够调用finishConnect(),以下:
socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("http://www.google.com", 80)); while(! socketChannel.finishConnect() ){ //wait, or do something else... }
write()
在非阻塞模式下,调用write()方法不能确保方法返回后写入操做必定获得了执行。所以咱们须要把write()调用放到循环内。这和前面在讲write()时是同样的,此处就不在代码演示。
read()
在非阻塞模式下,调用read()方法也不能确保方法返回后,确实读到了数据。所以咱们须要本身检查的整型返回值,这个返回值会告诉咱们实际读取了多少字节的数据。
Selector结合非阻塞模式
SocketChannel的非阻塞模式能够和Selector很好的协同工做。把一个或多个SocketChannel注册到一个Selector后,咱们能够经过Selector指导哪些channels通道是处于可读,可写等等状态的。
NIO ServerSocketChannel服务端套接字通道
在Java NIO中,ServerSocketChannel是用于监听TCP连接请求的通道,正如Java网络编程中的ServerSocket同样。
ServerSocketChannel实现类位于java.nio.channels包下面。
void test() throws IOException { //打开一个ServerSocketChannel咱们须要调用他的open()方法 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(9999)); while(true) { SocketChannel socketChannel = serverSocketChannel.accept(); //do something with socketChannel... if (socketChannel.isConnected()) { break; } } //关闭一个ServerSocketChannel咱们须要调用close()方法 serverSocketChannel.close(); }
监听连接
经过调用accept()方法,咱们就开始监听端口上的请求链接。当accept()返回时,他会返回一个SocketChannel链接实例,实际上accept()是阻塞操做,他会阻塞带去线程知道返回一个链接; 不少时候咱们是不知足于监听一个链接的,所以咱们会把accept()的调用放到循环中,就像这样:
while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); //do something with socketChannel... }
固然咱们能够在循环体内加上合适的中断逻辑,而不是单纯的在while循环中写true,以此来结束循环监听;
非阻塞模式
实际上ServerSocketChannel是能够设置为非阻塞模式的。在非阻塞模式下,调用accept()函数会马上返回,若是当前没有请求的连接,那么返回值为空null。所以咱们须要手动检查返回的SocketChannel是否为空,例如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(9999)); //设置为非阻塞模式 serverSocketChannel.configureBlocking(false); while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); if(socketChannel != null){ //do something with socketChannel... } }
Non-blocking Server非阻塞服务器
非阻塞服务器代码
非阻塞IO通道(Non-blocking IO Pipelines)
非阻塞的IO管道(Non-blocking IO Pipelines)能够看作是整个非阻塞IO处理过程的链条。包括在以非阻塞形式进行的读与写操做。
一个非阻塞的IO管道没必要同时须要读和写数据,一般来讲有些管道只须要读数据,而另外一些管道则只需写数据。
固然一个非阻塞的IO管道他也能够同时从多个Channel中读取数据,例如同时从多个SocketChannel中读取数据;
非阻塞和阻塞通道比较(Non-blocking vs. Blocking IO Pipelines)
非阻塞IO管道和阻塞IO管道之间最大的区别是他们各自如何从Channel(套接字socket或文件file)读写数据。
阻塞IO通道的缺点(Blocking IO Pipeline Drawbacks)
上面提到了阻塞的Message Reader易于实现,可是阻塞也给他带了不可避免的缺点,必须为每一个数据数量都分配一个单独线程。缘由就在于IO接口在读取数据时在有数据返回前会一直被阻塞住。这直接致使咱们没法用单线程来处理一个流没有数据返回时去读取其余的流。每当一个线程尝试去读取一个流的数据,这个线程就会被阻塞直到有数据真正返回。
若是这样的IO管道运用到服务器去处理高并发的连接请求,服务器将不得不为每个到来的连接分配一个单独的线程。若是并发数不高好比每一时刻只有几百并发,也行不会有太大问题。一旦服务器的并发数上升到百万级别,这种设计就缺少伸缩性。每一个线程须要为堆栈分配320KB(32位JVM)到1024KB(64位JVM)的内存空间。这就是说若是有1,000,000个线程,须要1TB的内存。而这些在还没开始真正处理接收到的消息前就须要(消息处理中还须要为对象开辟内存)。
为了减小线程数,不少服务器都设计了线程池,把全部接收到的请求放到队列内,每次读取一条链接进行处理。这种设计能够用下图表示:
可是这种设计要求缓冲的链接进程发送有意义的数据。若是这些链接长时间处于非活跃的状态,那么大量非活跃的链接会阻塞线程池中的全部线程。这会致使服务器的响应速度特别慢甚至无响应。
有些服务器为了减轻这个问题,采起的操做是适当增长线程池的弹性。例如,当线程池全部线程都处于饱和时,线程池可能会自动扩容,启动更多的线程来处理事务。这个解决方案会使得服务器维护大量不活跃的连接。可是须要谨记服务器所能开辟的线程数是有限制的。全部当有1,000,000个低速的连接时(大量非活跃连接时),服务器仍是不具有伸缩性。
基础的非阻塞通道设计(Basic Non-blocking IO Pipeline Design)
一个非阻塞的IO通道能够用单线程读取多个数据流。这个前提是相关的流能够切换为非阻塞模式(并非全部流均可以以非阻塞形式操做,FileChannel就不能切换非阻塞模式)。在非阻塞模式下,读取一个流可能返回0个或多个字节。若是流尚未可供读取的数据那么就会返回0,其余大于1的返回都代表这是实际读取到的数据;
为了避开没有数据可读的流,咱们结合Java NIO中的Selector。一个Selector能够注册多个SelectableChannel实例。当咱们调用select()或selectorNow()方法时Selector会返回一个有数据可读的SelectableChannel实例。这个设计能够以下插图:
读取部分信息(Reading Partial Messages)
当咱们冲SelectableChannel中读取一段数据后,咱们并不知道这段数据是不是完整的一个message。由于一个数据段可能包含部分message,也就是说便可能少于一个message,也可能多一个message(0到多个message),正以下面这张插图所示意的那样:
要处理这种截断的message,咱们会遇到两个问题(非阻塞读取数据时):
检测完整message要求Message Reader查看数据段中的数据是否至少包含一个完整的message。若是包含一个或多个完整message,这些message能够被下发到通道中处理。查找完整message的过程是个大量重复的操做,因此这个操做必须是越快越好的。
当数据段中有一个不完整的message时,不管不完整消息是整个数据段仍是说在完整message先后,这个不完整的message数据都须要在剩余部分得到前存储起来。
检查message完整性和存储不完整message都是Message Reader的职责。为了不混淆来自不一样Channel的数据,咱们为每个Channel分配一个Message Reader。整个设计大概是这样的:
当咱们经过Selector获取到一个有数据能够读取的Channel以后,该Channel关联的Message Reader会读取数据,而且把数据打断为Message块。获得完整的message后就能够经过通道下发到其余组件进行处理。
一个Message Reader天然是协议相关的。他须要知道message的格式以便读取。若是咱们的服务器是跨协议复用的,那他必须实现Message Reader的协议-大体相似于接收一个Message Reader工厂做为配置参数。
存储不完整的Message(Storing Partial Messages)
如今咱们已经明确了由Message Reader负责不完整消息的存储直到接收到完整的消息。如今咱们还须要知道这个存储过程须要如何来实现。
在设计的时候咱们须要考虑两个关键因素:
为每一个Message Reade分配Buffer(A Buffer Per Message Reader)
固定大小buffer
显然不完整的消息数据须要存储在某种buffer中。比较直接的办法是咱们为每一个Message Reader都分配一个内部的buffer成员。可是,多大的buffer才合适呢?这个buffer必须能存储下一个message最大的大小。若是一个message最大是1MB,那每一个Message Reader内部的buffer就至少有1MB大小。
在百万级别的并发连接数下,1MB的buffer基本无法正常工做。举例来讲,1,000,000 x 1MB就是1TB的内存大小!若是消息的最大数据量是16MB又须要多少内存呢?128MB呢?
缺点:这种直接分配一个message最大的大小值的buffer是很是浪费空间的。
可伸缩Buffer(Resizable Buffers)
另外一个方案是在每一个Message Reader内部维护一个容量可变的buffer。一个可变的buffer在初始化时占用较少空间,在消息变得很大超出容量时自动扩容。这样每一个连接就不须要都占用好比1MB的空间。每一个连接只使用承载下一个消息所必须的内存大小。
容量可变的buffer优势就是高效利用内存空间,不会浪费内存。
要实现一个可伸缩的buffer有几种不一样的办法。每一种都有它的优缺点,下面几个小结我会逐一讨论它们。
拷贝扩容(Resize by Copy)
第一种实现可伸缩buffer的办法是初始化buffer的时候只申请较少的空间,好比4KB。若是消息超出了4KB的大小那么开赔一个更大的空间,好比8KB,而后把4KB中的数据拷贝纸8KB的内存块中。
拷贝方式扩容的优势:一个消息的所有数据都被保存在了一个连续的字节数组中。这使得数据解析变得更加容易。
缺点:会增长大量的数据拷贝操做。
拷贝扩容操做举例分析:
为了减小数据的拷贝操做,你能够分析整个消息流中的消息大小,一次来找到最适合当前机器的能够减小拷贝操做的buffer大小。例如,你可能会注意到觉大多数的消息都是小于4KB的,由于他们仅仅包含了一个很是请求和响应。这意味着消息的处所荣校应该设置为4KB。
同时,你可能会发现若是一个消息大于4KB,极可能是由于他包含了一个文件。你会可能注意到 大多数经过系统的数据都是小于128KB的。因此咱们能够在第一次扩容设置为128KB。
最后你可能会发现当一个消息大于128KB后,没有什么规律可循来肯定下次分配的空间大小,这意味着最后的buffer容量应该设置为消息最大的可能数据量。
结合这三次扩容时的大小设置,能够必定程度上减小数据拷贝。4KB如下的数据无需拷贝。在1百万的链接下须要的空间例如1,000,000x4KB=4GB,目前(2015)大多数服务器都扛得住。4KB到128KB会仅需拷贝一次,即拷贝4KB数据到128KB的里面。消息大小介于128KB和最大容量的时须要拷贝两次。首先4KB数据被拷贝第二次是拷贝128KB的数据,因此总共须要拷贝132KB数据。假设没有不少的消息会超过128KB,那么这个方案仍是能够接受的。
当一个消息被完整的处理完毕后,它占用的内容应立即刻被释放。这样下一个来自同一个连接通道的消息能够从最小的buffer大小从新开始。这个操做是必须的若是咱们须要尽量高效地复用不一样连接之间的内存。大多数状况下并非全部的连接都会在同一时刻须要大容量的buffer。
笔者写了一个完整的教程阐述了如何实现一个内存buffer使其支持扩容:Resizable Arrays 。这个教程也附带了一个指向GitHub上的源码仓地址,里面有实现方案的具体代码。
追加扩容(Resize by Append)
另外一种实现buffer扩容的方案是让buffer包含几个数组。当须要扩容的时候只须要在开辟一个新的字节数组,而后把内容写到里面去。
这种扩容也有两个具体的办法。一种是开辟单独的字节数组,而后用一个列表把这些独立数组关联起来。另外一种是开辟一些更大的,相互共享的字节数组切片,而后用列表把这些切片和buffer关联起来。我的而言,笔者认为第二种切片方案更好一点点,可是它们以前的差别比较小
这种追加扩容的方案无论是用独立数组仍是切片都有一个优势,那就是写数据的时候不须要额外的拷贝操做。全部的数据能够直接从socket(Channel)中拷贝至数组活切片当中。
这种方案的缺点也很明显,就是数据不是存储在一个连续的数组中。这会使得数据的解析变得更加复杂,由于解析器不得不一样时查找每个独立数组的结尾和全部数组的结尾。正由于咱们须要在写数据时查找消息的结尾,这个模型在设计实现时会相对不那么容易。
TLV编码消息(TLV Encoded Messages)
有些协议的消息消失采用的是一种TLV格式(Type, Length, Value)。这意味着当消息到达时,消息的完整大小存储在了消息的开始部分。咱们能够马上判断为消息开辟多少内存空间。
优势:TLV编码是的内存管理变得更加简单。咱们能够马上知道为消息分配多少内存。即使是不完整的消息,buffer结尾后面也不会有浪费的内存。
缺点:咱们须要在消息的所有数据接收到以前就开辟好须要用的全部内存。所以少许连接慢,但发送了大块数据的连接会占用较多内存,致使服务器无响应。
解决上诉问题的一个变通办法是使用一种内部包含多个TLV的消息格式。这样咱们为每一个TLV段分配内存而不是为整个的消息分配,而且只在消息的片断到达时才分配内存。可是消息片断很大时,任然会出现同样的问题。
另外一个办法是为消息设置超时,若是长时间未接收到的消息(好比10-15秒)。这可让服务器从偶发的并发处理大块消息恢复过来,不过仍是会让服务器有一段时间无响应。另外恶意的DoS攻击会致使服务器开辟大量内存。
TLV编码使得内存管理更加简单,这也是HTTP1.1协议让人以为是一个不太优良的的协议的缘由。正因如此,HTTP2.0协议在设计中也利用TLV编码来传输数据帧。也是由于这个缘由咱们设计了本身的利用TLV编码的网络协议VStack.co。
写不完整的消息(Writing Partial Messages)
在非阻塞IO管道中,写数据也是一个不小的挑战。当你调用一个非阻塞模式Channel的write()方法时,没法保证有多少机字节被写入了ByteBuffer中。write方法返回了实际写入的字节数,因此跟踪记录已被写入的字节数也是可行的。这就是咱们遇到的问题:持续记录被写入的不完整的消息直到一个消息中全部的数据都发送完毕。
为了不多个消息传递到Message Writer超出他所能处理到Channel的量,咱们须要让到达的消息进入队列。Message Writer则尽量快的将数据写到Channel里。
为了使Message Writer可以持续发送刚才已经发送了一部分的消息,Message Writer须要被一直调用,这样他就能够发送更多数据。
示例:
若是你有大量的连接,你会持有大量的Message Writer实例。检查好比1百万的Message Writer实例是来肯定他们是否处于可写状态是很慢的操做。首先,许多Message Writer可能根本就没有数据须要发送。咱们不想检查这些实例。其次,不是全部的Channel都处于可写状态。咱们不想浪费时间在这些非写入状态的Channel。
为了检查一个Channel是否可写,能够把它注册到Selector上。可是咱们不但愿把全部的Channel实例都注册到Selector。试想一下,若是你有1百万的连接,这里面大部分是空闲的,把1百万连接都祖册到Selector上。而后调用select方法的时候就会有不少的Channel处于可写状态。你须要检查全部这些连接中的Message Writer以确认是否有数据可写。
为了不检查全部的这些Message Writer,以及那些根本没有消息须要发送给他们的Channel实例,我么能够采用两步策略:
这两个小步骤确保只有有数据要写的Channel才会被注册到Selector。
集成(Putting it All Together)
正如你所知到的,一个被阻塞的服务器须要时刻检查当前是否有新的完整消息抵达。在一个消息被完整的收到前,服务器可能须要检查屡次。检查一次是不够的。
相似的,服务器也须要时刻检查当前是否有任何可写的数据。若是有的话,服务器须要检查相应的连接看他们是否处于可写状态。仅仅在消息第一次进入队列时检查是不够的,由于一个消息可能被部分写入。
总而言之,一个非阻塞的服务器要三个管道,而且常常执行:
这三个管道在循环中重复执行。你能够尝试优化它的执行。好比,若是没有消息在队列中等候,那么能够跳过写数据管道。或者,若是没有收到新的完整消息,你甚至能够跳过处理数据管道。
下面这张流程图阐述了这整个服务器循环过程:
服务器线程模型(Server Thread Model)
咱们在GitHub上的源码中实现的非阻塞IO服务使用了一个包含两条线程的线程模型。第一个线程负责从ServerSocketChannel接收到达的连接。另外一个线程负责处理这些连接,包括读消息,处理消息,把响应写回到连接。这个双线程模型以下:
NIO DatagramChannel数据报通道
一个Java NIO DatagramChannel是一个能够发送、接收UDP数据包的通道。因为UDP是面向无链接的网络协议,咱们不可用像使用其余通道同样直接进行读写数据。正确的作法是发送、接收数据包。
打开一个DatagramChannel(Opening a DatagramChannel)
打开一个DatagramChannel你这么操做:
DatagramChannel channel = DatagramChannel.open(); channel.socket().bind(new InetSocketAddress(9999));
上述示例中,咱们打开了一个DatagramChannel,它能够在9999端口上收发UDP数据包。
接收数据(Receiving Data)
接收数据,直接调用DatagramChannel的receive()方法:
ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); channel.receive(buf);
receive()方法会把接收到的数据包中的数据拷贝至给定的Buffer中。若是数据包的内容超过了Buffer的大小,剩余的数据会被直接丢弃。
发送数据(Sending Data)
发送数据是经过DatagramChannel的send()方法:
String newData = "New String to wrte to file..." +System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); int byteSent = channel.send(buf, new InetSocketAddress("java.com", 80));
上述示例会把一个字符串发送到“java.com”服务器的UDP端口80.目前这个端口没有被任何程序监听,因此什么都不会发生。当发送了数据后,咱们不会收到数据包是否被接收的的通知,这是因为UDP自己不保证任何数据的发送问题。
连接特定机器地址(Connecting to a Specific Address)
DatagramChannel其实是能够指定到网络中的特定地址的。因为UDP是面向无链接的,这种连接方式并不会建立实际的链接,这和TCP通道相似。确切的说,他会锁定DatagramChannel,这样咱们就只能经过特定的地址来收发数据包。
看一个例子先:
channel.connect(new InetSocketAddress("jenkov.com"), 80));
当链接上后,能够向使用传统的通道那样调用read()和Writer()方法。区别是数据的读写状况得不到保证。下面是几个示例:
int bytesRead = channel.read(buf); int bytesWritten = channel.write(buf);
实例:
public class DataGramChannelClient { public static void main(String[] args) throws IOException { //open a datagramChannel DatagramChannel datagramChannel = DatagramChannel.open(); try { //set non-blocking style datagramChannel.configureBlocking(false); //create a byteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //get test data from console Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String next = scanner.next(); byteBuffer.put(next.getBytes()); byteBuffer.flip(); //Sending Data datagramChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 9999)); byteBuffer.clear(); } } finally { datagramChannel.close(); } } } public class DataGramChannelServer { public static void main(String[] args) throws IOException { //打开了一个DatagramChannel,它能够在9999端口上收发UDP数据包。 DatagramChannel datagramChannel = DatagramChannel.open(); datagramChannel.configureBlocking(false); datagramChannel.bind(new InetSocketAddress(9999)); Selector selector = Selector.open(); //注意要把数据报通道注册到selector上,不然不能检测到请求 datagramChannel.register(selector, SelectionKey.OP_READ); while (selector.select() > 0) { Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey selectionKey = keyIterator.next(); if (selectionKey.isAcceptable()) { System.out.println("ready Acceptable"); } else if (selectionKey.isReadable()) { System.out.println("ready Readable"); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); datagramChannel.receive(byteBuffer); byteBuffer.flip(); //System.out.println(new String(byteBuffer.array()));//this(bytes, 0, bytes.length); byteBuffer不必定是读满的,全部用下面的limit System.out.println(new String(byteBuffer.array(),0,byteBuffer.limit())); byteBuffer.clear(); } } keyIterator.remove(); } } }
NIO Pipe管道
一个Java NIO的管道是两个线程间单向传输数据的链接。一个管道(Pipe)有一个source channel和一个sink channel(没想到合适的中文名)。咱们把数据写到sink channel中,这些数据能够同过source channel再读取出来。
下面是一个管道的示意图:
建立管道(Creating a Pipe)
打开一个管道经过调用Pipe.open()工厂方法,以下:
Pipe pipe = Pipe.open();
向管道写入数据(Writing to a Pipe)
向管道写入数据须要访问他的sink channel:
Pipe.SinkChannel sinkChannel = pipe.sink();
接下来就是调用write()方法写入数据了:
String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); while(buf.hasRemaining()) { sinkChannel.write(buf); }
从管道读取数据(Reading from a Pipe)
相似的从管道中读取数据须要访问他的source channel:
Pipe.SourceChannel sourceChannel = pipe.source();
接下来调用read()方法读取数据:
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf);
注意这里read()的整形返回值表明实际读取到的字节数。
示例:
public class PipeTest { public static void main(String[] args) throws IOException { Pipe pipe = Pipe.open(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //经过缓冲区向管道写入数据 Pipe.SinkChannel sinkChannel = pipe.sink(); byteBuffer.put("i am pipe".getBytes()); byteBuffer.flip(); sinkChannel.write(byteBuffer); //经过缓冲区从管道读数据 //先要重置缓冲区 byteBuffer.clear(); Pipe.SourceChannel sourceChannel = pipe.source(); int length = sourceChannel.read(byteBuffer); //缓冲区转为读模式 byteBuffer.flip(); System.out.println(new String(byteBuffer.array(),0,length)); } }
NIO vs. IO
问题:
当学习Java的NIO和IO时,有个问题会跳入脑海当中:何时该用IO,何时用NIO?二者之间的区别,使用场景以及他们是如何影响代码设计的。
NIO和IO之间的主要差别(Mian Differences Between Java NIO and IO)
下面这个表格归纳了NIO和IO的主要差别。咱们会针对每一个差别进行解释。
IO NIO
Stream oriented Buffer oriented
Blocking IO No blocking IO
Selectors
即:
IO NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
无 选择器
面向流和面向缓冲区比较(Stream Oriented vs. Buffer Oriented)
一、第一个重大差别是IO是面向流的,而NIO是面向缓冲区的。这句话是什么意思呢?
Java IO面向流意思是咱们每次从流当中读取一个或多个字节。怎么处理读取到的字节是咱们本身的事情。他们不会再任何地方缓存。再有就是咱们不能在流数据中向先后移动。若是须要向先后移动读取位置,那么咱们须要首先为它建立一个缓存区。
Java NIO是面向缓冲区的,这有些细微差别。数据是被读取到缓存当中以便后续加工。咱们能够在缓存中向前向后移动。这个特性给咱们处理数据提供了更大的弹性空间。固然咱们仍然须要在使用数据前检查缓存中是否包含咱们须要的全部数据。另外须要确保在往缓冲中写入数据时避免覆盖了已经写入可是还未被处理的数据。
二、阻塞和非阻塞IO比较(Blocking vs. No-blocking IO)
Java IO的各类流都是阻塞的。这意味着一个线程一旦调用了read(),write()方法,那么该线程就被阻塞住了,直到读取到数据或者数据完整写入了。在此期间线程不能作其余任何事情。
Java NIO的非阻塞模式使得线程能够经过channel来读数据,而且是返回当前已有的数据,或者什么都不返回。若是当前没有数据可读的话。这样一来线程不会被阻塞住,它能够继续向下执行其余事情。
一般线程在调用非阻塞操做后,会通知处理其余channel上的IO操做。所以一个线程能够管理多个channel的输入输出。
三、Selectors
Java NIO的selector容许一个单一线程监听多个channel输入。咱们能够注册多个channel到selector上,而后用一个线程来挑出一个处于可读或者可写状态的channel。selector机制使得单线程管理过个channel变得容易。
NIO和IO是如何影响程序设计的(How NIO and IO Influences Application Design)
开发中选择NIO或者IO会在多方面影响程序设计:
API调用(The API Calls)
显而易见使用NIO的API接口和使用IO时是不一样的。不一样于直接从InputStream读取字节,咱们的数据须要先写入到buffer中,而后再从buffer中处理它们。
数据处理(The Processing of Data)
数据的处理方式也随着是NIO或IO而异。
BIO下数据处理是阻塞的,一旦数据方法处理返回时数据就必定能读取到或写入好了,不会有只作一半的状况,且不能在流数据中向先后移动。而NIO是非阻塞的,在读取或写入数据缓冲区时是不能肯定数据是否已经完整读完的,可能须要屡次检查数据完整性。
例子:
在IO设计中,咱们从InputStream或者Reader中读取字节。假设咱们如今须要处理一个按行排列的文本数据,以下:
Name: Anna Age: 25 Email: anna@mailserver.com Phone: 1234567890
这个处理文本行的过程大概是这样的:
InputStream input = ... ; // get the InputStream from the client socket BufferedReader reader = new BufferedReader(new InputStreamReader(input)); String nameLine = reader.readLine(); String ageLine = reader.readLine(); String emailLine = reader.readLine(); String phoneLine = reader.readLine();
请注意处理状态由程序执行多久决定。换句话说,一旦reader.readLine()方法返回,你就知道确定文本行就已读完, readline()阻塞直到整行读完,这就是缘由。你也知道此行包含名称;一样,第二个readline()调用返回的时候,你知道这行包含年龄等。 正如你能够看到,该处理程序仅在有新数据读入时运行,并知道每步的数据是什么。一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退数据(大多如此)。下图也说明了这条原则:
而一个NIO的实现会有所不一样,下面是一个简单的例子:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer);
注意第二行,从通道读取字节到ByteBuffer。当这个方法调用返回时,你不知道你所需的全部数据是否在缓冲区内。你所知道的是,该缓冲区包含一些字节,这使得处理有点困难。假设第一次 read(buffer)调用后,读入缓冲区的数据只有半行,例如,“Name:An”,你能处理数据吗?显然不能,须要等待,直到整行数据读入缓存,在此以前,对数据的任何处理毫无心义。因此,你怎么知道是否该缓冲区包含足够的数据能够处理呢?好了,你不知道。发现的方法只能查看缓冲区中的数据。其结果是,在你知道全部数据都在缓冲区里以前,你必须检查几回缓冲区的数据。这不只效率低下,并且能够使程序设计方案杂乱不堪。例如:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer); while(! bufferFull(bytesRead) ) { bytesRead = inChannel.read(buffer); }
bufferFull()方法必须跟踪有多少数据读入缓冲区,并返回真或假,这取决于缓冲区是否已满。换句话说,若是缓冲区准备好被处理,那么表示缓冲区满了。
bufferFull()方法扫描缓冲区,但必须保持在bufferFull()方法被调用以前状态相同。若是没有,下一个读入缓冲区的数据可能没法读到正确的位置。这是不可能的,但倒是须要注意的又一问题。
若是缓冲区已满,它能够被处理。若是它不满,而且在你的实际案例中有意义,你或许能处理其中的部分数据。可是许多状况下并不是如此。下图展现了“缓冲区数据循环就绪”:
小结
NIO容许咱们只用一条线程来管理多个通道(网络链接或文件),随之而来的代价是解析数据相对于阻塞流来讲可能会变得更加的复杂。
若是你须要同时管理成千上万的连接,这些连接只发送少许数据,例如聊天服务器,用NIO来实现这个服务器是有优点的。相似的,若是你须要维持大量的连接,例如P2P网络,用单线程来管理这些 连接也是有优点的。这种单线程多链接的NIO设计图:
若是连接数不是不少,可是每一个连接的占用较大带宽,每次都要发送大量数据,那么使用传统的IO设计服务器多是最好的选择。下面是经典IO服务设计图:
NIO Path路径
Java的path接口是做为Java NIO 2的一部分是Java6,7中NIO的升级增长部分。Path在Java 7新增的。相关接口位于java.nio.file包下,因此Path接口的完整名称是java.nio.file.Path.
一个Path实例表明一个文件系统内的路径。path能够指向文件也能够指向目录。能够是相对路径也能够是绝对路径。绝对路径包含了从根目录到该文件(目录)的完整路径。相对路径包含该文件(目录)相对于其余路径的路径。
在不少状况下java.no.file.Path接口和java.io.File比较类似,可是他们之间存在一些细微的差别。尽管如此,在大多数状况下,咱们均可以用Path接口来替换File相关类。
建立Path实例(Creating a Path Instance)
为了使用java.nio.file.Path实例咱们必须建立Path对象。建立Path实例能够经过Paths的工厂方法get()。
注意Paths.get("c:\data\myfile.txt")的调用。这个方法会建立一个Path实例,换句话说Paths.get()是Paths的一个工厂方法。
建立绝对路径(Creating an Absolute Path)
建立绝对路径只须要调动Paths.get()这个工厂方法,同时传入绝对文件。这是一个例子:
Path path = Paths.get("c:\\data\\myfile.txt");
对路径是c:\data\myfile.txt,里面的双斜杠\字符是Java 字符串中必须的,由于\是转义字符,表示后面跟的字符在字符串中的真实含义。双斜杠\表示\自身。
上面的路径是Windows下的文件系统路径表示。在Unixx系统中(Linux, MacOS,FreeBSD等)上述的绝对路径长得是这样的:
Path path = Paths.get("/home/jakobjenkov/myfile.txt");
他的绝对路径是/home/jakobjenkov/myfile.txt。 若是在Windows机器上使用用这种路径,那么这个路径会被认为是相对于当前磁盘的。例如:
/home/jakobjenkov/myfile.txt
这个路径会被理解其C盘上的文件,因此路径又变成了
C:/home/jakobjenkov/myfile.txt
建立相对路径(Creating a Relative Path)
相对路径是从一个路径(基准路径)指向另外一个目录或文件的路径。完整路径实际上等同于相对路径加上基准路径。
Java NIO的Path类能够用于相对路径。建立一个相对路径能够经过调用Path.get(basePath, relativePath),下面是一个示例:
Path projects = Paths.get("d:\\data", "projects"); Path file = Paths.get("d:\\data", "projects\\a-project\\myfile.txt");
第一行建立了一个指向d:\data\projects的Path实例。第二行建立了一个指向d:\data\projects\a-project\myfile.txt的Path实例。 在使用相对路径的时候有两个特殊的符号:
.表示的是当前目录,例如咱们能够这样建立一个相对路径:
Path currentDir = Paths.get("."); System.out.println(currentDir.toAbsolutePath());
currentDir的实际路径就是当前代码执行的目录。 若是在路径中间使用了.那么他的含义实际上就是目录位置自身,例如:
Path currentDir = Paths.get("d:\\data\\projects\.\a-project");
上诉路径等同于:
d:\data\projects\a-project
..表示父目录或者说是上一级目录:
Path parentDir = Paths.get("..");
这个Path实例指向的目录是当前程序代码的父目录。 若是在路径中间使用..那么会相应的改变指定的位置:
String path = "d:\\data\\projects\\a-project\\..\\another-project"; Path parentDir2 = Paths.get(path);
这个路径等同于:
d:\data\projects\another-project
.和..也能够结合起来用,这里不过多介绍。
Path.normalize()
Path的normalize()方法能够把路径规范化。也就是把.和..都等价去除:
String originalPath = "d:\\data\\projects\\a-project\\..\\another-project"; Path path1 = Paths.get(originalPath); System.out.println("path1 = " + path1); Path path2 = path1.normalize(); System.out.println("path2 = " + path2);
这段代码的输出以下:
path1 = d:\data\projects\a-project\..\another-project path2 = d:\data\projects\another-project
实例:
import java.nio.file.Path; import java.nio.file.Paths; public class PathTest { public static void main(String[] args) { //建立Path实例 Path path = Paths.get("c:\\data\\myfile.txt"); //建立绝对路径(Creating an Absolute Path) Path path1 = Paths.get("c:\\data\\myfile.txt"); //建立相对路径 Path path2 = Paths.get("d:\\data", "projects\\a-project\\myfile.txt"); //Path的normalize()方法能够把路径规范化 String originalPath = "d:\\data\\projects\\a-project\\..\\another-project"; Path path3 = Paths.get(originalPath); System.out.println("path3 = " + path3); Path path4 = path3.normalize(); System.out.println("path4 = " + path4); } }
NIO Files
Java NIO中的Files类(java.nio.file.Files)提供了多种操做文件系统中文件的方法。本节教程将覆盖大部分方法。Files类包含了不少方法,因此若是本文没有提到的你也能够直接查询JavaDoc文档。
java.nio.file.Files类是和java.nio.file.Path相结合使用的,因此在用Files以前确保你已经理解了Path类。
Files.exists()
Files.exits()方法用来检查给定的Path在文件系统中是否存在。 在文件系统中建立一个本来不存在的Path是可行的。例如,你想新建一个目录,那么先建立对应的Path实例,而后建立目录。
因为Path实例可能指向文件系统中的不存在的路径,因此须要用Files.exists()来确认。
下面是一个使用Files.exists()的示例:
Path path = Paths.get("data/logging.properties"); boolean pathExists = Files.exists(path, new LinkOption[]{ LinkOption.NOFOLLOW_LINKS});
这个示例中,咱们首先建立了一个Path对象,而后利用Files.exists()来检查这个路径是否真实存在。
注意Files.exists()的的第二个参数。他是一个数组,这个参数直接影响到Files.exists()如何肯定一个路径是否存在。在本例中,这个数组内包含了LinkOptions.NOFOLLOW_LINKS,表示检测时不包含符号连接文件。
Files.createDirectory()
Files.createDirectory()会建立Path表示的路径,下面是一个示例:
Path path = Paths.get("data/subdir"); try { Path newDir = Files.createDirectory(path); } catch(FileAlreadyExistsException e){ // the directory already exists. } catch (IOException e) { //something else went wrong e.printStackTrace(); }
第一行建立了一个Path实例,表示须要建立的目录。接着用try-catch把Files.createDirectory()的调用捕获住。若是建立成功,那么返回值就是新建立的路径。
若是目录已经存在了,那么会抛出java.nio.file.FileAlreadyExistException异常。若是出现其余问题,会抛出一个IOException。好比说,要建立的目录的父目录不存在,那么就会抛出IOException。父目录指的是你要建立的目录所在的位置。也就是新建立的目录的上一级父目录。
Files.copy()
Files.copy()方法能够吧一个文件从一个地址复制到另外一个位置。例如:
Path sourcePath = Paths.get("data/logging.properties"); Path destinationPath = Paths.get("data/logging-copy.properties"); try { Files.copy(sourcePath, destinationPath); } catch(FileAlreadyExistsException e) { //destination file already exists } catch (IOException e) { //something else went wrong e.printStackTrace(); }
这个例子当中,首先建立了原文件和目标文件的Path实例。而后把它们做为参数,传递给Files.copy(),接着就会进行文件拷贝。
若是目标文件已经存在,就会抛出java.nio.file.FileAlreadyExistsException异常。相似的目标地址路径不对,也会抛出IOException。
覆盖已经存在的文件(Overwriting Existing Files)
copy操做能够强制覆盖已经存在的目标文件。下面是具体的示例:
Path sourcePath = Paths.get("data/logging.properties"); Path destinationPath = Paths.get("data/logging-copy.properties"); try { Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING); } catch(FileAlreadyExistsException e) { //destination file already exists } catch (IOException e) { //something else went wrong e.printStackTrace(); }
注意copy方法的第三个参数,这个参数决定了是否能够覆盖文件。
Files.move()
Java NIO的Files类也包含了移动的文件的接口。移动文件和重命名是同样的,可是还会改变文件的目录位置。java.io.File类中的renameTo()方法与之功能是同样的。
Path sourcePath = Paths.get("data/logging-copy.properties"); Path destinationPath = Paths.get("data/subdir/logging-moved.properties"); try { Files.move(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { //moving file failed. e.printStackTrace(); }
首先建立源路径和目标路径的,原路径指的是须要移动的文件的初始路径,目标路径是指须要移动到的位置。
这里move的第三个参数也容许咱们覆盖已有的文件。
Files.delete()
Files.delete()方法能够删除一个文件或目录:
Path path = Paths.get("data/subdir/logging-moved.properties"); try { Files.delete(path); } catch (IOException e) { //deleting file failed e.printStackTrace(); }
首先建立须要删除的文件的path对象。接着就能够调用delete了。
Files.walkFileTree()
Files.walkFileTree()方法具备递归遍历目录的功能。walkFileTree接受一个Path和FileVisitor做为参数。Path对象是须要遍历的目录,FileVistor则会在每次遍历中被调用。
下面先来看一下FileVisitor这个接口的定义:
public interface FileVisitor { public FileVisitResult preVisitDirectory( Path dir, BasicFileAttributes attrs) throws IOException; public FileVisitResult visitFile( Path file, BasicFileAttributes attrs) throws IOException; public FileVisitResult visitFileFailed( Path file, IOException exc) throws IOException; public FileVisitResult postVisitDirectory( Path dir, IOException exc) throws IOException { }
FileVisitor须要调用方自行实现,而后做为参数传入walkFileTree().FileVisitor的每一个方法会在遍历过程当中被调用屡次。若是不须要处理每一个方法,那么能够继承他的默认实现类SimpleFileVisitor,它将全部的接口作了空实现。
下面看一个walkFileTree()的示例:
Files.walkFileTree(path, new FileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { System.out.println("pre visit dir:" + dir); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println("visit file: " + file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { System.out.println("visit file failed: " + file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { System.out.println("post visit directory: " + dir); return FileVisitResult.CONTINUE; } });
FileVisitor的方法会在不一样时机被调用: preVisitDirectory()在访问目录前被调用。postVisitDirectory()在访问后调用。
visitFile()会在整个遍历过程当中的每次访问文件都被调用。他不是针对目录的,而是针对文件的。visitFileFailed()调用则是在文件访问失败的时候。例如,当缺乏合适的权限或者其余错误。
上述四个方法都返回一个FileVisitResult枚举对象。具体的可选枚举项包括:
返回这个枚举值可让调用方决定文件遍历是否须要继续。 CONTINE表示文件遍历和正常状况下同样继续。
TERMINATE表示文件访问须要终止。
SKIP_SIBLINGS表示文件访问继续,可是不须要访问其余同级文件或目录。
SKIP_SUBTREE表示继续访问,可是不须要访问该目录下的子目录。这个枚举值仅在preVisitDirectory()中返回才有效。若是在另外几个方法中返回,那么会被理解为CONTINE。
Searching For Files
下面看一个例子,咱们经过walkFileTree()来寻找一个README.txt文件:
Path rootPath = Paths.get("data"); String fileToFind = File.separator + "README.txt"; try { Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String fileString = file.toAbsolutePath().toString(); //System.out.println("pathString = " + fileString); if(fileString.endsWith(fileToFind)){ System.out.println("file found at path: " + file.toAbsolutePath()); return FileVisitResult.TERMINATE; } return FileVisitResult.CONTINUE; } }); } catch(IOException e){ e.printStackTrace(); }
Deleting Directies Recursively
Files.walkFileTree()也能够用来删除一个目录以及内部的全部文件和子目。Files.delete()只用用于删除一个空目录。咱们经过遍历目录,而后在visitFile()接口中三次全部文件,最后在postVisitDirectory()内删除目录自己。
Path rootPath = Paths.get("data/to-delete"); try { Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println("delete file: " + file.toString()); Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); System.out.println("delete dir: " + dir.toString()); return FileVisitResult.CONTINUE; } }); } catch(IOException e){ e.printStackTrace(); }
示例:
public class FileTest { public static void main(String[] args) throws IOException { //建立绝对路径 Path path = Paths.get("D:\\text\\file\\testfile.txt");//false //Path path = Paths.get("D:\\text\\1_loan.sql");//true //Path path = Paths.get("D:\\text\\test1\\1_loan.sql");//false //检查给定的Path在文件系统中是否存在,NOFOLLOW_LINKS:表示检测时不包含符号连接文件 boolean exists = Files.exists(path, new LinkOption[]{LinkOption.NOFOLLOW_LINKS}); //默认不传的话是包含符号连接文件的 //boolean exists = Files.exists(path); System.out.println("exists =" + exists); //路径格式能够是这两种 Path filePath = Paths.get("D:\\text\\file"); //Path filePath = Paths.get("D:/text/file_copy"); //返回值就是新建立的路径.建立文件夹 //Path directoryPath = Files.createDirectory(filePath); //createDirectory directoryPath=D:\text\file //System.out.println("createDirectory directoryPath=" + directoryPath); //返回值就是新建立的路径.建立文件夹 //Path rtfilePath = Files.createFile(path); //createFile rtfilePath=D:\text\file\testfile.txt //System.out.println("createFile rtfilePath=" + rtfilePath); //Path path1 = Paths.get("D:\\text\\file\\subfile\\testfile.sql"); //NoSuchFileException: D:\text\file\subfile\testfile.sql because subfile not exist //Path rtpath1 = Files.createFile(path1); //System.out.println("createFile rtpath1=" + rtpath1); //Path path2 = Paths.get("D:\\text\\file\\subfile\\testfile.sql"); //建立连续不存在的文件夹,不存在就建立,不过只能建立文件夹,不能连同文件建立,文件要另外建立 //Path rtpath2 = Files.createDirectories(path2); //createFile rtpath2=D:\text\file\subfile\testfile.sql //System.out.println("createFile rtpath2=" + rtpath2); //copy Path sourcePath = Paths.get("D:\\text\\file\\testfile.txt"); Path destinationPath = Paths.get("D:\\text\\file\\testfile_copy.txt"); //copy =D:\text\file\testfile_copy.txt //copy的目标路径文件不能存在,不然抛java.nio.file.FileAlreadyExistsException: D:\text\file\testfile_copy.txt异常 //Path copy = Files.copy(sourcePath, destinationPath); //copy操做能够强制覆盖已经存在的目标文件,传入参数 StandardCopyOption.REPLACE_EXISTING //Path copy = Files.copy(sourcePath, destinationPath,StandardCopyOption.REPLACE_EXISTING); //System.out.println("copy =" + copy); //move:移动文件和重命名是同样的 //Path sourcePathM = Paths.get("D:\\text\\file\\testfile.txt"); //Path destinationPathM = Paths.get("D:\\text\\file\\testfile_move.txt"); //Path move = Files.move(sourcePathM, destinationPathM); //move操做能够强制覆盖已经存在的目标文件,传入参数 StandardCopyOption.REPLACE_EXISTING //原有的testfile.txt将被移动或重命名而不存在了,若是有目标文件testfile_move.txt存在,则会被覆盖 //Path move = Files.move(sourcePathM, destinationPathM,StandardCopyOption.REPLACE_EXISTING); //System.out.println("move =" + move); //delete 删除一个文件或目录 //Path deletePath = Paths.get("D:\\text\\file\\testfile_move.txt"); //Exception in thread "main" java.nio.file.NoSuchFileException: D:\text\file\testfile_move.txt //要求要删除的文件或目录必须存在,不然报错 //Files.delete(deletePath); //存在才删除 //Files.deleteIfExists(deletePath); Path rootPath = Paths.get("D:"); final String fileToFind = File.separator + "README.txt"; try { Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String fileString = file.toAbsolutePath().toString(); //System.out.println("pathString = " + fileString); if(fileString.endsWith(fileToFind)){ System.out.println("file found at path: " + file.toAbsolutePath()); return FileVisitResult.TERMINATE; } return FileVisitResult.CONTINUE; } }); } catch(IOException e){ e.printStackTrace(); } } }
NIO AsynchronousFileChannel异步文件通道(AIO)
Java7中新增了AsynchronousFileChannel做为nio的一部分。AsynchronousFileChannel使得数据能够进行异步读写。
建立AsynchronousFileChannel(Creating an AsynchronousFileChannel)
AsynchronousFileChannel的建立能够经过open()静态方法:
Path path = Paths.get("data/test.xml"); AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
open()的第一个参数是一个Path实体,指向咱们须要操做的文件。 第二个参数是操做类型。上述示例中咱们用的是StandardOpenOption.READ,表示以读的形式操做文件。
读取数据(Reading Data)
读取AsynchronousFileChannel的数据有两种方式。每种方法都会调用AsynchronousFileChannel的一个read()接口。下面分别看一下这两种写法。
一、经过Future读取数据(Reading Data Via a Future)
第一种方式是调用返回值为Future的read()方法:
Future<Integer> operation = fileChannel.read(buffer, 0);
这种方式中,read()接受一个ByteBuffer做为第一个参数,数据会被读取到ByteBuffer中。第二个参数是开始读取数据的位置。
read()方法会马上返回,即便读操做没有完成。咱们能够经过isDone()方法检查操做是否完成。
下面是一个略长的示例:
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); ByteBuffer buffer = ByteBuffer.allocate(1024); long position = 0; Future<Integer> operation = fileChannel.read(buffer, position); while(!operation.isDone()); buffer.flip(); byte[] data = new byte[buffer.limit()]; buffer.get(data); System.out.println(new String(data)); buffer.clear();
在这个例子中咱们建立了一个AsynchronousFileChannel,而后建立一个ByteBuffer做为参数传给read。接着咱们建立了一个循环来检查是否读取完毕isDone()。这里的循环操做比较低效,它的意思是咱们须要等待读取动做完成。
一旦读取完成后,咱们就能够把数据写入ByteBuffer,而后输出。
二、经过CompletionHandler读取数据(Reading Data Via a CompletionHandler)
另外一种方式是调用接收CompletionHandler做为参数的read()方法。下面是具体的使用:
fileChannel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("result = " + result); attachment.flip(); byte[] data = new byte[attachment.limit()]; attachment.get(data); System.out.println(new String(data)); attachment.clear(); } @Override public void failed(Throwable exc, ByteBuffer attachment) { } });
这里,一旦读取完成,将会触发CompletionHandler的completed()方法,并传入一个Integer和ByteBuffer。前面的整形表示的是读取到的字节数大小。第二个ByteBuffer也能够换成其余合适的对象方便数据写入。 若是读取操做失败了,那么会触发failed()方法。
写数据(Writing Data)
和读数据相似某些数据也有两种方式,调动不一样的的write()方法,下面分别看介绍这两种方法。
经过Future写数据(Writing Data Via a Future)
经过AsynchronousFileChannel咱们能够异步写数据。
Path path = Paths.get("data/test-write.txt"); AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE); ByteBuffer buffer = ByteBuffer.allocate(1024); long position = 0; buffer.put("test data".getBytes()); buffer.flip(); Future<Integer> operation = fileChannel.write(buffer, position); buffer.clear(); while(!operation.isDone()); System.out.println("Write done");
首先把文件以写方式打开,接着建立一个ByteBuffer做为写入数据的目的地。再把数据进入ByteBuffer。最后检查一下是否写入完成。 须要注意的是,这里的文件必须是已经存在的,不然在尝试write数据是会抛出一个java.nio.file.NoSuchFileException.
检查一个文件是否存在能够经过下面的方法:
if(!Files.exists(path)){ Files.createFile(path); }
经过CompletionHandler写数据(Writing Data Via a CompletionHandler)
咱们也能够经过CompletionHandler来写数据:
Path path = Paths.get("data/test-write.txt"); if(!Files.exists(path)){ Files.createFile(path); } AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE); ByteBuffer buffer = ByteBuffer.allocate(1024); long position = 0; buffer.put("test data".getBytes()); buffer.flip(); fileChannel.write(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("bytes written: " + result); } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.out.println("Write failed"); exc.printStackTrace(); } });
一样当数据吸入完成后completed()会被调用,若是失败了那么failed()会被调用。
示例:
public class AIOTest { public static void main1(String[] args) throws IOException { //经过Future读取数据 Path path = Paths.get("D:/test/file/README.txt"); AsynchronousFileChannel asynChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); Future<Integer> future = asynChannel.read(byteBuffer, 0); while (!future.isDone()) { //等待读取动做完成 }; byteBuffer.flip(); //有如下两种输出方式,本质上都是把缓冲区byteBuffer转为byte数组,再用new String接收 //System.out.println(new String(byteBuffer.array(),0,byteBuffer.limit())); byte[] data = new byte[byteBuffer.limit()]; byteBuffer.get(data); //设置编码格式 //System.out.println(new String(data, StandardCharsets.UTF_8)); //不设置编码格式时取的是系统默认的编码格式。在linux中是utf-8 System.out.println(new String(data)); byteBuffer.clear(); asynChannel.close(); } public static void main2(String[] args) throws IOException { //经过CompletionHandler读取数据 Path path = Paths.get("D:/test/file/README.txt"); AsynchronousFileChannel asynChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); asynChannel.read(byteBuffer, 0, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("result = " + result); attachment.flip(); byte[] data = new byte[attachment.limit()]; attachment.get(data); System.out.println(new String(data)); attachment.clear(); } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.out.println("result failed " + exc.getMessage()); } }); asynChannel.close(); } public static void main3(String[] args) throws IOException { //经过Future写数据 Path path = Paths.get("D:/test/file/README_WRITE.txt"); //若文件不存在则建立一个 if (!Files.exists(path)){ Files.createFile(path); } AsynchronousFileChannel asynChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put("i am batman,and i am rich".getBytes()); byteBuffer.flip(); Future<Integer> future = asynChannel.write(byteBuffer, 0); byteBuffer.clear(); while (!future.isDone()){ } System.out.println("Write done"); asynChannel.close(); } public static void main(String[] args) throws IOException { //经过CompletionHandler写数据 Path path = Paths.get("D:/test/file/README_WRITE.txt"); //若文件不存在则建立一个 if (!Files.exists(path)){ Files.createFile(path); } AsynchronousFileChannel asynChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put("i am batman,and i am rich".getBytes()); byteBuffer.flip(); asynChannel.write(byteBuffer, 0, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("Write done"); System.out.println("bytes written: " + result); } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.out.println("bytes written failes: " + exc.getMessage()); } }); asynChannel.close(); } }
疑问:
Q:NIO的具体使用场景都有哪些?网络链接?学习NIO的目的?
Q:Linux的五种IO模型?与java的io模型的关系?
《漫话:如何给女友解释什么是Linux的五种IO模型?》
Q:equals()判断两个buffer相对,需知足:
从上面的三个条件能够看出,equals只比较buffer中的部份内容,并不会去比较每个元素。
全部剩余字节相等是指若是这个buffer有被读或者写过,只比较他们剩余没有读或者写的部分是么?
Q:FileChannel不能切换为非阻塞模式,都有哪些Channel能够切换为非阻塞模式?
Q:select()方法的返回值是一个int整形,表明有多少channel处于就绪了。也就是自上一次select后有多少channel进入就绪。举例来讲,假设第一次调用select时正好有一个channel就绪,那么返回值是1,而且对这个channel作任何处理,接着再次调用select,此时刚好又有一个新的channel就绪,那么返回值仍是1,如今咱们一共有两个channel处于就绪,可是在每次调用select时只有一个channel是就绪的。每次调用select时只有一个channel是就绪的?为何?
Q:如何检查message完整性?
Q:一个Message Reader天然是协议相关的?都有哪些协议?协议的做用是为了约定规范么?
Q:UDP是面向无链接的网络协议,什么叫无链接的网络协议?
Q:面向流和面向缓冲区的区别,咱们不能在流数据中向先后移动。若是须要向先后移动读取位置,那么咱们须要首先为它建立一个缓存区?怎么在缓冲区中向先后移动?
Q:针对做者画的NIO和BIO这两个交互图不能很明确得观察有什么不一样,特别是BIO前面部分,和NIO后面部分没有画出来,后面部分也是多线程处理啊?不一样点是是否阻塞进行链接仍是非阻塞链接吧?
NIO:
BIO:
Q:NIO Path路径(java.nio.file.Path )和以前的BIOPath路径( java.io.File )有什么区别?在使用时怎么选择?
其余:
在用main方法测试时怎么给String args[] 参数赋值?
一、直接在代码中给args参数赋值一个咱们想要的数组。
static public void main(String args[]) throws Exception { args = new String[]{"D:\\text\\1_loan.sql", "D:\\text\\1_loan_copy.sql"}; //.... }
二、在idea里的运行debug时能够设置program arguments,以空格符分开参数。
2、注意当copy其余java类进来时,若是引用类的包名路径不一样,会致使报错,且还不能引用正确路径上的类,这时候要点开import 里引用的错误包路径的类引用路径,删除了从新引入。快捷键 ctrl + alt + o。
3、符号连接文件:与硬链接相对应,Lnux系统中还存在另外一种链接,称为符号链接(Symbilc Link),也叫软链接。软连接文件有点相似于Windows的快捷方式 。
什么是linux下的符号连接文件