天天看點

網絡傳輸層TCP協定中InputStream的read方法是否傳回-1

由于通信路徑隻是單機并沒有經過網絡,是以兩個程序之間的互通相對與網絡傳輸是比較快速的。是以,程序間的互動使用了如下方式:

(見上傳圖檔)

讓我們看一下代碼實作:

Java代碼

  1. public synchronized void send(byte[] bytes) throws IOException   
  2. {   
  3.     if (bytes != null && bytes.length > 0)   
  4.      {   
  5.         this.bos.write(bytes);   
  6.         this.bos.flush();   
  7.      }   
  8. }   
  9. public synchronized byte[] receiveOnce() throws IOException   
  10. {   
  11.     byte[] reciveBytes = new byte[0];   
  12.     int len = this.bis.read(this.b_buf, 0, this.bufferSize);   
  13.     return ArrayUtils.addAll(reciveBytes, ArrayUtils.subarray(this.b_buf, 0, len));   
  14. }   
  15. public byte[] sendAndReceiveOnce(byte[] bytes) throws IOException   
  16. {   
  17.     this.send(bytes);   
  18.     return this.receiveOnce();   
  19. }  

我們通過調用sendAndReceiveOnce()來接收并傳回資料。

這樣實作會導緻什麼問題呢?

1.       最明顯的就是發送資料,和接收資料在同一個線程裡邊,如果運作在主線程,那麼主線程将會在執行receiveOnce()的時候停滞,除非接收到資料或者觸發逾時異常。但之前已經說明了,應用本函數的環境是單機的程序間的通信,是以接收到資料的時間實際上就取決于Server處理并給予響應時間的長短,不存在網絡傳輸的滞留時間。是以,如果在Server響應快速的情況下,用戶端Socket幾乎感覺不到延遲太多。

2.       再一個明顯問題就是,receiveOnce()根據其函數名稱就可以得知,它隻能讀取一次,如果傳回資訊的長度,大于其緩沖區的長度,那它隻能得到部分資料了。

綜合分析以上兩種情況,我們先解決問題2吧,看看如何讀取完整的響應資訊?

下面是我的實作方法:

Java代碼

  1. public synchronized byte[] blockReceive() throws IOException   
  2. {   
  3.     byte[] reciveBytes = new byte[0];   
  4.     // 偏移量   
  5.     int offset = 0;   
  6.     // 每次讀取的位元組數   
  7.     int len = 0;   
  8.     while(len != -1)   
  9.      {   
  10.         try  
  11.          {   
  12.              len = this.in.read(this.buffer, 0, this.bufferSize);   
  13.          }   
  14.         catch(SocketTimeoutException e)   
  15.          {   
  16.             break;   
  17.          }   
  18.         if(len != -1)   
  19.          {   
  20.              reciveBytes = ArrayUtils.addAll(reciveBytes, ArrayUtils   
  21.                      .subarray(this.buffer, 0, len));   
  22.              offset = offset + len;   
  23.          }   
  24.      }   
  25.     return reciveBytes;   
  26. }  

這個方法就如同它的注釋所說的那樣,它總是嘗試去僅可能多的讀取資訊,但是在觸發了一次逾時之後将會傳回讀取到的位元組。

有人提問:那判斷流是否已經讀完不是看讀到最後會傳回-1麼?

我的回答是:傳回-1取決于處理的流的源頭是什麼,如果是檔案流,這種想法或許是對的,因為檔案流的大小是固定的,持續的讀,總會讀到檔案的末尾(EOF),它總會傳回-1的。

就像下面的檔案流在讀取檔案的實作一樣,我們這樣讀取檔案流:

Java代碼

  1. protected byte[] receiveBytes() throws IOException   
  2. {   
  3.     byte[] reciveBytes = new byte[0];   
  4.     // 偏移量   
  5.      int offset = 0;   
  6.     // 每次讀取的位元組數   
  7.      int len = 0;   
  8.     while(len != -1)   
  9.      {   
  10.         this.buffer = new byte[bufferSize];   
  11.          len = this.in.read(this.buffer, 0, this.bufferSize);   
  12.         if(len != -1)   
  13.          {   
  14.              reciveBytes = ArrayUtils.addAll(reciveBytes, this.buffer);   
  15.              offset = offset + len;   
  16.          }   
  17.      }   
  18.     return ArrayUtils.subarray(reciveBytes, 0, offset);   
  19. }  

