无法分配更多的internet句柄怎么回事_一文精通Java NIO(内容较多,无耐心者勿点)...
本文揭示了Java NIO底層的諸多細(xì)節(jié)與使用和理解上的陷阱,對(duì)于NIO的學(xué)習(xí)非常有幫助。
本文是筆者在學(xué)習(xí)NIO過程中發(fā)現(xiàn)的一些比較容易讓人忽略的知識(shí)的一個(gè)總結(jié),而這些讓人忽略的小細(xì)節(jié)恰恰是NIO網(wǎng)絡(luò)編程中必不可少。雖然現(xiàn)在我們不會(huì)直接編寫NIO來完成我們的網(wǎng)絡(luò)層通訊,而是使用成熟的基于NIO的網(wǎng)絡(luò)框架來實(shí)現(xiàn)我們的網(wǎng)絡(luò)層。如,netty、mina。但對(duì)NIO網(wǎng)絡(luò)編程過程的了解,非常有助于我們更深入的理解netty、mina等網(wǎng)絡(luò)框架,以至于能更好的使用它們。因此,本文并不對(duì)NIO的一些底層知識(shí)做過多的介紹,主要側(cè)重于NIO編程中細(xì)節(jié)的講解。
NIO vs IO
標(biāo)準(zhǔn)的IO基于字節(jié)流和字符流進(jìn)行操作的;而NIO是基于通道(Channel)進(jìn)行操作的。
通道是雙向的,既可以寫數(shù)據(jù)到通道,又可以從通道中讀取數(shù)據(jù);而流的讀寫通常是單向的,要么是輸入流,要么是輸出流,不能既是輸入流又是輸出流。
NIO能夠?qū)崿F(xiàn)非阻塞的網(wǎng)絡(luò)通信,而IO只能實(shí)現(xiàn)阻塞式的網(wǎng)絡(luò)通信。?
Buffer
Java NIO中的Buffer用于和NIO通道進(jìn)行交互。數(shù)據(jù)總是從通道讀取到緩沖區(qū),或者從緩沖區(qū)寫入到通道中。
Buffer是一個(gè)特定的原生類型數(shù)據(jù)容器。
Buffer是一種特定的原生類型的線程的、有限的元素序列。除了它的內(nèi)容之外,一個(gè)Buffer一個(gè)重要的本質(zhì)屬性是它的capacity、limit、和position;
capacity:一個(gè)buffer的capacity指的就是它所包含的元素的個(gè)數(shù)。buffer的capacity永遠(yuǎn)不會(huì)是負(fù)數(shù),且永遠(yuǎn)不會(huì)變化。
limit:一個(gè)buffer的limit指的是不應(yīng)該被讀或?qū)懙牡谝粋€(gè)元素的索引( position <= limit )。一個(gè)buffer的limit永遠(yuǎn)不會(huì)是負(fù)數(shù)的,并且永遠(yuǎn)不會(huì)超過它的capacity。
position:一個(gè)buffer的position指的是下一個(gè)將要被讀或?qū)懙脑氐乃饕R粋€(gè)buffer的position永遠(yuǎn)不會(huì)是負(fù)數(shù)的,并且永遠(yuǎn)不會(huì)超過它的limit( 這里也說明,position最多等于limit,當(dāng)position==limit時(shí),這個(gè)時(shí)候是不能夠在從buffer中讀取到數(shù)據(jù)了 )。
數(shù)據(jù)操作:
Buffer的每一個(gè)子類都定義了兩類get和put操作。
相對(duì)操作:讀或?qū)?一個(gè)或多個(gè)元素 從當(dāng)前position位置開始并且會(huì)根據(jù)轉(zhuǎn)換元素?cái)?shù)量增加position的值。如果要求的轉(zhuǎn)換超過了limit,那么一個(gè)相關(guān)的get操作會(huì)拋出BufferUnderflowException,一個(gè)相關(guān)的put操作會(huì)拋出一個(gè)BufferOverflowException,無論是這兩個(gè)哪種情況發(fā)生,都不會(huì)有數(shù)據(jù)被傳遞。
絕對(duì)操作:會(huì)接受一個(gè)顯示元素的索引并且不會(huì)影響position。如果索引參數(shù)超過了limit,那么絕對(duì)的get和put操作會(huì)拋出一個(gè)IndexOutOfBoundsException異常。
不變性:
0 <= mark <= position <= limit <= capacity
線程安全性:
buffer在多線程并發(fā)下并不是安全的。如果一個(gè)buffer會(huì)在多個(gè)線程使用,那么需要使用恰當(dāng)?shù)耐讲僮鱽碓L問buffer。也就是buffer本身并不是線程安全的。
Java NIO 內(nèi)存分配
Heap buffer :堆棧的內(nèi)存分配。堆棧就是Java內(nèi)存模型當(dāng)中內(nèi)存的區(qū)域,位于堆上,堆是我們生成對(duì)象的區(qū)域。
Direct buffer :堆外內(nèi)存分配。這個(gè)內(nèi)存本身不是由JVM進(jìn)行控制的,它是由操作系統(tǒng)進(jìn)行統(tǒng)一的處理的。通過這種直接的緩沖就能實(shí)現(xiàn)zero-copy(零拷貝)的動(dòng)作。[ 關(guān)于堆外內(nèi)存可詳見:堆外內(nèi)存 之 DirectByteBuffer 詳解?]
方法
flip()
flip方法將Buffer從寫模式切換到讀模式。rewind()
rewind()方法將position設(shè)回0,limit保持不變,所以你可以重讀Buffer中的所有數(shù)據(jù)。可見在調(diào)用rewind()之前Buffer已經(jīng)是處于讀模式了clear()
讓Buffer重新準(zhǔn)備好重頭開始再次被寫入。該方法會(huì)將position、limit重置。如果此時(shí)還沒有讀取的數(shù)據(jù),則就無法讀取到了。雖然clear()不會(huì)清楚數(shù)據(jù),但是position、limit標(biāo)志位被重置了,所以無法找到哪些未讀取數(shù)據(jù)的位置了。compact()
compact()方法將所有未讀的數(shù)據(jù)拷貝到Buffer起始處。然后將position設(shè)到最后一個(gè)未讀元素正后面。limit屬性依然像clear()方法一樣,設(shè)置成capacity。現(xiàn)在Buffer準(zhǔn)備好寫數(shù)據(jù)了,但是不會(huì)覆蓋未讀的數(shù)據(jù)。clear() VS compact()
clear只是對(duì)position、limit、mark進(jìn)行重置,而compact在對(duì)position進(jìn)行設(shè)置,以及l(fā)imit、mark進(jìn)行重置的同時(shí),還涉及到數(shù)據(jù)在內(nèi)存中拷貝。所以compact比clear更耗性能。但compact能保存你未讀取的數(shù)據(jù),將新數(shù)據(jù)追加到為讀取的數(shù)據(jù)之后;而clear則不行,若你調(diào)用了clear,則未讀取的數(shù)據(jù)就無法再讀取到了。Slice Buffer與原有buffer共享相同的底層數(shù)據(jù)
ByteBuffer.slice(start, end) —————— [start, end),即包含start,不包含end
slice返回的ByteBuffer底層數(shù)據(jù)和源ByteBuffer是共享的,所以無論對(duì)那個(gè)buffer進(jìn)行修改,都會(huì)影響到另一buffer。buffer.asReadOnlyBuffer()
只讀buffer適用于方法傳遞時(shí),你只希望你的調(diào)用端去讀取你所提供的buffer。即,將一個(gè)只讀buffer當(dāng)做參數(shù)傳遞給某個(gè)方法。ByteBuffer.wrap(byte[] array)
該方法生成的ByteBuffer底層就是你傳進(jìn)來的這個(gè)array數(shù)組,并沒有進(jìn)行數(shù)組拷貝,所以是和你傳進(jìn)來的array共享內(nèi)容的。這也導(dǎo)致如果你修改了傳進(jìn)來的array數(shù)組的內(nèi)容,是會(huì)反映到ByteBuffer的。關(guān)于Buffer的Scattering與Gathering
Scattering:允許read的時(shí)候傳遞一個(gè)buffer[]數(shù)組。將一個(gè)Channel中的數(shù)據(jù)給讀到了多個(gè)buffer當(dāng)中,它是按照順序依次讀入buffer當(dāng)中的,而且總是當(dāng)當(dāng)前buffer已經(jīng)寫滿了才會(huì)寫下一個(gè)buffer。
Gathering:允許write的時(shí)候傳遞一個(gè)buffer[]數(shù)組。將多個(gè)buffer的數(shù)據(jù)寫到一個(gè)Channel中。它會(huì)將第一個(gè)buffer中可讀的數(shù)據(jù)都寫入channel后,再將下一個(gè)buffer中的數(shù)據(jù)寫入到channel中,以此依次將buffer中可讀取的數(shù)據(jù)寫到channel中。
Scattering與Gathering適用于網(wǎng)絡(luò)操作中的自定義協(xié)議。比如,一個(gè)請(qǐng)求中帶有兩個(gè)請(qǐng)求頭以及一個(gè)body,第一個(gè)請(qǐng)求頭的數(shù)據(jù)長(zhǎng)度固定是10個(gè)byte,第二個(gè)請(qǐng)求頭的數(shù)據(jù)長(zhǎng)度固定是5個(gè)byte,而body的長(zhǎng)度是不確定的。那么我們就可以用3個(gè)buffer組成的數(shù)組來接這樣的請(qǐng)求。bytebuffer[]數(shù)組中,第一個(gè)bytebuffer元素的容量為10,用于接受第一個(gè)請(qǐng)求頭的信息;第二個(gè)bytebuffer元素的容量為5,用于接受第二個(gè)請(qǐng)求頭的信息;第三個(gè)定義一個(gè)大容量的bytebuffer用于接受body的信息。這樣就天然的實(shí)現(xiàn)了一種數(shù)據(jù)的分門別類。
Selector
為什么使用Selector?
僅用單個(gè)線程來處理多個(gè)Channels的好處是,只需要更少的線程來處理通道。事實(shí)上,可以只用一個(gè)線程處理所有的通道。因?yàn)閷?duì)于操作系統(tǒng)來說,線程之間上下文切換的開銷很大,而且每個(gè)線程都要占用系統(tǒng)的一些資源。因此,使用的線程越少越好。
selector的非阻塞模式
與Selector一起使用時(shí),Channel必須處于非阻塞模式下。這意味著不能將FileChannel與Selector一起使用,因?yàn)镕ileChannel不能切換到非阻塞模式。而套接字通道都可以。?
方法
wakeUp()
如果有其它線程調(diào)用了wakeup()方法,但當(dāng)前沒有線程阻塞在select()方法上,下個(gè)調(diào)用select()方法的線程會(huì)立即"醒來(wake up)"。close()
用完Selector后調(diào)用其close()方法會(huì)關(guān)閉該Selector,即使注冊(cè)到該Selector上的所有SelectionKey實(shí)例無效。通道本身并不會(huì)關(guān)閉。
linux下Selector底層是通過epoll來實(shí)現(xiàn)的,當(dāng)創(chuàng)建好epoll句柄后,它就會(huì)占用一個(gè)fd值,所以在使用完epoll后,必須調(diào)用close()關(guān)閉,否則可能導(dǎo)致fd被耗盡。
關(guān)于selector的詳細(xì)實(shí)現(xiàn)可見淺談 Linux 中 Selector 的實(shí)現(xiàn)原理?
SocketChannel
Java NIO中的SocketChannel是一個(gè)連接到TCP網(wǎng)絡(luò)套接字的通道。?
方法
connect()
如果這個(gè)channel是非阻塞模式的,那么該方法的調(diào)用將啟動(dòng)一個(gè)非阻塞的操作。如果連接立即建立,當(dāng)在連接一個(gè)本地地址時(shí)會(huì)發(fā)生,那么該方法會(huì)返回true。否則若連接還未建立該方法會(huì)返回一個(gè)false,并且連接操作最后必須通過調(diào)用finishConnect方法來完成。
這個(gè)方法可能在任何時(shí)候被調(diào)用。如果在該方法調(diào)用時(shí),對(duì)應(yīng)的channel執(zhí)行了read或write操作,那么read或write操作將先會(huì)被阻塞直到connect操作完成。如果連接嘗試啟動(dòng)但是失敗了,也就是說,如果connect方法的調(diào)用拋出了一個(gè)檢查異常,那么該通道將被關(guān)閉。
寫了代碼測(cè)試了下,無論是是本機(jī),還是跨機(jī)器調(diào)用,都是返回false。finishConnect()
通過設(shè)置一個(gè)socket為非阻塞模式來開啟一個(gè)非阻塞連接操作,然后調(diào)用該socket的connect方法。一旦連接建立,或者嘗試連接失敗,那么socket channel將變?yōu)榭蛇B接的并且該方法可能被調(diào)用已完成連接的后續(xù)事件。如果連接操作失敗,則調(diào)用該方法將導(dǎo)致一個(gè)相關(guān)的IOException異常被拋出。
如果這個(gè)channel已經(jīng)連接了,那么調(diào)用該方法不會(huì)阻塞并會(huì)立即返回true。如果這個(gè)channel是非阻塞模式的,那么該方法將返回false如果連接操作還沒完成。如果這個(gè)channel是阻塞模式的,那么該方法將會(huì)阻塞直到連接成功或失敗,如果連接成功則返回true,否則將拋出一個(gè)檢查異常以描述失敗。
這個(gè)方法可能在任何時(shí)候被調(diào)用。如果在該方法調(diào)用時(shí),對(duì)應(yīng)的channel執(zhí)行了read或write操作,那么read或write操作將先會(huì)被阻塞直到connect操作完成。如果了解嘗試啟動(dòng)但是失敗了,也就是說,如果connect方法的調(diào)用拋出了一個(gè)檢查異常,那么該通道將被關(guān)閉。isConnectionPending()
告知這個(gè)channel是否正在進(jìn)行連接操作。
僅當(dāng)這個(gè)channel的連接操作已經(jīng)啟動(dòng),但是還沒完成( 用通過調(diào)用finishConnect方法來完成 )。
示例:
無論如何在connect后finishConnect()sorry 方法都是需要被調(diào)用的。調(diào)用finishConnect()的三種返回:
① 如果你在connect()后直接調(diào)用了finishConnect()( 并非在CONNECT事件中調(diào)用 ),則若finishConnect()返回了true,則表示channel連接已經(jīng)建立,而且CONNECT事件也不會(huì)被觸發(fā)了。
② 如果finishConnect()方法返回false,則表示連接還未建立好。那么就可以通過CONNECT事件來監(jiān)聽連接的完成。當(dāng)然也可以像上面的寫法,無論如何都會(huì)給SocketChannel注冊(cè)CONNECT事件,finishConnect()方法的調(diào)用放到CONNECT事件處理中調(diào)用。
③ 如果finishConnect()方法拋出了一個(gè)IOException異常,則表示連接操作失敗。
支持的事件:SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE
ServerSocketChannel Java NIO中的 ServerSocketChannel 是一個(gè)可以監(jiān)聽新進(jìn)來的TCP連接的通道, 就像標(biāo)準(zhǔn)IO中的ServerSocket一樣。
支持的事件:SelectionKey.OP_ACCEPT
ServerSocketChannel & SocketChannel 關(guān)于selectedKey集合的處理 對(duì)于已經(jīng)處理的SelectionKey需要充selectedKey集合中移除,如果不將已經(jīng)處理的SelectionKey從selectedKey集合中移除,那么下次有新事件到來時(shí),在遍歷selectedKey集合時(shí)又會(huì)遍歷到這個(gè)SelectionKey,這個(gè)時(shí)候就很可能出錯(cuò)了。比如,如果沒有在處理完OP_ACCEPT事件后將對(duì)應(yīng)SelectionKey從selectedKey集合移除,那么下次遍歷selectedKey集合時(shí),處理到到該SelectionKey,相應(yīng)的ServerSocketChannel.accept()將返回一個(gè)空(null)的SocketChannel。
關(guān)于OP_WRITE事件:
OP_WRITE事件的就緒條件并不是發(fā)生在調(diào)用channel的write方法之后,而是在當(dāng)?shù)讓泳彌_區(qū)有空閑空間的情況下。因?yàn)閷懢彌_區(qū)在絕大部分時(shí)候都是有空閑空間的,所以如果你注冊(cè)了寫事件,這會(huì)使得寫事件一直處于就就緒,選擇處理現(xiàn)場(chǎng)就會(huì)一直占用著CPU資源。所以,只有當(dāng)你確實(shí)有數(shù)據(jù)要寫時(shí)再注冊(cè)寫操作,并在寫完以后馬上取消注冊(cè)。
其實(shí),在大部分情況下,我們直接調(diào)用channel的write方法寫數(shù)據(jù)就好了,沒必要都用OP_WRITE事件。那么OP_WRITE事件主要是在什么情況下使用的了?
其實(shí)OP_WRITE事件主要是在發(fā)送緩沖區(qū)空間滿的情況下使用的。如:
while (buffer.hasRemaining()) { ? ? int len = socketChannel.write(buffer); ?if (len == 0) {? ? ? ? ?selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE);
? ? ? ? ?selector.wakeup(); ? ? ? ? ?break;
? ? }
}
當(dāng)buffer還有數(shù)據(jù),但緩沖區(qū)已經(jīng)滿的情況下,socketChannel.write(buffer)會(huì)返回已經(jīng)寫出去的字節(jié)數(shù),此時(shí)為0。那么這個(gè)時(shí)候我們就需要注冊(cè)O(shè)P_WRITE事件,這樣當(dāng)緩沖區(qū)又有空閑空間的時(shí)候就會(huì)觸發(fā)OP_WRITE事件,這是我們就可以繼續(xù)將沒寫完的數(shù)據(jù)繼續(xù)寫出了。
而且在寫完后,一定要記得將OP_WRITE事件注銷:
selectionKey.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);
注意,這里在修改了interest之后調(diào)用了wakeup();方法是為了喚醒被堵塞的selector方法,這樣當(dāng)while中判斷selector返回的是0時(shí),會(huì)再次調(diào)用selector.select()。而selectionKey的interest是在每次selector.select()操作的時(shí)候注冊(cè)到系統(tǒng)進(jìn)行監(jiān)聽的,所以在selector.select()調(diào)用之后修改的interest需要在下一次selector.select()調(diào)用才會(huì)生效。
關(guān)于遠(yuǎn)端關(guān)閉事件
SelectionKey并沒有提供關(guān)閉事件,其實(shí)通過OP_READ是可以監(jiān)聽到遠(yuǎn)端的關(guān)閉操作的。
當(dāng)OP_READ事件觸發(fā)使,int readByteNum = channel.read(buffer)會(huì)返回從channel讀取到的字節(jié)數(shù)。
① 當(dāng)readByteNum > 0 時(shí),表示從channel讀取到了readByteNum個(gè)字節(jié)到buffer中。
② 當(dāng)readByteNum == 0 時(shí),表示channel中已經(jīng)沒有數(shù)據(jù)可以讀取了,這個(gè)時(shí)候buffer的position==limit。
③?當(dāng) readByteNum == -1 時(shí),表示遠(yuǎn)端channel正常關(guān)閉了。這個(gè)時(shí)候我們就需要進(jìn)行該通道的關(guān)閉和注銷操作了。?netty源碼中OP_READ事件也會(huì)根據(jù)讀取到的字節(jié)數(shù)為-1時(shí),進(jìn)行channel的關(guān)閉操作。
這里closeOnRead(pipeline)方法最終會(huì)調(diào)用channel.close()方法來完成tcp套接字的關(guān)閉(這點(diǎn)下面會(huì)詳細(xì)說明)
如何正確的關(guān)閉一個(gè)已經(jīng)注冊(cè)的SelectableChannel了?
需要調(diào)用channel.close()
最終調(diào)用的會(huì)使AbstractInterruptibleChannel的close方法?
總歸來說,調(diào)用channel.close()方法:
① 能夠調(diào)動(dòng)channel對(duì)應(yīng)的SelectionKey的cancel()方法使該SelectionKey加到Selector的cancel selectionKey set集合中,這樣在下一次selector的時(shí)候,就會(huì)將其從selector中相關(guān)的selectionKey集合中移除,并且不會(huì)監(jiān)聽該selectionKey所感興趣的事件了。
② 會(huì)關(guān)閉底層的套接字連接。
這里注意:如果只是通過調(diào)用SelectionKey.cancel()來注銷一個(gè)遠(yuǎn)端已經(jīng)關(guān)閉了的channel,是一個(gè)不對(duì)的方法。因?yàn)閟elector.select()在處理cancel selectionKey set(注銷的SelectionKey集合)的時(shí)候,會(huì)判斷若該SelectionKey對(duì)應(yīng)的channel已經(jīng)沒有注冊(cè)到其他的selector,并且該channel open表示為false的情況下,才會(huì)去調(diào)用底層套接字的關(guān)閉操作。所以如果之調(diào)用SelectionKey.cancel()來注銷一個(gè)遠(yuǎn)端已經(jīng)關(guān)閉了的channel,會(huì)導(dǎo)致本段的TCP連接處于“CLOSE_WAIT”狀態(tài),一直在等待程序調(diào)用套接字的關(guān)閉。
補(bǔ)充:channel的open標(biāo)志,只有在下面兩種情況下才會(huì)將open置為false。
a) 調(diào)用了channel.close()方法;
b) 或者操作channel讀/寫的當(dāng)前線程發(fā)生中斷時(shí)。
總結(jié)
以上是生活随笔為你收集整理的无法分配更多的internet句柄怎么回事_一文精通Java NIO(内容较多,无耐心者勿点)...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python打地鼠游戏代码100行_Py
- 下一篇: java美元兑换,(Java实现) 美元