http://www.lxweimin.com/p/191041073919 , 主要講解了 NIO 中的每個(gè)部分(沒有完全看完)
要求:
nio 是什么,三個(gè)組件 selector, channel, buffer 分別擔(dān)任了什么角色,起到了什么作用。
數(shù)據(jù)的請求流程是什么
核心組件
Java NIO 由以下幾個(gè)核心部分組成:
Channel
Buffer
Selector
Channel 和 Buffer
基本上,所有的 IO 在NIO 中都從一個(gè)Channel 開始。Channel 有點(diǎn)象流。 數(shù)據(jù)可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中。這里有個(gè)圖示:
NIO 和 IO 的對比
IO 和 NIO 的區(qū)別主要體現(xiàn)在三個(gè)方面:
- IO 基于流(Stream oriented), 而 NIO 基于 Buffer (Buffer oriented)
- IO 操作是阻塞的, 而 NIO 操作是非阻塞的
- IO 沒有 selector 概念, 而 NIO 有 selector 概念.
selector
selector 是 NIO 中才有的概念, 它是 Java NIO 之所以可以非阻塞地進(jìn)行 IO 操作的關(guān)鍵。
通過 Selector, 一個(gè)線程可以監(jiān)聽多個(gè) Channel 的 IO 事件, 當(dāng)我們向一個(gè) Selector 中注冊了 Channel 后, Selector 內(nèi)部的機(jī)制就可以自動(dòng)地為我們不斷地查詢(select) 這些注冊的 Channel 是否有已就緒的 IO 事件(例如可讀, 可寫, 網(wǎng)絡(luò)連接完成等). 通過這樣的 Selector 機(jī)制, 我們就可以很簡單地使用一個(gè)線程高效地管理多個(gè) Channel 了。
Channel
通常來說, 所有的 NIO 的 I/O 操作都是從 Channel 開始的. 一個(gè) channel 類似于一個(gè) stream
java Stream 和 NIO Channel 對比
我們可以在同一個(gè) Channel 中執(zhí)行讀和寫操作, 然而同一個(gè) Stream 僅僅支持讀或?qū)?/p>
Channel 可以異步地讀寫, 而 Stream 是阻塞的同步讀寫
Channel 總是從 Buffer 中讀取數(shù)據(jù), 或?qū)?shù)據(jù)寫入到 Buffer 中
Channel 類型有:
FileChannel:文件操作
DatagramChannel:UDP 操作
SocketChannel:TCP 操作
ServerSocketChannel:TCP 操作, 使用在服務(wù)器端
這些通道涵蓋了 UDP 和 TCP網(wǎng)絡(luò) IO以及文件 IO
FileChannel
FileChannel 是操作文件的Channel, 我們可以通過 FileChannel 從一個(gè)文件中讀取數(shù)據(jù), 也可以將數(shù)據(jù)寫入到文件中
注意:FileChannel 不能設(shè)置為非阻塞模式
SocketChannel
SocketChannel 是一個(gè)客戶端用來進(jìn)行 TCP 連接的 Channel
創(chuàng)建一個(gè) SocketChannel 的方法有兩種:
- 打開一個(gè) SocketChannel, 然后將其連接到某個(gè)服務(wù)器中
- 當(dāng)一個(gè) ServerSocketChannel 接受到連接請求時(shí), 會(huì)返回一個(gè) SocketChannel 對象
打開 SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://example.com", 80));
讀取數(shù)據(jù)
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
如果 read()返回 -1, 那么表示連接中斷了
寫入數(shù)據(jù)
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);
}
// 非阻塞模式
// 我們可以設(shè)置 SocketChannel 為異步模式, 這樣我們的 connect, read, write 都是異步的了.
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://example.com", 80));
while(! socketChannel.finishConnect() ){
//wait, or do something else...
}
非阻塞模式
在非阻塞模式下, accept()是非阻塞的, 因此如果此時(shí)沒有連接到來, 那么 accept()方法會(huì)返回null:
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...
}
}
Buffer
當(dāng)我們需要與 NIO Channel 進(jìn)行交互時(shí), 我們就需要使用到 NIO Buffer, 即數(shù)據(jù)從 Buffer讀取到 Channel 中, 并且從 Channel 中寫入到 Buffer 中
實(shí)際上, 一個(gè) Buffer 其實(shí)就是一塊內(nèi)存區(qū)域, 我們可以在這個(gè)內(nèi)存區(qū)域中進(jìn)行數(shù)據(jù)的讀寫. NIO Buffer 其實(shí)是這樣的內(nèi)存塊的一個(gè)封裝, 并提供了一些操作方法讓我們能夠方便地進(jìn)行數(shù)據(jù)的讀寫
Buffer 類型有:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
這些 Buffer 覆蓋了能從 IO 中傳輸?shù)乃械?Java 基本數(shù)據(jù)類型
基本使用
- 將數(shù)據(jù)寫入到 Buffer 中
- 調(diào)用 Buffer.flip()方法, 將 NIO Buffer 轉(zhuǎn)換為讀模式
- 從 Buffer 中讀取數(shù)據(jù)
- 調(diào)用 Buffer.clear() 或 Buffer.compact()方法, 將 Buffer 轉(zhuǎn)換為寫模式
緩沖區(qū)四個(gè)屬性值一定遵循capacity ≥ limit ≥ position ≥ mark ≥ 0,put()時(shí),若 position 超過 limit 則會(huì)拋出 BufferOverflowException;get() 時(shí),若 position 超過 limit 則會(huì)拋出 BufferUnderflowException。
一旦讀取了所有的 Buffer 數(shù)據(jù), 那么我們必須清理 Buffer, 讓其從新可寫, 清理 Buffer 可以調(diào)用 Buffer.clear() 或 Buffer.compact()。
Buffer 屬性
一個(gè) Buffer 有三個(gè)屬性: capacity, position, limit。
其中 position 和 limit 的含義與 Buffer 處于讀模式或?qū)懩J接嘘P(guān), 而 capacity 的含義與 Buffer 所處的模式無關(guān)
Capacity
一個(gè)內(nèi)存塊會(huì)有一個(gè)固定的大小, 即容量(capacity), 我們最多寫入capacity 個(gè)單位的數(shù)據(jù)到 Buffer 中, 例如一個(gè) DoubleBuffer, 其 Capacity 是100, 那么我們最多可以寫入100個(gè) double 數(shù)據(jù)
Position
當(dāng)從一個(gè) Buffer 中寫入數(shù)據(jù)時(shí), 我們是從 Buffer 的一個(gè)確定的位置(position)開始寫入的. 在最初的狀態(tài)時(shí), position 的值是0. 每當(dāng)我們寫入了一個(gè)單位的數(shù)據(jù)后, position 就會(huì)遞增一
當(dāng)我們從 Buffer 中讀取數(shù)據(jù)時(shí), 我們也是從某個(gè)特定的位置開始讀取的. 當(dāng)我們調(diào)用了 filp()方法將 Buffer 從寫模式轉(zhuǎn)換到讀模式時(shí), position 的值會(huì)自動(dòng)被設(shè)置為0, 每當(dāng)我們讀取一個(gè)單位的數(shù)據(jù), position 的值遞增1
position 表示了讀寫操作的位置指針
舉例說明
下圖是新創(chuàng)建的一個(gè)容量為10的字節(jié)緩沖區(qū)的內(nèi)存圖:
向緩沖區(qū) put() 四次后的緩沖區(qū)內(nèi)存示意圖:
執(zhí)行buff.flip()將緩沖區(qū)翻轉(zhuǎn)后的內(nèi)存示意圖
執(zhí)兩次get()后的緩沖區(qū)內(nèi)存示意圖
關(guān)于 Direct Buffer 和 Non-Direct Buffer 的區(qū)別
Direct Buffer:
- 所分配的內(nèi)存不在 JVM 堆上, 不受 GC 的管理.(但是 Direct Buffer 的 Java 對象是由 GC 管理的, 因此當(dāng)發(fā)生 GC, 對象被回收時(shí), Direct Buffer 也會(huì)被釋放)
- 因?yàn)?Direct Buffer 不在 JVM 堆上分配, 因此 Direct Buffer 對應(yīng)用程序的內(nèi)存占用的影響就不那么明顯(實(shí)際上還是占用了這么多內(nèi)存, 但是 JVM 不好統(tǒng)計(jì)到非 JVM 管理的內(nèi)存.)
- 申請和釋放 Direct Buffer 的開銷比較大. 因此正確的使用 Direct Buffer 的方式是在初始化時(shí)申請一個(gè) Buffer, 然后不斷復(fù)用此 buffer, 在程序結(jié)束后才釋放此 buffer.
- 使用 Direct Buffer 時(shí), 當(dāng)進(jìn)行一些底層的系統(tǒng) IO 操作時(shí), 效率會(huì)比較高, 因?yàn)榇藭r(shí) JVM 不需要拷貝 buffer 中的內(nèi)存到中間臨時(shí)緩沖區(qū)中.
Non-Direct Buffer:
- 直接在 JVM 堆上進(jìn)行內(nèi)存的分配, 本質(zhì)上是 byte[] 數(shù)組的封裝.
- 因?yàn)?Non-Direct Buffer 在 JVM 堆中, 因此當(dāng)進(jìn)行操作系統(tǒng)底層 IO 操作中時(shí), 會(huì)將此 buffer 的內(nèi)存復(fù)制到中間臨時(shí)緩沖區(qū)中. 因此 Non-Direct Buffer 的效率就較低.
Buffer 的比較
我們可以通過 equals() 或 compareTo() 方法比較兩個(gè) Buffer, 當(dāng)且僅當(dāng)如下條件滿足時(shí), 兩個(gè) Buffer 是相等的:
- 兩個(gè) Buffer 是相同類型的
- 兩個(gè) Buffer 的剩余的數(shù)據(jù)個(gè)數(shù)是相同的
- 兩個(gè) Buffer 的剩余的數(shù)據(jù)都是相同的.
通過上述條件我們可以發(fā)現(xiàn), 比較兩個(gè) Buffer 時(shí), 并不是 Buffer 中的每個(gè)元素都進(jìn)行比較, 而是比較 Buffer 中剩余的元素.
flip, rewind 和 clear 的區(qū)別
flip:當(dāng)從寫模式變?yōu)樽x模式時(shí), 原先的 寫 position 就變成了讀模式的 limit。
rewind: 這個(gè)方法僅僅是將 position 置為0。
clear:將 positin 設(shè)置為0, 將 limit 設(shè)置為 capacity。
Selector
Selector 允許一個(gè)單一的線程來操作多個(gè) Channel. 如果我們的應(yīng)用程序中使用了多個(gè) Channel, 那么使用 Selector 很方便的實(shí)現(xiàn)這樣的目的, 但是因?yàn)樵谝粋€(gè)線程中使用了多個(gè) Channel, 因此也會(huì)造成了每個(gè) Channel 傳輸效率的降低。
為了使用 Selector, 我們首先需要將 Channel 注冊到 Selector 中, 隨后調(diào)用 Selector 的 select()方法, 這個(gè)方法會(huì)阻塞, 直到注冊在 Selector 中的 Channel 發(fā)送可讀寫事件. 當(dāng)這個(gè)方法返回后, 當(dāng)前的這個(gè)線程就可以處理 Channel 的事件了。
注意:
- 如果一個(gè) Channel 要注冊到 Selector 中, 那么這個(gè) Channel 必須是非阻塞的, 即channel.configureBlocking(false)。因此 FileChannel 不可以注冊到 Selector 上,因?yàn)?FileChannel 是阻塞的。
- 一個(gè) Channel 僅僅可以被注冊到一個(gè) Selector 一次, 如果將 Channel 注冊到 Selector 多次, 那么其實(shí)就是相當(dāng)于更新 SelectionKey 的 interest set
注意到, 在使用 Channel.register()方法時(shí), 第二個(gè)參數(shù)指定了我們對 Channel 的什么類型的事件感興趣, 這些事件有:
- Connect, 即連接事件(TCP 連接), 對應(yīng)于SelectionKey.OP_CONNECT
- Accept, 即確認(rèn)事件, 對應(yīng)于SelectionKey.OP_ACCEPT
- Read, 即讀事件, 對應(yīng)于SelectionKey.OP_READ, 表示 buffer 可讀
- Write, 即寫事件, 對應(yīng)于SelectionKey.OP_WRITE, 表示 buffer 可寫
關(guān)于 SelectionKey
包含如下內(nèi)容:
- interest set, 即我們感興趣的事件集, 即在調(diào)用 register 注冊 channel 時(shí)所設(shè)置的 interest set
- ready set,代表了 Channel 所準(zhǔn)備好了的操作
- channel
- selector
- attached object, 可以在 selectionKey 中附加一個(gè)對象
獲取可操作的 Channel
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> 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();
}
注意, 在每次迭代時(shí), 我們都調(diào)用 "keyIterator.remove()" 將這個(gè) key 從迭代器中刪除, 因?yàn)?select() 方法僅僅是簡單地將就緒的 IO 操作放到 selectedKeys 集合中, 因此如果我們從 selectedKeys 獲取到一個(gè) key, 但是沒有將它刪除, 那么下一次 select 時(shí), 這個(gè) key 所對應(yīng)的 IO 事件還在 selectedKeys 中。
例如此時(shí)我們收到 OP_ACCEPT 通知, 然后我們進(jìn)行相關(guān)處理, 但是并沒有將這個(gè) Key 從 SelectedKeys 中刪除, 那么下一次 select() 返回時(shí) 我們還可以在 SelectedKeys 中獲取到 OP_ACCEPT 的 key。
Selector 的基本使用流程
- 通過 Selector.open() 打開一個(gè) Selector.
- 將 Channel 注冊到 Selector 中, 并設(shè)置需要監(jiān)聽的事件(interest set)
- 不斷重復(fù):
- 調(diào)用 select() 方法
- 調(diào)用 selector.selectedKeys() 獲取 selected keys
- 迭代每個(gè) selected key:
- 從 selected key 中獲取 對應(yīng)的 Channel 和附加信息(如果有的話)
- 判斷是哪些 IO 事件已經(jīng)就緒了, 然后處理它們。如果是 OP_ACCEPT 事件, 則調(diào)用
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()
獲取 SocketChannel, 并將它設(shè)置為 非阻塞的, 然后將這個(gè) Channel 注冊到 Selector 中。 - 根據(jù)需要更改 selected key 的監(jiān)聽事件
- 將已經(jīng)處理過的 key 從 selected keys 集合中刪除
關(guān)閉 Selector
當(dāng)調(diào)用了 Selector.close()方法時(shí), 我們其實(shí)是關(guān)閉了 Selector 本身并且將所有的 SelectionKey 失效, 但是并不會(huì)關(guān)閉 Channel。
NIO 代碼示范:hello world
客戶端代碼
package com.study.nio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOClient {
/*標(biāo)識(shí)數(shù)字*/
private static int flag = 0;
/*緩沖區(qū)大小*/
private static int BLOCK = 4096;
/*接受數(shù)據(jù)緩沖區(qū)*/
private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*發(fā)送數(shù)據(jù)緩沖區(qū)*/
private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
/*服務(wù)器端地址*/
private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
"localhost", 9988);
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
// 打開socket通道
SocketChannel socketChannel = SocketChannel.open();
// 設(shè)置為非阻塞方式
socketChannel.configureBlocking(false);
// 打開選擇器
Selector selector = Selector.open();
// 注冊連接服務(wù)端socket動(dòng)作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 連接
socketChannel.connect(SERVER_ADDRESS);
// 分配緩沖區(qū)大小內(nèi)存
Set<SelectionKey> selectionKeys;
Iterator<SelectionKey> iterator;
SelectionKey selectionKey;
SocketChannel client;
String receiveText;
String sendText;
int count=0;
while (true) {
//選擇一組鍵,其相應(yīng)的通道已為 I/O 操作準(zhǔn)備就緒。
//此方法執(zhí)行處于阻塞模式的選擇操作。
selector.select();
//返回此選擇器的已選擇鍵集。
selectionKeys = selector.selectedKeys();
//System.out.println(selectionKeys.size());
iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.out.println("client connect");
client = (SocketChannel) selectionKey.channel();
// 判斷此通道上是否正在進(jìn)行連接操作。
// 完成套接字通道的連接過程。
if (client.isConnectionPending()) {
client.finishConnect();
System.out.println("客戶端完成連接!");
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
sendbuffer.flip();
client.write(sendbuffer);
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();
//將緩沖區(qū)清空以備下次讀取
receivebuffer.clear();
//讀取服務(wù)器發(fā)送來的數(shù)據(jù)到緩沖區(qū)中
count=client.read(receivebuffer);
if(count>0){
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("客戶端接受服務(wù)器端數(shù)據(jù)--:"+receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
sendbuffer.clear();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
client = (SocketChannel) selectionKey.channel();
System.out.println("enter a world");
while ((sendText = br.readLine()) != null) {
sendbuffer.put(sendText.getBytes());
//將緩沖區(qū)各標(biāo)志復(fù)位,因?yàn)橄蚶锩鎝ut了數(shù)據(jù)標(biāo)志被改變要想從中讀取數(shù)據(jù)發(fā)向服務(wù)器,就要復(fù)位
sendbuffer.flip();
client.write(sendbuffer);
System.out.println("客戶端向服務(wù)器端發(fā)送數(shù)據(jù)--:"+sendText);
client.register(selector, SelectionKey.OP_READ);
System.out.println("enter a world");
}
}
}
selectionKeys.clear();
}
}
}
服務(wù)端代碼
package com.study.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
/*標(biāo)識(shí)數(shù)字*/
private int flag = 0;
/*緩沖區(qū)大小*/
private int BLOCK = 4096;
/*接受數(shù)據(jù)緩沖區(qū)*/
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*發(fā)送數(shù)據(jù)緩沖區(qū)*/
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
private Selector selector;
public NIOServer(int port) throws IOException {
// 打開服務(wù)器套接字通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 服務(wù)器配置為非阻塞
serverSocketChannel.configureBlocking(false);
// 檢索與此通道關(guān)聯(lián)的服務(wù)器套接字
ServerSocket serverSocket = serverSocketChannel.socket();
// 進(jìn)行服務(wù)的綁定
serverSocket.bind(new InetSocketAddress(port));
// 通過open()方法找到Selector
selector = Selector.open();
// 注冊到selector,等待連接
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server Start----" + port);
}
// 監(jiān)聽
private void listen() throws IOException {
while (true) {
// 選擇一組鍵,并且相應(yīng)的通道已經(jīng)打開
selector.select();
// 返回此選擇器的已選擇鍵集。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
handleKey(selectionKey);
}
}
}
// 處理請求
private void handleKey(SelectionKey selectionKey) throws IOException {
// 接受請求
ServerSocketChannel server = null;
SocketChannel client = null;
String receiveText = "";
String sendText = "";
int count=0;
// 測試此鍵的通道是否已準(zhǔn)備好接受新的套接字連接。
if (selectionKey.isAcceptable()) {
// 返回為之創(chuàng)建此鍵的通道。
server = (ServerSocketChannel) selectionKey.channel();
// 接受到此通道套接字的連接。
// 此方法返回的套接字通道(如果有)將處于阻塞模式。
client = server.accept();
System.out.println("客戶端連接成功!" + client.getLocalAddress());
// 配置為非阻塞
client.configureBlocking(false);
// 注冊到selector,等待連接
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
//將緩沖區(qū)清空以備下次讀取
receivebuffer.clear();
// 返回為之創(chuàng)建此鍵的通道。
client = (SocketChannel) selectionKey.channel();
//讀取服務(wù)器發(fā)送來的數(shù)據(jù)到緩沖區(qū)中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("服務(wù)器端接受客戶端數(shù)據(jù)--:"+receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
//將緩沖區(qū)清空以備下次寫入
sendbuffer.clear();
// 返回為之創(chuàng)建此鍵的通道。
client = (SocketChannel) selectionKey.channel();
sendText="message from server--" + receiveText.toUpperCase();
//向緩沖區(qū)中輸入數(shù)據(jù)
sendbuffer.put(sendText.getBytes());
//將緩沖區(qū)各標(biāo)志復(fù)位,因?yàn)橄蚶锩鎝ut了數(shù)據(jù)標(biāo)志被改變要想從中讀取數(shù)據(jù)發(fā)向服務(wù)器,就要復(fù)位
sendbuffer.flip();
//輸出到通道
client.write(sendbuffer);
System.out.println("服務(wù)器端向客戶端發(fā)送數(shù)據(jù)--:"+sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
new NIOServer(9988).listen();
}
}