但是,如果是網絡流,例如TcpSocket,這樣的流是沒有末尾(EOF)的,如果你想讀到-1,或許在遠端被關閉了,而你還在讀取,還是有可能讀到-1的。實際情況是:網絡連接配接狀況很複雜,很有可能遠端沒有正常關閉而是程序死掉了,而是連接配接的線路斷掉了,或者任何一個原因導緻連接配接的通路無法正常傳輸資料。由于在這種情況下,java中BufferedInputStream的read(byte[] b, int off, int len)函數(其它流對象也一樣)總是嘗試讀取更多的資料,如果沒有設定逾時,就會一直堵塞在那裡,像死掉了一樣。而不是像你所期待的那樣,傳回-1。是以,我們才才用讓它最少經過一次逾時,來嘗試讀取更多的資料。當然,這也是僅僅在網絡狀況足夠好的情況下,或者逾時對于響應結果不會影響太多的情況下的解決方法。

(加一個小插曲:前段時間本人曾經在電話裡面被一個面試我的作開發的兄弟考到“怎麼擷取tcpScoket遠端關閉”,我就是闡述了因為以上觀點“檢測遠端是否關閉的最好方法就是向遠端發送資料,看是否發生IO異常”,而那位仁兄一直堅持遠端關閉得到-1是對的,我說不對就反問:如果不是關閉,而是把網線切斷或者網絡不通也能得到-1麼?仁兄語塞,面試後來以尴尬結束。後來反思自己當時實在太輕狂了,沒給仁兄面子。去不去應聘倒無所謂,但是态度還是不是面試時應該有的态度啊。現在想向那位仁兄道歉,也沒機會了)

那現在又有一個問題了,雖然與遠端的互動出現無法讀取到資料的時候不會一直堵塞在那裡,像死掉了一樣。但是我在使用blockReceive()的時候總是需要等一個逾時的時間才能傳回!

那我如果設逾時為5秒,操作完了之後,也至少等到5秒才能達到消息的回報。哦,天哪,慢到要死了!!!!

這個問題必須解決,那麼:

讓我們用更聰明的實作方法吧,我們不要阻塞了!!!

Java代碼

  1. public synchronized byte[] unblocReceive() throws IOException   
  2. {   
  3.     byte[] reciveBytes = new byte[0];   
  4.     // 目前流中的最大可讀數   
  5.     int contentLength = this.in.available();   
  6.     // 偏移量   
  7.     int offset = 0;   
  8.     // 每次讀取的位元組數   
  9.     int len = 0;   
  10.     while(contentLength > 0 && offset < contentLength && len != -1)   
  11.      {   
  12.         try  
  13.          {   
  14.              len = this.in.read(this.buffer, 0, this.bufferSize);   
  15.          }   
  16.         catch(SocketTimeoutException e)   
  17.          {   
  18.             break;   
  19.          }   
  20.         if(len != -1)   
  21.          {   
  22.              reciveBytes = ArrayUtils.addAll(reciveBytes, ArrayUtils   
  23.                      .subarray(this.buffer, 0, len));   
  24.              offset = offset + len;   
  25.          }   
  26.      }   
  27.     return reciveBytes;   
  28. }   

我們發現:這個方法真是不錯!我們每次在讀取資料之前,總是先用available()方法擷取流中目前的最大可讀位元組數,然後再讀。否則,我直接傳回。但是為了以防我在讀取資料的時候也出現逾時問題導緻堵塞,我還是小心的加入了逾時的處理,雖然它在絕大部分情況下并不會發生。

好了!現在我們滿懷希望的來調用所謂的“完美”解決方案:

Java代碼

  1. public byte[] sendAndUnblockReceive(byte[] bytes) throws IOException   
  2. {   
  3.     this.send(bytes);          
  4.     return this.unblocReceive();   
  5. }  

