1、NIO和I/O区别
I/O和NIO的区别在于数据的打包和传输方式。
I/O流的方式处理数据
NIO块的方式处理数据
面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。面向流的 I/O 通常相当慢。
一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的I/O比较复杂。NIO的主要应用在高性能、高容量服务端应用程序。
IO | NIO |
---|---|
面向流 | 面向块,缓冲 |
阻塞IO | 非阻塞IO |
无 | Selector |
NIO的核心梳理
1、Channels and Buffers(通道和缓冲区)
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
2、Non-blocking IO(非阻塞IO)
Java NIO可以非阻塞的方式使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
3、Selectors(选择器)
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
2、Buffers(缓冲区)
通道和缓冲区是 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。
clear()与compact()区别
1、clear()方法,position将被设回0,limit被设置成 capacity的值。Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”
2、compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。
Buffer的分配
在能够读和写之前,必须有一个缓冲区。要创建缓冲区,必须要进行分配,。我们使用静态方法 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、Channel
Java NIO的通道类似流,但又有些不同:
既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
通道可以异步地读写。
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
Channel类型
FileChannel 从文件中读写数据。
DatagramChannel 能通过UDP读写网络中的数据。
SocketChannel 能通过TCP读写网络中的数据。
ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
close Channel
与缓冲区不同,通道不能被重复使用;关闭通道后,通道将不再连接任何东西,任何的读或写操作都会导致ClosedChannelException。
调用通道的close()方法时,可能会导致线程暂时阻塞,就算通道处于非阻塞模式也不例外。如果通道实现了InterruptibleChannel接 口,那么阻塞在该通道上的一个线程被中断时,该通道将被关闭,被阻塞线程也会抛出ClosedByInterruptException异常。
当一个通道 关闭时,休眠在该通道上的所有线程都将被唤醒并收到一个AsynchronousCloseException异常。
Scatter/Gather
scatter/gather用于描述从Channel中读取或者写入到Channel的操作。
分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据"分散(scatter)"到多个Buffer中。
聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据"聚集(gather)"后发送到Channel。
scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。
Scatter Reader
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
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能较好的处理动态消息。
FileChannel
Java 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);
参考资料引用
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。