新聞中心
對(duì)企業(yè)級(jí)的服務(wù)器軟件,高性能和可擴(kuò)展性是基本的要求。除此之外,還應(yīng)該有應(yīng)對(duì)各種不同環(huán)境的能力。例如,一個(gè)好的服務(wù)器軟件不應(yīng)該假設(shè)所有的客戶端都有很快的處理能力和很好的網(wǎng)絡(luò)環(huán)境。如果一個(gè)客戶端的運(yùn)行速度很慢,或者網(wǎng)絡(luò)速度很慢,這就意味著整個(gè)請(qǐng)求的時(shí)間變長(zhǎng)。而對(duì)于服務(wù)器來(lái)說(shuō),這就意味著這個(gè)客戶端的請(qǐng)求將占用更長(zhǎng)的時(shí)間。這個(gè)時(shí)間的延遲不是由服務(wù)器造成的,因此CPU的占用不會(huì)增加什么,但是網(wǎng)絡(luò)連接的時(shí)間會(huì)增加,處理線程的占用時(shí)間也會(huì)增加。這就造成了當(dāng)前處理線程和其他資源得不到很快的釋放,無(wú)法被其他客戶端的請(qǐng)求來(lái)重用。例如Tomcat,當(dāng)存在大量慢速連接的客戶端時(shí),線程資源被這些慢速的連接消耗掉,使得服務(wù)器不能響應(yīng)其他的請(qǐng)求了。

