Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。
Java NIO: Channels and Buffers(通道和缓冲区)
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Java NIO: Non-blocking IO(非阻塞IO)
Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
Java NIO: Selectors(选择器)
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
通道可以异步地读写。
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
这些是Java NIO中最重要的通道的实现:
FileChannel:从文件中读写数据。
DatagramChannel:能通过UDP读写网络中的数据。
SocketChannel:能通过TCP读写网络中的数据。
ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
FileChannel文件的操作(FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下?):transferFrom(),transferTo()实现通道之间的数据交换
//最后两行代码实现的结果是一样的 RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); //transferFrom toChannel.transferFrom(position, count, fromChannel); //transferTo fromChannel.transferTo(position, count, toChannel);
FileChannel的方法:
read(buf):从fileChannel读取数据到buffer.
write(buf):从buffer向FileChannel写数据.
close():用完FileChannel后必须将其关闭.
size():获得关联文件的大小
truncate(Int):截取一个文件的部分字段。
force(bool):将文件数据(原来存在内存中)和元数据强制写到磁盘上.
position():获取FileChannel的当前位置
position(int):设置FileChannel的当前位置
打开一个SocketChannel并连接到互联网上的某台服务器。一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。SocketChannel两个创建方式 :
阻塞模式和非阻塞模式:阻塞模式等到相应的事件之后返回。非阻塞模式:连接就立即放回(可能在连接未完成就返回了)
SocketChannel的方法:
open():直接通过SocketChannel.open()打开一个通道。其他都是使用在实例上的方法。
connnect
():建立连接
read(buf):从SocketChannel读取数据到buffer.
write(buf):从buffer向SocketChannel写数据.
close():用完SocketChannel后将其关闭.
ServerSocketChannel:监听新进来的TCP连接的通道
例子:
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... } }
DatagramChannel:收发UDP包的通道
例子:
//发送数据 String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80)); //接收数据: ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); channel.receive(buf);
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。数据是从通道读入缓冲区,从缓冲区写入到通道中的。
Buffer的基本用法
使用Buffer读写数据一般遵循以下四个步骤:
写入数据到Buffer
调用flip()
方法(filp()切换写到读模式)
从Buffer中读取数据
调用clear()
方法或者compact()
方法(clear()清空所有数据,compact()方法只会清除已经读过的数据,并移动数据到开始位置)
Buffer的工作原理
三个属性capacity(容量),position(读/写指针的位置),limit(本次读/写大小)
buffer方法
read():从Channel写数据到Buffer
put() :放入数据
wirte() : 从Buffer读取数据到Channel,和read对应
get() : 从Buffer中读取数据
flip() : 将Buffer从写模式切换到读模式
rewind() :重新读取数据
mark() : 标记buffer位置
reset() : 回到标记buffer位置
注:支持批量读取/写入数据(通过buffer数组来操作)
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
selector使用的简单步骤:
打开一个selector
注册通道(channel)到selector
监听四种事件(接受,连接,读,写),并书写相关逻辑代码。
代码:
Selector selector = Selector.open(); channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ); while(true) { int readyChannels = selector.select(); if(readyChannels == 0) continue; 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();//必须移除操作完成的channel事件 } }
当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了许多属性:
interest集合(就是四种事件[ 接受,连接,读,写 ]的集合[int])
ready集合(就是四种事件[ 接受,连接,读,写 ]是否可用的集合[int])
Channel
Selector
interest集合就是一个整数(interestSet),使用二进制操作(& |)
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;
ready 集合是通道已经准备就绪的操作的集合,也是一个整数。
int readySet = selectionKey.readyOps();//获取整个集合 //单个获取 selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
selector和channel:
Channel channel = selectionKey.channel(); Selector selector = selectionKey.selector();
附加对象:自己添加对象,标识通道
selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment(); //还可以在用register()方法向Selector注册Channel的时候附加对象。如: SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用 select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。
如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。
用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。
一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。换句话说,如果你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。
下面是select()方法:
int select()
int select(long timeout)
int selectNow()
select()
阻塞到至少有一个通道在你注册的事件上就绪了。
select(long timeout)
和select()一样,除了最长会阻塞timeout毫秒(参数)。
selectNow()
不会阻塞,不管什么通道就绪都立刻返回
Java NIO 管道是2个线程之间的单向数据连接。Pipe
有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
这里是Pipe原理的图示:
创建管道
通过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); int bytesRead = sourceChannel.read(buf);
read()
方法返回的int值会告诉我们多少字节被读进了缓冲区。