http://tutorials.jenkov.com/java-nio/index.html 原文地址
Java NIO (New IO) is an alternative IO API for Java (from Java 1.4), meaning alternative to the standard Java IO and Java Networking API's. Java NIO offers a different way of working with IO than the standard IO API's
Java NIO: Channels and Buffers
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Java NIO: Non-blocking IO ( 非阻塞IO )
Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。Java NIO: Selectors ( 选择器 )
Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
Channel
jdk中的注释 :
A nexus for I/O operations.
A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.
Java NIO Channels 和Stream类似,但又有些不同:
Channel既可以从通道中读取数据,又可以写数据到通道。但流通常是只读或只写的。
Channel可以异步地读写。
Channel总是读数据到一个Buffer,或者从一个Buffer中写数据到Channel。如下图所示:
Channel的实现:
FileChannel : 从文件中读写数据
DatagramChannel : 通过UDP读写网络中的数据
SocketChannel : 能通过TCP读写网络中的数据
ServerSocketChannel : 可以监听指定端口TCP连接,对每一个新进来的连接都创建一个SocketChannel。
Buffer
使用Buffer读写数据一般遵循四个步骤 : 1. Channel.Read数据到Buffer中,或者直接Buffer.put(数据)。 2. 调用 Buffer.flip()切换Buffer到读模式。 3. Buffer.get()读取字节或者Channel.write(buffer)将Buffer中数据写入Channel。4. 调用Buffer.clear()或者Buffer.compact()。clear()方法清空整个缓冲区。compact()方法只会清除已读过的数据,未读数据移到Buffer的起始处。 下面是一个负值文件的example :
String infile = "nio/src/main/resources/CopyFile.java";
String outfile = "nio/src/main/resources/CopyFile.java.copy";
File file = new File(infile);
FileInputStream fin = new FileInputStream(file);
FileOutputStream fout = new FileOutputStream(outfile);
FileChannel finChannel = fin.getChannel();
FileChannel foutChannel = fout.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = finChannel.read(buffer);
if (read == -1) break;
buffer.flip();
foutChannel.write(buffer);
}
- Buffer 的 capacity position limit
capacity : Buffer内存块的上限值,一旦Buffer满了需要清空数据才能继续往Buffer写数据。
position : 写模式时,标识当前写的位置,初始值为0,最大值小于capacity。读模式时,标识当前读位置。写模式切换到读模式,position会被重置为0。
limit : 在写模式下,limit==capacity,读模式下,limit表示能读到的数据最高位置。因此,切换Buffer到读模式时,limit会被设置成写模式下position值。
-
Buffer的类型
Buffer api
//Buffer内存分配
ByteBuffer buffer = ByteBuffer.allocate(1024); //分配一个1024capacity的ByteBuffer
//向Buffer中写入数据
finChannel.read(buffer); //read data from channel into buffer
buffer.put((byte) 2); //put data into buffer
//flip()
buffer.flip(); //将Buffer从写模式切换到读模式
//从Buffer中读取数据
buffer.get(); //直接从Buffer中get数据
foutChannel.write(buffer); //将Buffer中数据写入到Channel
//rewind() 将position设置回0,可以用此重复读Buffer。
buffer.rewind();
//clear() 与compact()
buffer.clear() //position设置回0,limit等于capacity,进入写模式。
buffer.compact() //将未读数据拷贝到起始处,position设置到没有元素的位置,新写入数据不会覆盖未读数据。
//mark() reset()
buffer.mark(); //标记当前position
buffer.get(); //get()一次 position后移
buffer.reset(); //reset()position 复位到mark的位置
//equals() 与 compareTo()
buffer1.equals(buffer2); //比较Buffer重的剩余未读元素(position到limit之间的元素)相等
buffer1.compareTo(buffer2); //比较元素 第一个不相等的元素比较大小 或者 元素全部相等 元素个数多少比较
Scatter/Gather
A scattering read from a channel is a read operation that reads data into more than one buffer.
分散读是指从Channel中读取数据写入到多个Buffer中,下面是示例图和Code :
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = {header, body};
channel.read(buffers);
A gathering write to a channel is a write operation that writes data from multiple buffers into a single channel.
聚集写是指从多个Buffer中向一个Channel里写数据,下面是示例图和Code :
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
Channel to Channel Transfers
- transFrom()
In Java NIO you can transfer data directly from one channel to another, if one of the channels is a FileChannel. The FileChannel class has a transferTo() and a transferFrom() method which does this for you.
FileChannel finChannel = fin.getChannel();
FileChannel foutChannel = fout.getChannel();
long position = 0;
long size = finChannel.size();
foutChannel.transferFrom(finChannel,position,size);
- transTo
The transferTo() method transfer from a FileChannel into some other channel. Here is a simple example:
FileChannel finChannel = fin.getChannel();
FileChannel foutChannel = fout.getChannel();
long position = 0;
long size = finChannel.size();
finChannel.transferTo(1,size,foutChannel);
Selector
A Selector is a Java NIO component which can examine one or more NIO Channel's, and determine which channels are ready for e.g. reading or writing. This way a single thread can manage multiple channels, and thus multiple network connections.
- 为什么使用Selector
仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。但是,需要记住,现代的操作系统和CPU在多任务方面表现的越来越好,所以多线程的开销随着时间的推移,变得越来越小了。实际上,如果一个CPU有多个内核,不使用多任务可能是在浪费CPU能力。
- Selector api
SocketChannel channel = SocketChannel.open();
//创建Selector
Selector selector = Selector.open();
//向Selector注册通道
//Channel必须处于非阻塞模式下才能与Selector使用,这意味着FileChannel不能使用Selector
channel.configureBlocking(false);
//将Channel注册到Selector上 并表示是可读的 返回key
SelectionKey key = channel.register(selector, (SelectionKey.OP_READ | SelectionKey.OP_WRITE));
// 通道四种interest状态 如果不止一种状态可以用 | 传多个
// SelectionKey.OP_READ;
// SelectionKey.OP_ACCEPT;
// SelectionKey.OP_CONNECT;
// SelectionKey.OP_WRITE;
//SelectionKey interestSet
int interestOps = key.interestOps(); //interest集合 返回了之前注册的 OP_READ | OP_WRITE == 5
int readyOps = key.readyOps(); //ready集合 是已经准备就绪的操作集合 一次Selection后会首先访问这个ready set
//可通过key获取 channel 和 selector
SelectableChannel selectableChannel = key.channel();
Selector selector1 = key.selector();
//可以给key添加附加对象 标识区分key
Object attach = key.attach(new Buffer[]{});
Object attachment = key.attachment();
//通过Selector选择通道
//向Selector注册了通道后,可以调用select()方法,返回的int值表示多少通道已经就绪,这是个同步阻塞方法,若无就绪通道会等待直到有就绪通道.
//selector.wakeup(); 可以唤醒阻塞select()方法 立马返回
int ready = selector.select();
//在确认ready channel > 0 后 获取就绪通道注册的keys
Set selectionKeys = selector.selectedKeys();
//当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。
//这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象。
Iterator keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
SelectionKey next = keyIterator.next();
if (next.isAcceptable()){
SelectableChannel readyAcceptChannel = next.channel(); //获取通道
}
keyIterator.remove();
}
selector.close();//关闭selector 注册的keys全部失效 channel不会失效
FileChannel
A Java NIO FileChannel is a channel that is connected to a file. Using a file channel you can read data from a file, and write data to a file.
SocketChannel
A Java NIO SocketChannel is a channel that is connected to a TCP network socket.
//Open a SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",8080));
//Reading data from a SocketChannel into Buffer
ByteBuffer buffer = ByteBuffer.allocate(48);
socketChannel.read(buffer);
//Writing data from Buffer into SocketChannel
buffer.flip();
while (buffer.hasRemaining()){
socketChannel.write(buffer);
}
//Configure the SocketChannel be non-blocking mode,the method may return before a connection is established.
//Then call the finishConnect() method to determine whether connection is established.
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("localhost",8080));
if (socketChannel.finishConnect()) {
//TODO
}
//Closing a SocketChannel
socketChannel.close();
ServerSocketChannel
A Java NIO ServerSocketChannel is a channel that can listen for incoming TCP connections.
//Opening a ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//Binds the {@code ServerSocket} to a specific address (IP address and port number).
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true){
//Listening for Incoming Connections
//The accept() method blocks until an incoming connection arrives,and returns a SocketChannel.
SocketChannel socketChannel = serverSocketChannel.accept();
//TODO ...
//In non-blocking mode the accept() method returns immediately,
// if no incoming connection had arrived,you will have to check if the returned channel is null.
serverSocketChannel.configureBlocking(false);
if (socketChannel!=null){
//TODO ...
}
//Closing a ServerSocketChannel
serverSocketChannel.close();
}
DatagramChannel
A Java NIO DatagramChannel is a channel that can send and receive UDP packets.
Pipe
A Java NIO Pipe is a one-way data connection between two threads. A Pipe has a source channel and a sink channel. You write data to the sink channel. This data can then be read from the source channel.Here is an illustration of the Pipe principle :
//Creating a Pipe.
Pipe pipe = Pipe.open();
//Writing to a Pipe.
Pipe.SinkChannel sinkChannel = pipe.sink();//To write to a pipe you need to access the sink channel.
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.put(new byte[]{1,2,3,4,5});
buffer.flip();
while (buffer.hasRemaining()){
sinkChannel.write(buffer); //Wiriting data from buffer into pipe.sinkChannel
}
//Reading from a Pipe
Pipe.SourceChannel sourceChannel = pipe.source(); //To read from a pipe you need to access the channel.
ByteBuffer buffer2 = ByteBuffer.allocate(48);
int read = sourceChannel.read(buffer2);//Reading data from pipe.sourceChannel into buffer.
buffer.flip();
buffer2.flip();
System.out.println(buffer.equals(buffer2)); //true
NIO和IO比较
- Stream VS Buffer
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
- Blocking vs. Non-blocking IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
- Selectors
ava NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。