NIO 简介
上文我們描述了五中IO類型。第一種同步阻塞模型我們我們稱之為BIO(Blocking IO),
第三種IO復用模型我們稱之為NIO(Nonblocking IO)。
上圖我們可以很容易的發現 BIO會為每個socket請求創建一個線程,而NIO可以通過一個線程處理多個請求。當然,我們可以為BIO構建一個線程池,這是一種偽異步的BIO模型。BIO和NIO最大的區別還是在阻塞上面。
阻塞主要有兩方面
等待網絡可讀寫 server.accept()
讀寫阻塞
通過觀察InputStream的Api我們可以了解到,只有在下面三種情況下,BIO才會解除阻塞
1.有數據可讀 2.可用數據已讀取完畢 3.發送空指針或者I/O異常
所以,假如我們使用BIO進行網絡消息傳遞,在網絡不穩定的情況下,一次消息的傳遞需要花費30s,那這個bio的線程就需要阻塞30秒,假如所有的線程都阻塞30s,那系統基本就不可用了。
基于上述的問題,java推出了NIO。我們先用一段代碼看看NIO的編程
public static void main(String[] args) throws Exception {
// 打開一個ServerSocketChannel
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.configureBlocking(Boolean.FALSE);
// 獲取ServerSocketChannel綁定的Socket
ServerSocket socket = socketChannel.socket();
// 設置ServerSocket監聽的端口
socket.bind(new InetSocketAddress(PORT));
System.out.println("開始等待客戶端連接");
// 打開一個選擇器
Selector selector = Selector.open();
// 將ServerSocketChannel注冊到選擇器上去并監聽accept事件
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 這里會發生阻塞,等待就緒的通道
int select = selector.select();
// 沒有就緒的通道則什么也不做
if (select == 0) {
continue;
}
// 獲取SelectionKeys上已經就緒的通道的集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 遍歷每一個Key
while (iterator.hasNext()){
SelectionKey next = iterator.next();
if (next.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel) next.channel();
SocketChannel socketChannel1 = channel.accept();
socketChannel1.register(selector,SelectionKey.OP_READ);
}else if (next.isReadable()){
readDataFromSocket(next);
}
iterator.remove();
}
}
}
private static ByteBuffer bb = ByteBuffer.allocate(1024);
private static void readDataFromSocket(SelectionKey next) throws IOException {
SocketChannel sc = (SocketChannel)next.channel();
bb.clear();
while (sc.read(bb)>0){
bb.flip();//
//告知在當前位置和限制之間是否有元素
while (bb.hasRemaining()){
System.out.println((char) bb.get());
}
System.out.println();
bb.clear();
}
}
java為NIO提供了全新的API,大致有以下三種
緩沖區 Buffer
一個緩沖區對象是固定數量的數據的容器,其作用是一個存儲器,或者分段運輸區,在這里數據可被存儲并在之后用于檢索。從數據結構而言,緩沖區就是一個數組,通常是一個字節數組即ByteBuffer。每一種java基本類型都有對應的緩沖區
Channel
與socket類和SeverSocket類似。NIO提供了SocketChannel和ServerSocketChannel,這兩個新增的通道都支持阻塞和非阻塞模式,阻塞模式使用簡單,但是性能和可靠性都不好。非阻塞模式則相反。Channel可以自由的設置阻塞對Java來說意義非常重大。試想下之前的BIO網絡編程為什么一個連接必須要對應一個線程。由于NIO的channel可以設置非阻塞模式,我們完全可以通過一個線程接受多個socket請求。
有兩點需要我們注意:
1.文件通道總是阻塞的,不能設置成非阻塞模式
2.Channel只能往Buffer中寫入
Selector
選擇器的作用是協調管理多個channel,selector定義了4種channel事件,每次channel注冊的時候都必須定義好自己關心的是哪一種事件。注冊完成后selector會一直阻塞,直到某些事件就緒。
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
在了解上述三個api之后,我們再簡單分析下上述代碼
1.創建ServerSocketChannel 2.設置ServerSocketChannel為非阻塞狀態 3.監聽端口 4.將ServerSocketChannel 注冊到一個Selector 5.等待選擇接受就緒事件,一旦接收到 即可做出相應的操作
NIO的阻塞
如上圖所示,NIO其實是有阻塞的環節的。那為什么我們仍然稱NIO是同步非阻塞IO呢。這里主要涉及到一次完整的io請求是怎么進行讀寫的。
所有的系統I/O都分為兩個階段:
等待就緒和操作。舉例來說,讀函數,分為等待系統可讀和真正的讀;同理,寫函數分為等待網卡可以寫和真正的寫。等待就緒的阻塞是不使用CPU的,是在“空等”;而真正的讀寫操作的阻塞是使用CPU的,真正在"干活",而且這個過程非常快,屬于memory copy,帶寬通常在1GB/s級別以上,可以理解為基本不耗時。
對于BIO而言,如果TCP RecvBuffer里沒有數據,函數會一直阻塞,直到收到數據,再阻塞的讀到的數據。
對于NIO,如果TCP RecvBuffer有數據,就把數據從網卡讀到內存,并且返回給用戶;反之則直接返回0,永遠不會阻塞。
所以,socket主要的讀、寫、注冊和接收函數,在等待就緒階段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。這部分的阻塞相對于BIO而言,是可以忽略不計的。所以我們可以認為NIO是非阻塞的。
總結
- 上一篇: Linux学习笔记13——使用curse
- 下一篇: 我去年买了个表是什么意思(网络我去年买了