Java NIO
Contents
http://www.cnblogs.com/xdp-gacl/p/3634409.html
NIO 提供了一个全新的底层IO 模型,与最初Java.io包中面向流的概念不同,NIO采用了面向块的概念,在尽可能的情况下,将以大的数据块为单位进行操作,而不是每次一个字节或者一个字符。
NIO 提供了与平台无关的非阻塞IO,与面向线程,阻塞式的IO 方式相比,多道通信,非阻塞IO可以使程序更有效的处理大量连接的情况。
IO 与NIO
IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
常见I/O模型对比
所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。
下图是几种常见I/O模型的对比:
以socket.read()为例子:
传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。
换句话说,BIO里用户最关心“我要读”,NIO里用户最关心”我可以读了”,在AIO模型里用户更需要关注的是“读完了”。
NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。
##缓冲区与Buffer
基本IO 操作中所有操作都是直接以流的形式完成的,而在NIO中,所有操作都要使用到缓冲区处理,且所有的读写操作都是通过缓冲区完成的,缓冲区是一个线性的,有序的数据集,只能容纳某种特定的数据类型。可以把换冲区想象成一个水桶,总是水满了再进行和通道,内存交互,这样就减少了IO.详见http://www.cnblogs.com/xdp-gacl/p/3634409.html。
NIO中针对每一种基本数据类型都有一种对应的缓冲区操作类.
buffer的基本操作
Java.io.Buffer本身是一个抽象类
buffer 类常用方法
新IO 针对每一种数据类型都有一种对应的缓冲区操作类,Java.nio.ByteBuffer java.nio.CharBuffer java.nio.ShortBuffer java.nio.IntBuffer java.nio.LongBuffer java.nio.FloatBuffer java.nio.DoubleBuffer
1 | import java.nio.IntBuffer; |
在每次写入之后position会有变化。而当调用flip()方法时,position 和limit发生变化。
深入缓冲区操作
position 表示下一个缓冲区读取或者写入的操作指针,写入数据时,此指针就会改变。 limit 表示还有多少数据需要存储或者读取。limit指的是缓冲区中第一个不能读写的元素的数组下标索引,也可以认为是缓冲区中实际元素的数量(也就是上界)。position<=limit。 capacity 表示缓冲区的最大容量。在分配缓冲区时设置,一般不会改变。
创建子缓冲区
可以使用各个缓冲区类的slice() 方法从一个缓冲区创建一个新的子缓冲区,子缓冲区与原缓冲区中部分数据可以共享。
创建只读缓冲区
1 | IntBuffer buf =IntBuffer.allocate(10); |
通道(NIO把它支持的I/O对象抽象为Channel)
(通道和流的区别之处在于通道是双向的,流是单向的.)
通道可以用来读取和写入数据,通道类似于之前的输入输出流,程序不会直接操作通道,所有的内容都是先读到或者写入到缓冲区中,再通过缓冲区中取得或者写入的通道本身是双向操作的,既可以完成输入也可以输出。
Channel 本身是一个接口,
FileChannel
FileChannel 是Channel的子类,可以进行文件的读写操作,如果要使用FileChannel,则要依靠FileInputStream 或者FileOutputStream 类中的getChannel() 方法取得输入或者输出的通道。
1 | public class Demo() |
Selector
原来使用Io和socket构造网络服务时,所有的网络服务将使用阻塞的方式进行客户端的连接,而如果使用了新IO则可以构造一个非阻塞的网络服务。
要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。
NIO中的处理流程一般为
由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。
一般情况下,使用NIO,主要包括了下面三种要用到的线程。
- 事件分发器,单线程选择就绪的事件。
- I/O处理器,包括connect、read、write等,这种纯CPU操作,一般开启CPU核心个线程就可以。
- 业务线程,在处理完I/O后,业务一般还会有自己的业务逻辑,有的还会有其他的阻塞I/O,如DB操作,RPC等。只要有阻塞,就需要单独的线程。
其中,事件分发器的作用,即 将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁谁谁的快递到了, 快来拿吧!开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数。
Java NIO (Channels and Buffers)
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和 缓冲区(Buffer) 进行操作,数据总是从通道读到缓冲区,或者从缓冲区读写到通道中。NIO 还可以让你使用非阻塞的IO。NIO还引入了选择器的概念,选择器用于监听多个通道的事件。NIO的核心API就是Channel Buffer Selector
Channel 和 Buffer
基本上,所有的NIO都是从一个Channel开始,Channel 有点像流,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel 中,
Selector
selector 允许单线程处理多个Channel,如果一个应用打开了多个Channel,但是每个Channel的流量都很低。使用Selector会很方便。下图是一个单线程中使用一个Selector处理三个Channel的图示
Channel(流要写具体的流,才有getChannel方法,Channel 也是)
通道类似于流,但又有些不同。既可以从通道中读取数据,又可以写数据到通道,而流的读写通常是单向的。通道可以异步的读写,通道中的数据总是要先读到一个Buffer,或者从一个Buffer中写入。具体的实现类有 FileChannel,DatagramChannel通过UDP读写网络中的数据, SocketChannel 能通过TCP 读写网络中的数据,ServerSocketChannel 以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
1 | public static void main(String[] args) throws Exception |
Buffer
NIO 中的Buffer用于和NIO通道进行交互,缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装为NIO Buffer 对象,并提供一组方法,进行方便的访问。
使用Buffer 读写数据一般需要遵循以下四个步骤:
1 | 1. 写入数据到Buffer |
当向Buffer写入数据时,buffer会记录写入了多少数据,一旦需要读取数据,就需要通过flip()方法将Buffer从写模式切换为读模式,在读模式下,就可以读取之前写入到Buffer的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方法可以清空缓冲区,调用clear() 或者compact() 方法,clear() 方法会清空整个缓冲区,compact()方法只会清除已经读过的数据。任何未读的数据都会被移到缓冲区的起始处,新写入的数据会放到缓冲区未读数据的后面。
Buffer的三个工作属性 capacity position limit
Position 和limit的含义取决于Buffer在读模式还是写模式,
position(指向下一个可以读或者可以写的位置)
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0写数据后,position会移动到下一个可以写的位置。当切换到读模式的时候,position会被重置为0.当从Buffer的position出读取数据时,position向前移动到下一个可读的位置。
limit(读模式下表示最多能读的数据,写模式下最多能写的数据)
写模式下,Buffer的limit表示最多可以往里面写多少数据,所以写模式下,limit等于capacity。当切换到读模式时,limit表示你最多能读多少数据,因此,当切换到读模式时,limit设置成写模式的position。换句话说,你可以读到之前写入的所有数据。
Buffer的分配
要想获得一个Buffer首先要分配,每一个Buffer都有一个allocate方法。
1 | ByteBuffer buf = ByteBuffer.allocate(48);//分配一个48字节的Buffer。 |
向Buffer中写数据
两种方式,从Channel写到Buffer 、通过Buffer的put()方法写到Buffer里。
1 | int bytesRead = inChannel.read(buf);//从Channel写待Buffer |
flip() 方法
flip将写模式转换为读模式,调用flip()方法将会将position设回0,limit设置成原来的position。
从Buffer中读取数据
两种方式,1,从Buffer读数据到Channel 2. 使用get() 从Buffer读取数据。
1 | int byteWritten =inChannel.write(buf); |
rewind()
Buffer.rewind()将position设回0,所以可以重读Buffer中的所有数据limit保持不变,仍然表示能从Buffer中读取多少元素。
clear() 和compact()
Scatter/Gather
Java NIO开始支持scatter/gather.
Scatter/gather用于描述从Channel中读取或者写入到channel的操作
分散scatter 从Channel中读取是指在读操作时将读取的数据写入到多个Buffer中。
聚集 gather 写入Channel指在写操作时将多个Buffer的数据写到同一个Channel,因此,Channel将多个Buffer中的数据聚集后发送到Channel。
scatter/gather 经常用于需要将传输的数据分开处理的场合,例如传输一个消息头消息体组成的消息,这样将消息体消息头分散到不同的Buffer中。
1 | //scatter read |
1 | //gather write |
通道之间的数据传输
在NIO中,如果两个通道中一个是FileChannel,那你可以直接将数据从一个Channel传输到另外一个Channel中。
FileChannel的transferFrom()
方法可以将数据从源通道传输到FileChannel中。
transferTo()
方法将数据从FileChannel传输到其它Channel中。
1 | RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); |
1 | RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); |
Selector
https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html
Selector是NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程就可以管理多个Channel,从而管理多个网络连接。
Selector提供选择已经就绪的任务的能力
一个Selector实例可以同时检查一组信道的I/O状态。用专业术语来说,选择器就是一个多路开关选择器,因为一个选择器能够管理多个信道上的I/O操作。然而如果用传统的方式来处理这么多客户端,使用的方法是循环地一个一个地去检查所有的客户端是否有I/O操作,如果当前客户端有I/O操作,则可能把当前客户端扔给一个线程池去处理,如果没有I/O操作则进行下一个轮询,当所有的客户端都轮询过了又接着从头开始轮询;这种方法是非常笨而且也非常浪费资源,因为大部分客户端是没有I/O操作,我们也要去检查;而Selector就不一样了,它在内部可以同时管理多个I/O,当一个信道有I/O操作的时候,他会通知Selector,Selector就是记住这个信道有I/O操作,并且知道是何种I/O操作,是读呢?是写呢?还是接受新的连接;所以如果使用Selector,它返回的结果只有两种结果,一种是0,即在你调用的时刻没有任何客户端需要I/O操作,另一种结果是一组需要I/O操作的客户端,这是你就根本不需要再检查了,因为它返回给你的肯定是你想要的。这样一种通知的方式比那种主动轮询的方式要高效得多!
简单的说,selector就是多路复用器.Selector 会不断的轮询注册在其上的Channel,如果某个channel上面发生读或者写事件,这个Channel就会处于就绪状态,会被selector轮询出来,然后通过selectionKey可以获取到就绪Channel的集合,进行后续的IO操作.
一个多路复用器selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它没有最大连接句柄1024/2048的限制,这意味着只需要一个线程负责selector的轮询,就可以接入成千上万客户端.
使用Selector的原因
用单个线程来处理多个channel的好处是 只需要更少的线程来处理通道。所以使用selector 能够处理多个通道。
一个线程轮询selector ,当有IO时,从线程池中取出线程进行处理.
并且不必为每个连接都创建一个线程,不用去维护多个线程,
用法
我们需要做的第一件事就是创建一个 Selector
:
1 | Selector selector = Selector.open(); |
然后,我们将对不同的通道对象调用 register()
方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。register()
的第一个参数总是这个 Selector
。
为了接收连接,我们需要一个 ServerSocketChannel
。事实上,我们要监听的每一个端口都需要有一个 ServerSocketChannel
。对于每一个端口,我们打开一个 ServerSocketChannel
,如下所示:
1 | ServerSocketChannel ssc = ServerSocketChannel.open(); |
第一行创建一个新的 ServerSocketChannel
,最后三行将它绑定到给定的端口。第二行将 ServerSocketChannel
设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法,否则异步 I/O 就不能工作。
下一步是将新打开的 ServerSocketChannels
注册到 Selector
上。为此我们使用 ServerSocketChannel.register() 方法,如下所示:
1 | SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT ); |
register()
的第一个参数总是这个 Selector
。第二个参数是 OP_ACCEPT
,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。这是适用于 ServerSocketChannel
的唯一事件类型。
请注意对 register()
的调用的返回值。 SelectionKey
代表这个通道在此 Selector
上的这个注册。当某个 Selector
通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey
来进行的。SelectionKey
还可以用于取消通道的注册。
现在已经注册了我们对一些 I/O 事件的兴趣,下面将进入主循环。使用 Selectors
的几乎每个程序都像下面这样使用内部循环:
1 | int num = selector.select(); |
首先,我们调用 Selector
的 select()
方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select()
方法将返回所发生的事件的数量。
接下来,我们调用 Selector
的 selectedKeys()
方法,它返回发生了事件的 SelectionKey
对象的一个 集合
。
我们通过迭代 SelectionKeys
并依次处理每个 SelectionKey
来处理事件。对于每一个 SelectionKey
,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。
FileChannel
Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。所以无法使用selector
打开FileChannel
使用FileChannel前,必须先打开它,但是无法直接打开,必须通过使用一个InputStream OutputStream RandomAccessFile来获取一个FileChannel实例。下面是通过RandomAccessFile打开FileChannel的示例。
1 | RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); |
从FileChannel读取数据
调用多个read()方法从FileChannel读取数据
1 | ByteBuffer buf = ByteBuffer.allocate(48); |
首先,分配一个Buffer。从FileChannel中读取的数据将被读到Buffer中。然后,调用FileChannel.read()方法。该方法将数据从FileChannel读取到Buffer中。read()方法返回的int值表示了有多少字节被读到了Buffer中。如果返回-1,表示到了文件末尾。
从FileChannel写数据
1 | String newData = "New String to write to file..." + System.currentTimeMillis(); |
注意FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。
关闭FileChannel
1 | channel.close(); |
fileChannel的position 方法
特定位置进行读写,也可以调用position(Long pos)方法 设置FileChannel当前位置
1 | long pos = channel.position(); |
size()
FileChannel实例的size()方法将返回该实例所关联文件的大小。如:
1 | long filesize = channel.size(); |
force()
FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。
Author: corn1ng
Link: https://corn1ng.github.io/2017/10/18/Java NIO/
License: 知识共享署名-非商业性使用 4.0 国际许可协议