您好,登錄后才能下訂單哦!
本篇內容主要講解“Java NIO怎么處理慢速的連接”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Java NIO怎么處理慢速的連接”吧!
對企業級的服務器軟件,高性能和可擴展性是基本的要求。除此之外,還應該有應對各種不同環境的能力。例如,一個好的服務器軟件不應該假設所有的客戶端都有很快的處理能力和很好的網絡環境。如果一個客戶端的運行速度很慢,或者網絡速度很慢,這就意味著整個請求的時間變長。而對于服務器來說,這就意味著這個客戶端的請求將占用更長的時間。這個時間的延遲不是由服務器造成的,因此CPU的占用不會增加什么,但是網絡連接的時間會增加,處理線程的占用時間也會增加。這就造成了當前處理線程和其他資源得不到很快的釋放,無法被其他客戶端的請求來重用。例如Tomcat,當存在大量慢速連接的客戶端時,線程資源被這些慢速的連接消耗掉,使得服務器不能響應其他的請求了。
前面介紹過,NIO的異步非阻塞的形式,使得很少的線程就能服務于大量的請求。通過Selector的注冊功能,可以有選擇性地返回已經準備好的頻道,這樣就不需要為每一個請求分配單獨的線程來服務。
在一些流行的NIO的框架中,都能看到對OP_ACCEPT和OP_READ的處理。很少有對OP_WRITE的處理。我們經常看到的代碼就是在請求處理完成后,直接通過下面的代碼將結果返回給客戶端:
不對OP_WRITE進行處理的樣例:
while (bb.hasRemaining()) { int len = socketChannel.write(bb); if (len < 0) { throw new EOFException(); } }
這樣寫在大多數的情況下都沒有什么問題。但是在客戶端的網絡環境很糟糕的情況下,服務器會遭到很沉重的打擊。
因為如果客戶端的網絡或者是中間交換機的問題,使得網絡傳輸的效率很低,這時候會出現服務器已經準備好的返回結果無法通過TCP/IP層傳輸到客戶端。這時候在執行上面這段程序的時候就會出現以下情況。
(1) bb.hasRemaining()一直為“true”,因為服務器的返回結果已經準備好了。
(2) socketChannel.write(bb)的結果一直為0,因為由于網絡原因數據一直傳不過去。
(3) 因為是異步非阻塞的方式,socketChannel.write(bb)不會被阻塞,立刻被返回。
(4) 在一段時間內,這段代碼會被無休止地快速執行著,消耗著大量的CPU的資源。事實上什么具體的任務也沒有做,一直到網絡允許當前的數據傳送出去為止。
這樣的結果顯然不是我們想要的。因此,我們對OP_WRITE也應該加以處理。在NIO中最常用的方法如下。
一般NIO框架中對OP_WRITE的處理:
while (bb.hasRemaining()) { int len = socketChannel.write(bb); if (len < 0){ throw new EOFException(); } if (len == 0) { selectionKey.interestOps( selectionKey.interestOps() | SelectionKey.OP_WRITE); mainSelector.wakeup(); break; } }
上面的程序在網絡不好的時候,將此頻道的OP_WRITE操作注冊到Selector上,這樣,當網絡恢復,頻道可以繼續將結果數據返回客戶端的時候,Selector會通過SelectionKey來通知應用程序,再去執行寫的操作。這樣就能節約大量的CPU資源,使得服務器能適應各種惡劣的網絡環境。
可是,Grizzly中對OP_WRITE的處理并不是這樣的。我們先看看Grizzly的源碼吧。在Grizzly中,對請求結果的返回是在ProcessTask中處理的,經過SocketChannelOutputBuffer的類,最終通過OutputWriter類來完成返回結果的動作。在OutputWriter中處理OP_WRITE的代碼如下:
Grizzly中對OP_WRITE的處理:
public static long flushChannel(SocketChannel socketChannel, ByteBuffer bb, long writeTimeout) throws IOException { SelectionKey key = null; Selector writeSelector = null; int attempts = 0; int bytesProduced = 0; try { while (bb.hasRemaining()) { int len = socketChannel.write(bb); attempts++; if (len < 0){ throw new EOFException(); } bytesProduced += len; if (len == 0) { if (writeSelector == null){ writeSelector = SelectorFactory.getSelector(); if (writeSelector == null){ // Continue using the main one continue; } } key = socketChannel.register(writeSelector, key.OP_WRITE); if (writeSelector.select(writeTimeout) == 0) { if (attempts > 2) throw new IOException("Client disconnected"); } else { attempts--; } } else { attempts = 0; } } } finally { if (key != null) { key.cancel(); key = null; } if (writeSelector != null) { // Cancel the key. writeSelector.selectNow(); SelectorFactory.returnSelector(writeSelector); } } return bytesProduced; }
上面的程序例17.9與例17.8的區別之處在于:當發現由于網絡情況而導致的發送數據受阻(len==0)時,例17.8的處理是將當前的頻道注冊到當前的Selector中;而在例17.9中,程序從SelectorFactory中獲得了一個臨時的Selector。在獲得這個臨時的Selector之后,程序做了一個阻塞的操作:writeSelector.select(writeTimeout)。這個阻塞操作會在一定時間內(writeTimeout)等待這個頻道的發送狀態。如果等待時間過長,便認為當前的客戶端的連接異常中斷了。
這種實現方式頗受爭議。有很多開發者置疑Grizzly的作者為什么不使用例17.8的模式。另外在實際處理中,Grizzly的處理方式事實上放棄了NIO中的非阻塞的優勢,使用writeSelector.select(writeTimeout)做了個阻塞操作。雖然CPU的資源沒有浪費,可是線程資源在阻塞的時間內,被這個請求所占有,不能釋放給其他請求來使用。
Grizzly的作者對此的回應如下。
(1) 使用臨時的Selector的目的是減少線程間的切換。當前的Selector一般用來處理OP_ACCEPT,和OP_READ的操作。使用臨時的Selector可減輕主Selector的負擔;而在注冊的時候則需要進行線程切換,會引起不必要的系統調用。這種方式避免了線程之間的頻繁切換,有利于系統的性能提高。
(2) 雖然writeSelector.select(writeTimeout)做了阻塞操作,但是這種情況只是少數極端的環境下才會發生。大多數的客戶端是不會頻繁出現這種現象的,因此在同一時刻被阻塞的線程不會很多。
(3) 利用這個阻塞操作來判斷異常中斷的客戶連接。
(4) 經過壓力實驗證明這種實現的性能是非常好的。
到此,相信大家對“Java NIO怎么處理慢速的連接”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。