然後,測試,卻發現了一個奇怪的現象:

我們在程式裡面連續調用了兩次sendAndUnblockReceive(),并期待每次發送都會迅速并完整準确的接收它們每次的響應。但是,沒有效果。事實是:我第一次發送的請求,并沒有接收到它需要的正确響應。而我們第二次發送的請求,卻接收到了第一次的響應,還要第二次的響應,這樣兩條資料!!!

這是為什麼呢?因為:

我們第一次在發送完資料之後,馬上就調用了unblocReceive()。但是由于這次我們調用的實在太快了,Server那一端沒來的及處理,甚至沒來的及接收,更不用說響應了。是以unblocReceive()裡面我們用available()方法擷取流中目前的最大可讀位元組數為0!!!是以,當然就不會讀取了!!!而第二次再發送時,第一次的響應剛剛到達,是以,unblocReceive()再被第二次調用的時候“盡最大可能”的讀取到了這兩次的響應資訊。

唉,看來就沒有更好的方法了麼?或許,還是有的吧!!!

我們先這樣改一下:

Java代碼

  1. public byte[] sendAndUnblockReceive(byte[] bytes) throws IOException   
  2. {   
  3.     this.send(bytes);   
  4.     // 由于使用了非阻塞接收,為保證在執行read的時候流中恰好有資料,   
  5.     // 必須給背景足夠的響應時間   
  6.     try  
  7.      {   
  8.          Thread.sleep(500);   
  9.      }   
  10.     catch (InterruptedException e)   
  11.      {   
  12.          logger.error("InterruptedException error.", e);   
  13.      }   
  14.     return this.unblocReceive();   
  15. }  

強制的讓線程在發送完後sleep一端時間,半秒鐘,給Server足夠的響應時間,然後再去讀取,或許,這樣比那個blockReceive()的實作要好一點吧。

最後來一下總結:

我們在這個TcpScoket中,在發送和讀取使用同一線程的情況下,使用了三種讀取方式:

一次讀取,阻塞式完整讀取,非阻塞式完整讀取。

這三種讀取方式的優缺點分析如下:

一次讀取receiveOnce():   是最快速,并且在緩沖區足夠大的情況下能夠完整讀取的方法,當然如果沒有設定逾時,它仍然用可能存在阻塞。

阻塞式完整讀取blockReceive():在傳回資料之前總是至少經過一次逾時以讀取更多資料,是以在網絡狀況足夠好的情況下,速度仍然比較慢。

非阻塞式完整讀取unblocReceive(): 在嘗試讀取資料之前,首先判斷可以讀取的最大位元組數,如果有資料則嘗試去讀,否則直接傳回。所有是這一種不用考慮緩沖區大小,還能兼顧速度的方法。但是如果遠端響應慢的情況下,依然會錯過讀取資料。

綜合上述三中讀取方式:我們可以在确定傳回資料量較少,而又要求速度快而準确的情況下,使用receiveOnce()。在傳回資料量較多,而又要求速度快而準确的情況下,使用unblocReceive(),不過需要留給遠端足夠的響應時間。在不需要響應速度很快,而需要傳回大量資料,而且準确的情況下使用blockReceive()。

現在我們抛開這些,想一想:

我們真的需要這樣三種讀取方式嗎?需要嗎?

我們為什麼這麼羅裡羅嗦使用這三種方式?

因為,我們把發送資料,和接收資料這兩個功能放在一個線程裡執行了!!!

這才是最主要的問題!

是以,:

盡量不要在使用Socket的流的時候,把發送資料和接收資料的調用放在一個線程裡。

因為,網絡上的流是不穩定的,是以java在設計流的時候也是盡量去讀取盡可能多的資料,很可能發生堵塞。如果放在一個線程裡面,試圖我發送了就會想當然的又快又準确的接收到,就會像上面的解決方案一樣,用盡招數,仍然束手無策。

聲明:摘抄自  http://www.360doc.com/content/11/0731/10/2127922_136887195.shtml

繼續閱讀