概述
Java NIO提供了與標準IO不同的IO工作方式: - Channels and Buffers(通道和緩衝區):標準的IO基於字節流和字符流進行操作的,而NIO是基於通道(Channel)和緩衝區(Buffer)進行操作,數據總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。
- Asynchronous IO(異步IO):Java NIO可以讓你異步的使用IO,例如:當線程從通道讀取數據到緩衝區時,線程還是可以進行其他事情。當數據被寫入到緩衝區時,線程可以繼續處理它。從緩衝區寫入通道也類似。
- Selectors(選擇器):Java NIO引入了選擇器的概念,選擇器用於監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個的線程可以監聽多個數據通道。
使用場景
NIO
- 優勢在於一個線程管理多個通道;但是數據的處理將會變得複雜;
- 如果需要管理同時打開的成千上萬個連接,這些連接每次只是發送少量的數據,採用這種;
傳統的IO
- 適用於一個線程管理一個通道的情況;因爲其中的流數據的讀取是阻塞的;
- 如果需要管理同時打開不太多的連接,這些連接會發送大量的數據;
NIO vs IO區別
NIO vs IO之間的理念上面的區別(NIO將阻塞交給了後臺線程執行)
- IO是面向流的,NIO是面向緩衝區的
- Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方;
- NIO則能前後移動流中的數據,因爲是面向緩衝區的
- IO流是阻塞的,NIO流是不阻塞的
- Java IO的各種流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再幹任何事情了
- Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取。NIO可讓您只使用一個(或幾個)單線程管理多個通道(網絡連接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜。
- 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。
- 選擇器
- Java NIO的選擇器允許一個單獨的線程來監視多個輸入通道,你可以註冊多個通道使用一個選擇器,然後使用一個單獨的線程來“選擇”通道:這些通道里已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。
Java NIO 由以下幾個核心部分組成:
基本上,所有的 IO 在NIO 中都從一個Channel 開始。Channel 有點象流。 數據可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中。這裏有個圖示:
Channel
Channel的實現: (涵蓋了UDP 和 TCP 網絡IO,以及文件IO)
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
讀數據:
- int bytesRead = inChannel.read(buf);
寫數據:
- int bytesWritten = inChannel.write(buf);
還有部分的使用,如配置Channel爲阻塞或者非阻塞模式,以及如何註冊到Selector上面去,參考Selector部分;
Buffer
Buffer實現: (byte, char、short, int, long,
float, double )
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
Buffer使用
讀數據
- flip()方法
- 將Buffer從寫模式切換到讀模式
- 調用flip()方法會將position設回0,並將limit設置成之前position的值。
- buf.flip();
- (char) buf.get()
- Buffer.rewind()
- 將position設回0,所以你可以重讀Buffer中的所有數據
- limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)
- Buffer.mark()方法,可以標記Buffer中的一個特定position。之後可以通過調用
- Buffer.reset()方法,恢復到Buffer.mark()標記時的position
- 一旦讀完了所有的數據,就需要清空緩衝區,讓它可以再次被寫入。
- clear()方法會:
- 清空整個緩衝區。
- position將被設回0,limit被設置成 capacity的值
- compact()方法:
- 只會清除已經讀過的數據;任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。
- 將position設到最後一個未讀元素正後面,limit被設置成 capacity的值
寫數據
Buffer的三個屬性
- capacity:含義與模式無關;Buffer的一個固定的大小值;Buffer滿了需要將其清空才能再寫;
- ByteBuffer.allocate(48);該buffer的capacity爲48byte
- CharBuffer.allocate(1024);該buffer的capacity爲1024個char
- position:含義取決於Buffer處在讀模式還是寫模式(初始值爲0,寫或者讀操作的當前位置)
- 寫數據時,初始的position值爲0;其值最大可爲capacity-1
- 將Buffer從寫模式切換到讀模式,position會被重置爲0
- limit:含義取決於Buffer處在讀模式還是寫模式(寫limit=capacity;讀limit等於最多可以讀取到的數據)
- 寫模式下,limit等於Buffer的capacity
- 切換Buffer到讀模式時, limit表示你最多能讀到多少數據;
Selector
概述
Selector允許單線程處理多個 Channel。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。
要使用Selector,得向Selector註冊Channel,然後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新連接進來,數據接收等。
使用
- 創建:Selector selector = Selector.open();
- 註冊通道:
- channel.configureBlocking(false);
- //與Selector一起使用時,Channel必須處於非阻塞模式
- //這意味着不能將FileChannel與Selector一起使用,因爲FileChannel不能切換到非阻塞模式(而套接字通道都可以)
- SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
- //第二個參數表明Selector監聽Channel時對什麼事件感興趣
- //SelectionKey.OP_CONNECT SelectionKey.OP_ACCEPT SelectionKey.OP_READ SelectionKey.OP_WRITE
- //可以用或操作符將多個興趣組合一起
- SelectionKey
- 包含了interest集合 、ready集合 、Channel
、Selector 、附加的對象(可選)
- int interestSet = key.interestOps();可以進行類似interestSet & SelectionKey.OP_CONNECT的判斷
- 使用:
- select():阻塞到至少有一個通道在你註冊的事件上就緒了
- selectNow():不會阻塞,不管什麼通道就緒都立刻返回
- selectedKeys():訪問“已選擇鍵集(selected key set)”中的就緒通道
- close():使用完selector需要用其close()方法會關閉該Selector,且使註冊到該Selector上的所有SelectionKey實例無效
-
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();//注意這裏必須手動remove;表明該selectkey我已經處理過了;
-
}
Java測試關鍵代碼
- RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
- FileChannel inChannel = aFile.getChannel(); //從一個InputStream outputstream中獲取channel
-
- //create buffer with capacity of 48 bytes
- ByteBuffer buf = ByteBuffer.allocate(48);
-
- int bytesRead = inChannel.read(buf); //read into buffer.
- while (bytesRead != -1) {
-
- 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);
- }
- aFile.close();
文件通道
- RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
- FileChannel inChannel = aFile.getChannel();
讀數據
- ByteBuffer buf = ByteBuffer.allocate(48);
- int bytesRead = inChannel.read(buf);
寫數據
- 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);
- }
Socket 通道
- SocketChannel socketChannel = SocketChannel.open();
- socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
- ByteBuffer buf = ByteBuffer.allocate(48);
- int bytesRead = socketChannel.read(buf);
寫數據
- 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()) {
- socketChannel.write(buf);
- }
ServerSocket 通道
- ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
- serverSocketChannel.socket().bind(new InetSocketAddress(9999));
- while(true){
- SocketChannel socketChannel =
- serverSocketChannel.accept();
- //do something with socketChannel...
- }
Datagram 通道(channel的讀寫操作與前面的有差異)
- DatagramChannel channel = DatagramChannel.open();
- channel.socket().bind(new InetSocketAddress(9999));
讀數據
- ByteBuffer buf = ByteBuffer.allocate(48);
- buf.clear();
- channel.receive(buf);
- //receive()方法會將接收到的數據包內容複製到指定的Buffer. 如果Buffer容不下收到的數據,多出的數據將被丟棄。
寫數據
- 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));