I/O和NIO的区别在于数据的打包和传输方式。html
I/O流的方式处理数据java
NIO块的方式处理数据数组
面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据建立过滤器很是容易。连接几个过滤器,以便每一个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。面向流的 I/O 一般至关慢。服务器
一个 面向块 的 I/O 系统以块的形式处理数据。每个操做都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。可是面向块的I/O比较复杂。NIO的主要应用在高性能、高容量服务端应用程序。网络
IO | NIO |
---|---|
面向流 | 面向块,缓冲 |
阻塞IO | 非阻塞IO |
无 | Selector |
一、Channels and Buffers(通道和缓冲区)
标准的IO基于字节流和字符流进行操做的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操做,数据老是从通道读取到缓冲区中,或者从缓冲区写入到通道中。dom
二、Non-blocking IO(非阻塞IO)
Java NIO能够非阻塞的方式使用IO,例如:当线程从通道读取数据到缓冲区时,线程仍是能够进行其余事情。当数据被写入到缓冲区时,线程能够继续处理它。从缓冲区写入通道也相似。异步
三、Selectors(选择器)
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(好比:链接打开,数据到达)。所以,单个的线程能够监听多个数据通道。socket
通道和缓冲区是 NIO 中的核心对象,几乎在每个 I/O 操做中都要使用它们。性能
一个 Buffer 实质上是一个容器对象,它包含一些要写入或者刚读出的数据。spa
在 NIO 库中,全部数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任什么时候候访问 NIO 中的数据,您都是将它放到缓冲区中。
缓冲区实质上就是一个数组,一般它是一个字节数组,可是也可使用其余种类的数组。但它不只仅是一个数组,缓冲区还提供了对数据的结构化访问,并且还能够跟踪系统的读/写进程。
最经常使用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 能够在其底层字节数组上进行 get/set 操做(即字节的获取和设置)。
ByteBuffer 不是 NIO 中惟一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型:
Buffer抽象类中提供了须要处理的方法类型。
使用Buffer读写数据通常遵循如下四个步骤:
写入数据到Buffer
调用flip()方法
从Buffer中读取数据
调用clear()方法或者compact()方法
当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,须要经过flip()方法将Buffer从写模式切换到读模式。在读模式下,能够读取以前写入到buffer的全部数据。一旦读完了全部的数据,就须要清空缓冲区,让它能够再次被写入。
有两种方式能清空缓冲区:调用clear()或compact()方法。
clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
try (RandomAccessFile aFile = new RandomAccessFile("F:\\1.txt", "rw")) { //从RandomAccesFile获取通道 FileChannel inChannel = aFile.getChannel(); //建立一个缓冲区并在其中放入一些数据 ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf); //read into buffer. //检查状态 while (bytesRead != -1) { //flip() 方法让缓冲区能够将新读入的数据写入另外一个通道。 buf.flip(); //make buffer ready for read while (buf.hasRemaining()) { System.out.print((char) buf.get()); // read 1 byte at a time } buf.clear(); //make buffer ready for writing bytesRead = inChannel.read(buf); } }
缓冲区本质上是一块能够写入数据,而后能够从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
缓冲区对象有三个基本属性:
容量Capacity:缓冲区能容纳的数据元素的最大数量,在缓冲区建立时设定,没法更改
上界Limit:代表还有多少数据须要取出(在从缓冲区写入通道时),或者还有多少空间能够放入数据(在从通道读入缓冲区时),limit不能大于capacity
位置Position:下一个要被读或写的元素的索引
这三个变量一块儿能够跟踪缓冲区的状态和它所包含的数据。
四个属性老是遵循这样的关系:0<=mark<=position<=limit<=capacity。下图是新建立的容量为10的缓冲区逻辑视图:
写入模式
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');
五次调用put后的缓冲区:
此时,limit表示还有多少空间能够放入数据。position最大可为capacity-1。
读取模式
如今缓冲区满了,咱们必须将其清空。咱们想把这个缓冲区传递给一个通道,以使内容能被所有写出,但如今执行get()无疑会取出未定义的数据。咱们必须将posistion设为0,而后通道就会从正确的位置开始读了,但读到哪算读完了呢?这正是limit引入的缘由,它指明缓冲区有效内容的未端。这个操做 在缓冲区中叫作翻转:buffer.flip()。
flip这个方法作两件很是重要的事:
将 limit 设置为当前 position。
将 position 设置为 0。
rewind操做与flip类似,但不影响limit。
clear
最后一步是调用缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。
Clear 作两种很是重要的事情:
1.将 limit 设置为与 capacity 相同。
2.设置 position 为 0。
clear()与compact()区别
一、clear()方法,position将被设回0,limit被设置成 capacity的值。Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”
二、compact()方法将全部未读的数据拷贝到Buffer起始处。而后将position设到最后一个未读元素正后面。limit属性依然像clear()方法同样,设置成capacity。
在可以读和写以前,必须有一个缓冲区。要建立缓冲区,必需要进行分配,。咱们使用静态方法 allocate() 来分配缓冲区,每个Buffer类都有一个allocate方法。
//分配48字节capacity的ByteBuffer的例子。 ByteBuffer buf = ByteBuffer.allocate(48); //分配一个可存储1024个字符的CharBuffer: CharBuffer buf = CharBuffer.allocate(1024);
写数据到Buffer有两种方式:
从Channel写到Buffer。
经过Buffer的put()方法写到Buffer里。
//从Channel写到Buffer int bytesRead = inChannel.read(buf); //read into buffer. //经过put方法写Buffer的例子: buf.put(127);
从Buffer中读取数据有两种方式:
从Buffer读取数据到Channel。
使用get()方法从Buffer中读取数据。
//read from buffer into channel. int bytesWritten = inChannel.write(buf); //使用get()方法从Buffer中读取数据 byte aByte = buf.get();
get方法有不少版本,容许你以不一样的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组。
rewind()方法
Buffer.rewind()将position设回0,因此你能够重读Buffer中的全部数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。
mark()与reset()
经过调用Buffer.mark()方法,能够标记Buffer中的一个特定position。以后能够经过调用Buffer.reset()方法恢复到这个position。
equals()与compareTo()方法
可使用equals()和compareTo()方法两个Buffer。
equals()
当知足下列条件时,表示两个Buffer相等:
有相同的类型(byte、char、int等)。
Buffer中剩余的byte、char等的个数相等。
Buffer中全部剩余的byte、char等都相同。
equals只是比较Buffer的一部分,不是每个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。
compareTo()
compareTo()方法比较两个Buffer的剩余元素(byte、char等), 若是知足下列条件,则认为一个Buffer“小于”另外一个Buffer:
第一个不相等的元素小于另外一个Buffer中对应的元素
第一个Buffer的元素个数比另外一个少
Java NIO的通道相似流,但又有些不一样:
既能够从通道中读取数据,又能够写数据到通道。但流的读写一般是单向的。
通道能够异步地读写。
通道中的数据老是要先读到一个Buffer,或者老是要从一个Buffer中写入。
FileChannel 从文件中读写数据。
DatagramChannel 能经过UDP读写网络中的数据。
SocketChannel 能经过TCP读写网络中的数据。
ServerSocketChannel能够监听新进来的TCP链接,像Web服务器那样。对每个新进来的链接都会建立一个SocketChannel。
与缓冲区不一样,通道不能被重复使用;关闭通道后,通道将再也不链接任何东西,任何的读或写操做都会致使ClosedChannelException。
调用通道的close()方法时,可能会致使线程暂时阻塞,就算通道处于非阻塞模式也不例外。若是通道实现了InterruptibleChannel接 口,那么阻塞在该通道上的一个线程被中断时,该通道将被关闭,被阻塞线程也会抛出ClosedByInterruptException异常。
当一个通道 关闭时,休眠在该通道上的全部线程都将被唤醒并收到一个AsynchronousCloseException异常。
scatter/gather用于描述从Channel中读取或者写入到Channel的操做。
分散(scatter)从Channel中读取是指在读操做时将读取的数据写入多个buffer中。所以,Channel将从Channel中读取的数据"分散(scatter)"到多个Buffer中。
汇集(gather)写入Channel是指在写操做时将多个buffer的数据写入同一个Channel,所以,Channel 将多个Buffer中的数据"汇集(gather)"后发送到Channel。
scatter / gather常常用于须要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不一样的buffer中,这样你能够方便的处理消息头和消息体。
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = { header, body }; channel.read(bufferArray);
buffer首先被插入到数组,而后再将数组做为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另外一个buffer中写。
Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,若是存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads才能正常工做。
Gathering Writes是指数据从多个buffer写入到同一个channel。以下图描述:
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); //write data into buffers ByteBuffer[] bufferArray = { header, body }; channel.write(bufferArray);
buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。所以,若是一个buffer的容量为128byte,可是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。所以与Scattering Reads相反,Gathering Writes能较好的处理动态消息。
Java NIO中的FileChannel是一个链接到文件的通道。能够经过文件通道读写文件。FileChannel没法设置为非阻塞模式,它老是运行在阻塞模式下。
由于没法保证write()方法一次能向FileChannel写入多少字节,所以须要重复调用write()方法,直到Buffer中已经没有还没有写入通道的字节。
//经过inputstream或者RandomAccessFile,打开FileChannel RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel(); //往buffer里面读取数据 ByteBuffer buf = ByteBuffer.allocate(48); //int值表示了有多少字节被读到了Buffer中。若是返回-1,表示到了文件末尾。 int bytesRead = inChannel.read(buf);
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); } //用完FileChannel后必须将其关闭 channel.close();
Selector(选择器)是Java NIO中可以检测一到多个NIO通道,并可以知晓通道是否为诸如读写事件作好准备的组件。这样,一个单独的线程能够管理多个channel,从而管理多个网络链接。
对于操做系统来讲,线程之间上下文切换的开销很大,并且每一个线程都要占用系统的一些资源(如内存)。所以,使用的线程越少越好。
使用Selector.open()方法建立Selector,为了将Channel和Selector配合使用,必须将channel注册到selector上。经过SelectableChannel.register()方法来实现。以下所示
//建立Selector Selector selector = Selector.open(); //向Selector注册通道 channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
与Selector一块儿使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一块儿使用,由于FileChannel不能切换到非阻塞模式。而套接字通道均可以。
register()方法的第二个参数。这是一个“interest集合”,意思是在经过Selector监听Channel时对什么事件感兴趣。能够监听四种不一样类型的事件:
事件类型 | 常量 |
---|---|
Connect | SelectionKey.OP_CONNECT |
Accept | SelectionKey.OP_ACCEPT |
Read | SelectionKey.OP_READ |
Write | SelectionKey.OP_WRITE |
通道触发了一个事件意思是该事件已经就绪。因此,某个channel成功链接到另外一个服务器称为“链接就绪”。一个server socket channel准备好接收新进入的链接称为“接收就绪”。一个有数据可读的通道能够说是“读就绪”。等待写数据的通道能够说是“写就绪”。
当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了一些属性:
interest集合
ready集合
Channel
Selector
附加的对象
interest集合
interest集合是你所选择的感兴趣的事件集合。能够经过SelectionKey读写interest集合,像这样:
int interestSet = selectionKey.interestOps(); boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
用|
位与操做interest 集合和给定的SelectionKey常量,能够肯定某个肯定的事件是否在interest 集合中。
ready集合
ready 集合是通道已经准备就绪的操做的集合。在一次选择(Selection)以后,会首先访问这个ready set。能够这样访问ready集合:
int readySet = selectionKey.readyOps(); selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
能够用像检测interest集合那样的方法,来检测channel中什么事件或操做已经就绪。可是,也可使用以上四个方法,它们都会返回一个布尔类型。
Channel + Selector
从SelectionKey访问Channel和Selector很简单。以下:
Channel channel = selectionKey.channel(); Selector selector = selectionKey.selector();
附加的对象
能够将一个对象或者更多信息附着到SelectionKey上,能够方便识别某个给定的通道。例如,能够附加 与通道一块儿使用的Buffer,或是包含汇集数据的某个对象。使用方法以下:
selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment(); //用register()方法向Selector注册Channel的时候附加对象 SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
一旦向Selector注册了一或多个通道,就能够调用select()方法,选择已经准备就绪的事件的通道类型。
select方法
int select() //阻塞到至少有一个通道在你注册的事件上就绪了 int select(long timeout)//和select()同样,除了最长会阻塞timeout毫秒(参数)。 int selectNow()//不会阻塞,无论什么通道就绪都马上返回
select()方法返回的int值表示有多少通道已经就绪,而后能够经过调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。以下所示:
//访问“已选择键集(selected key set)”中的就绪通道 Set selectedKeys = selector.selectedKeys(); Iterator 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(); }
这个循环遍历已选择键集中的每一个键,并检测各个键所对应的通道的就绪事件。
注意每次迭代末尾的keyIterator.remove()调用。Selector不会本身从已选择键集中移除SelectionKey实例。必须在处理完通道时本身移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。
wakeUp()
某个线程调用select()方法后阻塞了,即便没有通道已经就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法便可。阻塞在select()方法上的线程会立马返回。
若是有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会当即“醒来(wake up)”。
close()
用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的全部SelectionKey实例无效。通道自己并不会关闭。
管道是2个线程之间的单向数据链接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
原理图以下:
//Pipe.open()方法打开管道 Pipe pipe = Pipe.open(); //访问sink通道,向管道写数据 Pipe.SinkChannel sinkChannel = pipe.sink(); //调用SinkChannel的write()方法,将数据写入SinkChannel 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); }
//访问source通道 Pipe.SourceChannel sourceChannel = pipe.source(); //调用source通道的read()方法来读取数据 ByteBuffer buf = ByteBuffer.allocate(48); //read()方法返回的int值表示多少字节被读进了缓冲区 int bytesRead = sourceChannel.read(buf);
参考资料引用