Java NIO入門

Java NIO(Non-Blocking I/O),是一種同步非阻塞的I/O模型,,也是I/O多路複用的基礎。

一般底層是透過Unix網路模型驅動的。

NIO主要有三大核心部分:Channel(通道)、Buffer(緩衝區)、Selector(多路複用器)。

傳統IO基於位元組流和字元流進行操作,而NIO基於Channel和Buffer進行操作,資料總是從Channel讀取到Buffer中,或者從Buffer寫入到Channel中。Selector用於監聽多個通道的事件,單個執行緒可以監聽多個數據通道。

Java NIO入門

NIO

Channnel

Channel是一個通道,網路資料透過Channel讀取和寫入。通道與流不同之處在於通道是雙向的,流只是在一個方向上移動(一個流必須是InputStream或者OutputStream的子類),而通道可以用於讀、寫或者二者同時進行。

因為Channel是全雙工的,所以它可以比流更好地對映底層作業系統的API。特別是UNIX網路程式設計模型中,底層作業系統的通道都是全雙工、同時支援讀寫操作的。

Buffer

Buffer是一個物件,它包含一些要寫入或者要讀出的資料。

在NIO庫中,所有資料都是用緩衝區處理的。在讀取資料時,它是直接讀到緩衝區中的;在寫入資料時,寫入到緩衝區中。任何時候訪問NIO中的資料,都是透過緩衝區操作。

緩衝區實質上是一個數組,通常是一個位元組陣列(ByteBuffer),也可以用其他種類的陣列。但是一個緩衝區不僅僅是一個數組,緩衝區提供了對資料的結構化訪問以及維護讀寫位置(limit)等資訊。

最常用的緩衝區是ByteBuffer,java每一個型別都對應有一種緩衝區,比如CharBuffer、IntBuffer等。

Java NIO入門

Buffer

當你寫資料到Buffer中時,position表示當前的位置。初始的position值為0。當一個byte、long等資料寫到Buffer後, position會向前移動到下一個可插入資料的Buffer單元。

當讀取資料時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置為0。 當從Buffer的position處讀取資料時,position向前移動到下一個可讀的位置。

指標limit:

寫模式時,limit表示可以往buffer中寫入的資料大小,等於capacity。

讀模式時, limit表示你最多能讀到多少資料。

Selector

Selector會不斷地輪詢註冊在其上的Channel,如果某個Channel上面發生讀或者寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,然後透過SelectionKey可以獲取就緒Channel的集合,進行後續的I/O操作。

一個多路複用器Selector可以同時輪詢多個Channel,由於JDK使用了epoll代替傳統的select實現,它沒有最大連結控制代碼1024或2048的限制。這也就意味著只需要一個執行緒負責Selector的輪詢,就可以接入成千上萬的客戶端。

Java NIO入門

Selector

NIO實現程式碼

服務端實現

//1。開啟ServerSocketChannel,用於監聽客戶端連線,它是所有客戶端連線的父管道ServerSocketChannel acceptor = ServerSocketChannel。open();//2。繫結監聽埠,設定為非阻塞模式acceptor。socket()。bind(new InetSocketAddress。getByName(“ip”,port));acceptor。configureBlocking(false);//3。建立Reactor執行緒,建立多路複用器並啟動執行緒Selector selector = Selector。open();new Thread(new ReactorTask())。start();//4。將ServerSocketChannel註冊到Reactor執行緒的多路複用器Selector上,監聽ACCEPT事件SelectionKey key = acceptor。register(selector, SelectionKey。OP_ACCEPT, ioHandler);//5。多路複用器線上程run方法的無限迴圈體內輪詢準備就緒的Keyint num = selector。select();Set selectedKeys = selector。selectedKeys();Iterator it = selectedKeys。iterator();while(it。hasNext()){ SelectionKey key = (SelectionKey) it。next(); //I/O event。。。}//6。多路複用器監聽到有新的客戶端接入,處理新的接入請求,完成TCP三次握手,建立物理鏈路SocketChannel channel = channel。accept();//7。設定客戶端鏈路為非阻塞模式channel。configureBlocking(false);channel。socket()。setReuseAddress(true);//8。新接入的客戶端註冊到Reactor執行緒的多路複用器上,監聽讀操作,讀取客戶端傳送的網路訊息SelectionKey key = socketChannel。register(selector, SelectionKey。OP_READ, ioHandler);//9。非同步讀取客戶端請求訊息到緩衝區int readNumber = channel。read(receivedBuffer);//10。對ByteBuffer進行編解碼,如果有半包訊息指標reset,繼續讀取後續豹紋,將解碼成功的訊息封裝成Task,投遞到業務執行緒池中,處理業務邏輯Object message = null;while(buffer。hasRemain()){ message = decode(byteBuffer); //此處省去處理讀半包問題}handlerTask(message);//11。將POJO物件encode成ByteBuffer,呼叫SocketChannel的非同步write介面將訊息傳送給客戶端socketChannel。write(buffer);//如果傳送區TCP緩衝區滿,會導致寫半包

客戶端實現:

SocketChannel channel = SocketChannel。open();channel。configureBlocking(false);socket。setReuseAddress(true);socket。setReceiveBufferSize(BUFFER_SIZE);socket。setSendBufferSize(BUFFER_SIZE);//非同步連線服務端Boolean connected = channel。connect(new InetSocketAddress(“ip”, port));if(connected){ channel。register(selector, SelectionKey。OP_READ, ioHandler);} else { channel。register(selector, SelectionKey。OP_CONNECT, ioHandler);}//建立reactor執行緒,建立多路複用器並啟動執行緒Selector selector = Selector。open();new Thread(new ReactorTask())。start();//輪詢準備就緒的keyint num = selector。select();Set selectedKeys = selector。selectedKeys();Iterator it = selectedKeys。iterator();while(it。hasNext()){ SelectionKey key = (SelectionKey) it。next(); //I/O event。。。}if(key。isConnectable())//hanlerConnect();

總結

1。 客戶端發起的連線操作是非同步的;

2。 SocketChannel的讀寫操作都是非同步的;

3。 執行緒模型的最佳化,使用了epoll實現,沒有連線控制代碼數的限制,可以處理成千上萬個客戶端連線,而且效能不會隨著客戶端的增加而線性下降

適合做高效能、高負載的網路伺服器。