摘要:当数据被写入到缓冲区时,线程可以继续处理它。当满足下列条件时,表示两个相等有相同的类型等。调用通道的方法时,可能会导致线程暂时阻塞,就算通道处于非阻塞模式也不例外。当一个通道关闭时,休眠在该通道上的所有线程都将被唤醒并收到一个异常。
1、NIO和I/O区别
I/O和NIO的区别在于数据的打包和传输方式。
I/O流的方式处理数据
NIO块的方式处理数据
面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。面向流的 I/O 通常相当慢。
一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的I/O比较复杂。NIO的主要应用在高性能、高容量服务端应用程序。
IO | NIO |
---|---|
面向流 | 面向块,缓冲 |
阻塞IO | 非阻塞IO |
无 | Selector |
1、Channels and Buffers(通道和缓冲区)
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
2、Non-blocking IO(非阻塞IO)
Java NIO可以非阻塞的方式使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
3、Selectors(选择器)
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
通道和缓冲区是 NIO 中的核心对象,几乎在每一个 I/O 操作中都要使用它们。
一个 Buffer 实质上是一个容器对象,它包含一些要写入或者刚读出的数据。
在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。
缓冲区实质上就是一个数组,通常它是一个字节数组,但是也可以使用其他种类的数组。但它不仅仅是一个数组,缓冲区还提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
Buffer 类型最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。
ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型:
Buffer抽象类中提供了需要处理的方法类型。
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); } }capacity & position & limit
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成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。
Buffer的分配clear()与compact()区别
1、clear()方法,position将被设回0,limit被设置成 capacity的值。Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”
2、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写入
写数据到Buffer有两种方式:
从Channel写到Buffer。
通过Buffer的put()方法写到Buffer里。
//从Channel写到Buffer int bytesRead = inChannel.read(buf); //read into buffer. //通过put方法写Buffer的例子: buf.put(127);Buffer读取
从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。
Buffer比较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的元素个数比另一个少
3、ChannelJava NIO的通道类似流,但又有些不同:
既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
通道可以异步地读写。
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
Channel类型FileChannel 从文件中读写数据。
DatagramChannel 能通过UDP读写网络中的数据。
SocketChannel 能通过TCP读写网络中的数据。
ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
close Channel与缓冲区不同,通道不能被重复使用;关闭通道后,通道将不再连接任何东西,任何的读或写操作都会导致ClosedChannelException。
调用通道的close()方法时,可能会导致线程暂时阻塞,就算通道处于非阻塞模式也不例外。如果通道实现了InterruptibleChannel接 口,那么阻塞在该通道上的一个线程被中断时,该通道将被关闭,被阻塞线程也会抛出ClosedByInterruptException异常。
当一个通道 关闭时,休眠在该通道上的所有线程都将被唤醒并收到一个AsynchronousCloseException异常。
Scatter/Gatherscatter/gather用于描述从Channel中读取或者写入到Channel的操作。
分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据"分散(scatter)"到多个Buffer中。
聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据"聚集(gather)"后发送到Channel。
scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。
Scatter ReaderByteBuffer 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 WritesGathering 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能较好的处理动态消息。
FileChannelJava NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。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);FileChannel写数据
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();4、Selector
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个多带带的线程可以管理多个channel,从而管理多个网络连接。
对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。
Selector的创建与注册使用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准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。
SelectionKey当向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选择通道
一旦向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实例无效。通道本身并不会关闭。
5、 管道(pip)管道是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);
参考资料引用
1、NIO 入门
2、Java NIO教程
3、理解Java NIO
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/66975.html
摘要:从通道进行数据写入创建一个缓冲区,填充数据,并要求通道写入数据。三之通道主要内容通道介绍通常来说中的所有都是从通道开始的。从中选择选择器维护注册过的通道的集合,并且这种注册关系都被封装在当中停止选择的方法方法和方法。 由于内容比较多,我下面放的一部分是我更新在我的微信公众号上的链接,微信排版比较好看,更加利于阅读。每一篇文章下面我都把文章的主要内容给列出来了,便于大家学习与回顾。 Ja...
摘要:的选择器允许单个线程监视多个输入通道。一旦执行的线程已经超过读取代码中的某个数据片段,该线程就不会在数据中向后移动通常不会。 1、引言 很多初涉网络编程的程序员,在研究Java NIO(即异步IO)和经典IO(也就是常说的阻塞式IO)的API时,很快就会发现一个问题:我什么时候应该使用经典IO,什么时候应该使用NIO? 在本文中,将尝试用简明扼要的文字,阐明Java NIO和经典IO之...
摘要:而我们现在都已经发布了,的都不知道,这有点说不过去了。而对一个的读写也会有响应的描述符,称为文件描述符,描述符就是一个数字,指向内核中的一个结构体文件路径,数据区等一些属性。 前言 只有光头才能变强 回顾前面: 给女朋友讲解什么是代理模式 包装模式就是这么简单啦 本来我预想是先来回顾一下传统的IO模式的,将传统的IO模式的相关类理清楚(因为IO的类很多)。 但是,发现在整理的过程已...
摘要:上篇说了最基础的五种模型,相信大家对相关的概念应该有了一定的了解,这篇文章主要讲讲基于多路复用的。 上篇说了最基础的五种IO模型,相信大家对IO相关的概念应该有了一定的了解,这篇文章主要讲讲基于多路复用IO的Java NIO。 背景 Java诞生至今,有好多种IO模型,从最早的Java IO到后来的Java NIO以及最新的Java AIO,每种IO模型都有它自己的特点,详情请看我的上...
摘要:的出现解决了这尴尬的问题,非阻塞模式下,通过,我们的线程只为已就绪的通道工作,不用盲目的重试了。注意要将注册到,首先需要将设置为非阻塞模式,否则会抛异常。 showImg(https://segmentfault.com/img/remote/1460000017053374); 背景知识 同步、异步、阻塞、非阻塞 首先,这几个概念非常容易搞混淆,但NIO中又有涉及,所以总结一下。 ...
阅读 2675·2021-11-16 11:53
阅读 2737·2021-07-26 23:38
阅读 2073·2019-08-30 15:55
阅读 1751·2019-08-30 13:21
阅读 3650·2019-08-29 17:26
阅读 3305·2019-08-29 13:20
阅读 875·2019-08-29 12:20
阅读 3192·2019-08-26 10:21