NIO buffer和netty buffer

NIO中的Buffer
前面提到過,在NIO中同樣存在一個緩衝區,叫做ByteBuffer,來配合Channel的使用。在ByteBuffer內部存儲數據的實質爲一個字節數組,如:final byte[] hb,並定義了四個標記來管理它。其中包括:mark <= position <= limit <= capacity。其中capacity用來表示緩衝區的大小;position用來標識下一個可讀取或者寫入的位置;limit表示讀取或者寫入的上限位置,如果要在>=limit的位置做讀寫操作會拋出異常;mark用來記錄當前position的值,記錄之後position隨着讀寫發生變化,在調用reset()方法時,會將position恢復爲mark記錄的值。在buffer中提供了很多put、get方法來放入和讀取數據,這裏不多做介紹,可以查看API。但其中有幾個重要的方法需要關注:
1. flip()方法:在讀取或者寫入n個字節(position + n < limit)後,position += n。如果是先讀取數據到buffer後寫入到Channel,必須將position的值回退到起初的值,並且將limit設置爲有效位置,才能讓讀入的數據真正的寫入Channel。調用flip()方法後,buffer中的四個標記會發生以下變化:

Java代碼
limit = position;
position = 0;
mark = -1;

2. clear()方法:同樣的,將buffer中的數據寫入Channel後,再讀取一些數據到buffer中,此時往往需要將各標記的值歸位,當做一個新的buffer來使用(當然也有特殊情況)。調用clear()方法後,標記變化如下:

Java代碼
position = 0;
limit = capacity;
mark = -1;

3. rewind()方法:如果發現剛纔從Channel讀取的數據需要重新讀取,可以調用該方法。調用後標記變化如下:

Java代碼
position = 0;
mark = -1;
尤其是flip()和clear()方法,在使用的過程中會頻繁用到,否則會造成讀取和寫入的錯亂。
ByteBuffer主要有兩個繼承的類分別是:HeapByteBuffer和MappedByteBuffer。他們的不同之處在於HeapByteBuffer會在JVM的堆上分配內存資源,而MappedByteBuffer的資源則會由JVM之外的操作系統內核來分配。DirectByteBuffer繼承了MappedByteBuffer,採用了直接內存映射的方式,將文件直接映射到虛擬內存,同時減少在內核緩衝區和用戶緩衝區之間的調用,尤其在處理大文件方面有很大的性能優勢。但是在使用內存映射的時候會造成文件句柄一直被佔用而無法刪除的情況,網上也有很多介紹。

Netty中的Buffer
Netty中使用ChannelBuffer來處理讀寫,之所以廢棄ByteBuffer,官方說法是ChannelBuffer簡單易用並且有性能方面的優勢。在ChannelBuffer中使用ByteBuffer或者byte[]來存儲數據。同樣的,ChannelBuffer也提供了幾個標記來控制讀寫並以此取代ByteBuffer的position和limit,分別是:
0 <= readerIndex <= writerIndex <= capacity,同時也有類似於mark的markedReaderIndex和markedWriterIndex。當寫入buffer時,writerIndex增加,從buffer中讀取數據時readerIndex增加,而不能超過writerIndex。有了這兩個變量後,就不用每次寫入buffer後調用flip()方法,方便了很多。

在ChannelBuffer中有幾個重要的類繼承,AbstractChannelBuffer中實現了基本的方法;HeapChannelBuffer是對NIO中heapBuffer的封裝,它有兩個繼承類:BigEndianHeapChannelBuffer和LittleEndianHeapChannelBuffer(試想,我們將一個int類型(32位)的數據放入內存中,內存會以什麼樣的順序放入這32位的數據呢?這就分爲big-endian和little-endian的字節序,big-endian就是說將數據的高位放在內存地址更小的位置,little-endian是將低位放在內存地址更小的位置,選擇和所用硬件和操作系統相同的字節序有利於提高性能);ByteBufferBackedChannelBuffer是對NIO中derectBuffer的封裝;DynamicChannelBuffer繼承於AbstractChannelBuffer,實現了buffer的自動擴容;CompositeChannelBuffer也是繼承於AbstractChannelBuffer,抽象了操作多個buffer的情況,將多個buffer有序的放入數組中,通過計算找出要操作的buffer的下標,而不是將多個buffer複製到一個更大的buffer中;實現WrappedChannelBuffer接口的類主要是對buffer進行進一步的包裝,一般由netty框架內部調用;ReplayingDecoderBuffer用於封裝瞭解碼時常有的處理,配合ReplayDecoder使用,後面會對編碼解碼做專門研究。
ChannelBuffer往往由BufferFactory或者ChannelBuffers類來創建實例。
回到本實例,netty在讀取到客戶端的msg時,根據用戶配置的BufferFactory的不同會將消息封裝成derectBuffer或者heapBuffer。所以,當我們在接收數據時,pipline中第一個upstreamHandler拿到的msg(e.getMessage())雖然是Object類型,但是肯定是這兩種形式的buffer。那麼,我們在寫回數據的時候能不能直接使用其他類型呢,答案是:pipline中第一個downstreamHandler不能隨意的放入其他對象,原因是pipline中downstream事件是從tail端往上執行的,所以第一個downstreamHandler調用的channel.write()或者channels.write()方法傳入的object會直接傳遞給netty底層處理,而在netty的寫入出口中,只接收兩種類型的對象:ChannelBuffer和FileRegion(FileRegion可用於0-copy的文件傳輸,一般情況下,應用程序向socket發送文件流時,操作系統需要先將文件的字節流存儲到內核緩衝區(file_read_buffer),然後拷貝到用戶緩衝區,在由用戶緩衝區拷貝到內核緩衝區(socket_buffer)由協議引擎發送。這樣會創建四個緩衝區,兩次複製的過程。所謂零拷貝是指:內核通過DMA引擎,直接將file_read_buffer的數據copy到socket_buffer中,而在後面的改進中廢除了複製的操作,給socket_buffer增加了數據的位置和長度信息描述,直接將數據從file_read_buffer傳遞給協議引擎。在netty中只有選擇NIO模型才能支持0-copy,當然JDK版本或者操作系統不支持也是不行的)。所以本實例先將字符串”success”方法放入到ChannelBuffer中,然後在調用write方法。
Channels爲我們提供了很多方法用於向上或者向下傳遞事件,對應於概念篇中講到的各種事件。其中以fire開頭方法用於upstream事件,而例如write這樣的方法主要用於downstream事件。而這些方法往往是成對出現的,例如:fireChannelOpen(Channel channel)、fireChannelOpen(ChannelHandlerContext ctx)等,由於這兩個方法有不同的參數,造成了流程的不同,如果參數是Channel,則整個流程會從pipline的head upstreamHandler開始重新執行,如果參數是ChannelHandlerContext,則會直接執行下一個upstreamHandler。同樣,write(Channel channel, Object message)、write(ChannelHandlerContext ctx, ChannelFuture future, Object message)等方法,出了多了一個future外,和前面的兩個方法相同,流程也會相同,如果沒有傳入ChannelFuture,則會在方法中的第一步中創建一個future來支持netty的異步事件處理機制。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章