前面介紹過(guò),NIO的異步非阻塞的形式,使得很少的線程就能服務(wù)于大量的請(qǐng)求。通過(guò)Selector的注冊(cè)功能,可以有選擇性地返回已經(jīng)準(zhǔn)備好的頻道,這樣就不需要為每一個(gè)請(qǐng)求分配單獨(dú)的線程來(lái)服務(wù)。
在一些流行的NIO的框架中,都能看到對(duì)OP_ACCEPT和OP_READ的處理。很少有對(duì)OP_WRITE的處理。我們經(jīng)??吹降拇a就是在請(qǐng)求處理完成后,直接通過(guò)下面的代碼將結(jié)果返回給客戶端:
不對(duì)OP_WRITE進(jìn)行處理的樣例:
- while (bb.hasRemaining()) {
- int len = socketChannel.write(bb);
- if (len < 0) {
- throw new EOFException();
- }
- }
這樣寫(xiě)在大多數(shù)的情況下都沒(méi)有什么問(wèn)題。但是在客戶端的網(wǎng)絡(luò)環(huán)境很糟糕的情況下,服務(wù)器會(huì)遭到很沉重的打擊。
因?yàn)槿绻蛻舳说木W(wǎng)絡(luò)或者是中間交換機(jī)的問(wèn)題,使得網(wǎng)絡(luò)傳輸?shù)男屎艿?,這時(shí)候會(huì)出現(xiàn)服務(wù)器已經(jīng)準(zhǔn)備好的返回結(jié)果無(wú)法通過(guò)TCP/IP層傳輸?shù)娇蛻舳恕_@時(shí)候在執(zhí)行上面這段程序的時(shí)候就會(huì)出現(xiàn)以下情況。
(1) bb.hasRemaining()一直為“true”,因?yàn)榉?wù)器的返回結(jié)果已經(jīng)準(zhǔn)備好了。
(2) socketChannel.write(bb)的結(jié)果一直為0,因?yàn)橛捎诰W(wǎng)絡(luò)原因數(shù)據(jù)一直傳不過(guò)去。
(3) 因?yàn)槭钱惒椒亲枞姆绞剑瑂ocketChannel.write(bb)不會(huì)被阻塞,立刻被返回。
(4) 在一段時(shí)間內(nèi),這段代碼會(huì)被無(wú)休止地快速執(zhí)行著,消耗著大量的CPU的資源。事實(shí)上什么具體的任務(wù)也沒(méi)有做,一直到網(wǎng)絡(luò)允許當(dāng)前的數(shù)據(jù)傳送出去為止。
這樣的結(jié)果顯然不是我們想要的。因此,我們對(duì)OP_WRITE也應(yīng)該加以處理。在NIO中最常用的方法如下。
一般NIO框架中對(duì)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;
- }
- }
上面的程序在網(wǎng)絡(luò)不好的時(shí)候,將此頻道的OP_WRITE操作注冊(cè)到Selector上,這樣,當(dāng)網(wǎng)絡(luò)恢復(fù),頻道可以繼續(xù)將結(jié)果數(shù)據(jù)返回客戶端的時(shí)候,Selector會(huì)通過(guò)SelectionKey來(lái)通知應(yīng)用程序,再去執(zhí)行寫(xiě)的操作。這樣就能節(jié)約大量的CPU資源,使得服務(wù)器能適應(yīng)各種惡劣的網(wǎng)絡(luò)環(huán)境。
可是,Grizzly中對(duì)OP_WRITE的處理并不是這樣的。我們先看看Grizzly的源碼吧。在Grizzly中,對(duì)請(qǐng)求結(jié)果的返回是在ProcessTask中處理的,經(jīng)過(guò)SocketChannelOutputBuffer的類(lèi),最終通過(guò)OutputWriter類(lèi)來(lái)完成返回結(jié)果的動(dòng)作。在OutputWriter中處理OP_WRITE的代碼如下:
Grizzly中對(duì)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的區(qū)別之處在于:當(dāng)發(fā)現(xiàn)由于網(wǎng)絡(luò)情況而導(dǎo)致的發(fā)送數(shù)據(jù)受阻(len==0)時(shí),例17.8的處理是將當(dāng)前的頻道注冊(cè)到當(dāng)前的Selector中;而在例17.9中,程序從SelectorFactory中獲得了一個(gè)臨時(shí)的Selector。在獲得這個(gè)臨時(shí)的Selector之后,程序做了一個(gè)阻塞的操作:writeSelector.select(writeTimeout)。這個(gè)阻塞操作會(huì)在一定時(shí)間內(nèi)(writeTimeout)等待這個(gè)頻道的發(fā)送狀態(tài)。如果等待時(shí)間過(guò)長(zhǎng),便認(rèn)為當(dāng)前的客戶端的連接異常中斷了。
這種實(shí)現(xiàn)方式頗受爭(zhēng)議。有很多開(kāi)發(fā)者置疑Grizzly的作者為什么不使用例17.8的模式。另外在實(shí)際處理中,Grizzly的處理方式事實(shí)上放棄了NIO中的非阻塞的優(yōu)勢(shì),使用writeSelector.select(writeTimeout)做了個(gè)阻塞操作。雖然CPU的資源沒(méi)有浪費(fèi),可是線程資源在阻塞的時(shí)間內(nèi),被這個(gè)請(qǐng)求所占有,不能釋放給其他請(qǐng)求來(lái)使用。
Grizzly的作者對(duì)此的回應(yīng)如下。
(1) 使用臨時(shí)的Selector的目的是減少線程間的切換。當(dāng)前的Selector一般用來(lái)處理OP_ACCEPT,和OP_READ的操作。使用臨時(shí)的Selector可減輕主Selector的負(fù)擔(dān);而在注冊(cè)的時(shí)候則需要進(jìn)行線程切換,會(huì)引起不必要的系統(tǒng)調(diào)用。這種方式避免了線程之間的頻繁切換,有利于系統(tǒng)的性能提高。
(2) 雖然writeSelector.select(writeTimeout)做了阻塞操作,但是這種情況只是少數(shù)極端的環(huán)境下才會(huì)發(fā)生。大多數(shù)的客戶端是不會(huì)頻繁出現(xiàn)這種現(xiàn)象的,因此在同一時(shí)刻被阻塞的線程不會(huì)很多。
(3) 利用這個(gè)阻塞操作來(lái)判斷異常中斷的客戶連接。
(4) 經(jīng)過(guò)壓力實(shí)驗(yàn)證明這種實(shí)現(xiàn)的性能是非常好的。
網(wǎng)站題目:JavaNIO如何處理慢速的連接
網(wǎng)頁(yè)URL:http://m.5511xx.com/article/cdehhhc.html


咨詢
建站咨詢
