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。如下圖所示:
ReadAndWrite.png
Channel的實現:
ChannelImple.png
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的類型
BufferImpl.png 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 :
ScatteringReads.png
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 :
GatheringWrites.png
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<SelectionKey> selectionKeys = selector.selectedKeys();
//當像Selector注冊Channel時,Channel.register()方法會返回一個SelectionKey 對象。
//這個對象代表了注冊到該Selector的通道。可以通過SelectionKey的selectedKeySet()方法訪問這些對象。
Iterator<SelectionKey> 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 :
Pipe Internals.png
//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的選擇器允許一個單獨的線程來監視多個輸入通道,你可以注冊多個通道使用一個選擇器,然后使用一個單獨的線程來“選擇”通道:這些通道里已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。