來源:https://blog.csdn.net/qq_36520235/
NIO和IO到底有什么區(qū)別?有什么關(guān)系?
首先說一下核心區(qū)別:
NIO是以塊的方式處理數(shù)據(jù),但是IO是以最基礎(chǔ)的字節(jié)流的形式去寫入和讀出的。所以在效率上的話,肯定是NIO效率比IO效率會高出很多。
NIO不在是和IO一樣用OutputStream和InputStream 輸入流的形式來進行處理數(shù)據(jù)的,但是又是基于這種流的形式,而是采用了通道和緩沖區(qū)的形式來進行處理數(shù)據(jù)的。
還有一點就是NIO的通道是可以雙向的,但是IO中的流只能是單向的。
還有就是NIO的緩沖區(qū)(其實也就是一個字節(jié)數(shù)組)還可以進行分片,可以建立只讀緩沖區(qū)、直接緩沖區(qū)和間接緩沖區(qū),只讀緩沖區(qū)很明顯就是字面意思,直接緩沖區(qū)是為加快 I/O 速度,而以一種特殊的方式分配其內(nèi)存的緩沖區(qū)。
補充一點:NIO比傳統(tǒng)的BIO核心區(qū)別就是,NIO采用的是多路復(fù)用的IO模型,普通的IO用的是阻塞的IO模型,兩個之間的效率肯定是多路復(fù)用效率更高
先了解一下什么是通道,什么是緩沖區(qū)的概念
通道是個什么意思?
通道是對原 I/O 包中的流的模擬。到任何目的地(或來自任何地方)的所有數(shù)據(jù)都必須通過一個 Channel 對象(通道)。一個 Buffer 實質(zhì)上是一個容器對象。發(fā)送給一個通道的所有對象都必須首先放到緩沖區(qū)中;同樣地,從通道中讀取的任何數(shù)據(jù)都要讀到緩沖區(qū)中。Channel是一個對象,可以通過它讀取和寫入數(shù)據(jù)。拿 NIO 與原來的 I/O 做個比較,通道就像是流。
正如前面提到的,所有數(shù)據(jù)都通過 Buffer 對象來處理。您永遠不會將字節(jié)直接寫入通道中,相反,您是將數(shù)據(jù)寫入包含一個或者多個字節(jié)的緩沖區(qū)。同樣,您不會直接從通道中讀取字節(jié),而是將數(shù)據(jù)從通道讀入緩沖區(qū),再從緩沖區(qū)獲取這個字節(jié)。
緩沖區(qū)是什么意思:
Buffer 是一個對象, 它包含一些要寫入或者剛讀出的數(shù)據(jù)。在 NIO 中加入 Buffer 對象,體現(xiàn)了新庫與原 I/O 的一個重要區(qū)別。在面向流的 I/O 中,您將數(shù)據(jù)直接寫入或者將數(shù)據(jù)直接讀到 Stream 對象中
在 NIO 庫中,所有數(shù)據(jù)都是用緩沖區(qū)處理的。在讀取數(shù)據(jù)時,它是直接讀到緩沖區(qū)中的。在寫入數(shù)據(jù)時,它是寫入到緩沖區(qū)中的。任何時候訪問 NIO 中的數(shù)據(jù),您都是將它放到緩沖區(qū)中。
緩沖區(qū)實質(zhì)上是一個數(shù)組。通常它是一個字節(jié)數(shù)組,但是也可以使用其他種類的數(shù)組。但是一個緩沖區(qū)不 僅僅 是一個數(shù)組。緩沖區(qū)提供了對數(shù)據(jù)的結(jié)構(gòu)化訪問,而且還可以跟蹤系統(tǒng)的讀/寫進程
緩沖區(qū)的類型:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
NIO的底層工作原理
先來了解一下buffer的工作機制:
- capacity 緩沖區(qū)數(shù)組的總長度
- position 下一個要操作的數(shù)據(jù)元素的位置
- limit 緩沖區(qū)數(shù)組中不可操作的下一個元素的位置,limit<=capacity
- mark 用于記錄當(dāng)前 position 的前一個位置或者默認(rèn)是 0
1.這一步其實是當(dāng)我們剛開始初始化這個buffer數(shù)組的時候,開始默認(rèn)是這樣的
2、但是當(dāng)你往buffer數(shù)組中開始寫入的時候幾個字節(jié)的時候就會變成下面的圖,position會移動你數(shù)據(jù)的結(jié)束的下一個位置,這個時候你需要把buffer中的數(shù)據(jù)寫到channel管道中,所以此時我們就需要用這個buffer.flip();方法,
3、當(dāng)你調(diào)用完2中的方法時,這個時候就會變成下面的圖了,這樣的話其實就可以知道你剛剛寫到buffer中的數(shù)據(jù)是在position---->limit之間,然后下一步調(diào)用clear();
4、這時底層操作系統(tǒng)就可以從緩沖區(qū)中正確讀取這 5 個字節(jié)數(shù)據(jù)發(fā)送出去了。在下一次寫數(shù)據(jù)之前我們在調(diào)一下 clear() 方法。緩沖區(qū)的索引狀態(tài)又回到初始位置。(其實這一步有點像IO中的把轉(zhuǎn)運字節(jié)數(shù)組 char[] buf = new char[1024];不足1024字節(jié)的部分給強制刷新出去的意思)
補充:
1、這里還要說明一下 mark,當(dāng)我們調(diào)用 mark()時,它將記錄當(dāng)前 position 的前一個位置,當(dāng)我們調(diào)用 reset 時,position 將恢復(fù) mark 記錄下來的值
2.clear()方法會:清空整個緩沖區(qū)。position將被設(shè)回0,limit被設(shè)置成 capacity的值(這個個人的理解就是當(dāng)你在flip()方法的基礎(chǔ)上已經(jīng)記住你寫入了多少字節(jié)數(shù)據(jù),直接把position到limit之間的也就是你寫入已經(jīng)記住的數(shù)據(jù)給“復(fù)制”到管道中)
3.當(dāng)你把緩沖區(qū)的數(shù)局寫入到管道中的時候,你需要調(diào)用flip()方法將Buffer從寫模式切換到讀模式,調(diào)用flip()方法會將position設(shè)回0,并將limit設(shè)置成之前position的值。buf.flip();(其實我個人理解的就相當(dāng)于先記住緩沖區(qū)緩沖了多少數(shù)據(jù))
NIO 工作代碼示例
public void selector() throws IOException {
//先給緩沖區(qū)申請內(nèi)存空間
ByteBuffer buffer = ByteBuffer.allocate(1024);
//打開Selector為了它可以輪詢每個 Channel 的狀態(tài)
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);//設(shè)置為非阻塞方式
ssc.socket().bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT);//注冊監(jiān)聽的事件
while (true) {
Set selectedKeys = selector.selectedKeys();//取得所有key集合
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
SocketChannel sc = ssChannel.accept();//接受到服務(wù)端的請求
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
it.remove();
} else if
((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
SocketChannel sc = (SocketChannel) key.channel();
while (true) {
buffer.clear();
int n = sc.read(buffer);//讀取數(shù)據(jù)
if (n <= 0) {
break;
}
buffer.flip();
}
it.remove();
}
}
}
}
最后給大家看一下整體的NIO的示意圖
NIO和Netty的工作模型對比?
(1)NIO的工作流程步驟:
- 首先是先創(chuàng)建ServerSocketChannel 對象,和真正處理業(yè)務(wù)的線程池
- 然后給剛剛創(chuàng)建的ServerSocketChannel 對象進行綁定一個對應(yīng)的端口,然后設(shè)置為非阻塞
- 然后創(chuàng)建Selector對象并打開,然后把這Selector對象注冊到ServerSocketChannel 中,并設(shè)置好監(jiān)聽的事件,監(jiān)聽 SelectionKey.OP_ACCEPT
- 接著就是Selector對象進行死循環(huán)監(jiān)聽每一個Channel通道的事件,循環(huán)執(zhí)行 Selector.select() 方法,輪詢就緒的 Channel
- 從Selector中獲取所有的SelectorKey(這個就可以看成是不同的事件),如果SelectorKey是處于 OP_ACCEPT 狀態(tài),說明是新的客戶端接入,調(diào)用 ServerSocketChannel.accept 接收新的客戶端。
- 然后對這個把這個接受的新客戶端的Channel通道注冊到ServerSocketChannel上,并且把之前的OP_ACCEPT 狀態(tài)改為SelectionKey.OP_READ讀取事件狀態(tài),并且設(shè)置為非阻塞的,然后把當(dāng)前的這個SelectorKey給移除掉,說明這個事件完成了
- 如果第5步的時候過來的事件不是OP_ACCEPT 狀態(tài),那就是OP_READ讀取數(shù)據(jù)的事件狀態(tài),然后調(diào)用本文章的上面的那個讀取數(shù)據(jù)的機制就可以了
(2)Netty的工作流程步驟:
- 創(chuàng)建 NIO 線程組 EventLoopGroup 和 ServerBootstrap。
- 設(shè)置 ServerBootstrap 的屬性:線程組、SO_BACKLOG 選項,設(shè)置 NioServerSocketChannel 為 Channel,設(shè)置業(yè)務(wù)處理 Handler
- 綁定端口,啟動服務(wù)器程序。
- 在業(yè)務(wù)處理 TimeServerHandler 中,讀取客戶端發(fā)送的數(shù)據(jù),并給出響應(yīng)
(3)兩者之間的區(qū)別:
- OP_ACCEPT 的處理被簡化,因為對于 accept 操作的處理在不同業(yè)務(wù)上都是一致的。
- 在 NIO 中需要自己構(gòu)建 ByteBuffer 從 Channel 中讀取數(shù)據(jù),而 Netty 中數(shù)據(jù)是直接讀取完成存放在 ByteBuf 中的。相當(dāng)于省略了用戶進程從內(nèi)核中復(fù)制數(shù)據(jù)的過程。
- 在 Netty 中,我們看到有使用一個解碼器 FixedLengthFrameDecoder,可以用于處理定長消息的問題,能夠解決 TCP 粘包讀半包問題,十分方便。