我在一個項目中碰到了一個TcpSocket的應用。在java程式中使用TcpSocket同本機的一個服務進行程序間的通信。
由于通信路徑隻是單機并沒有經過網絡,是以兩個程序之間的互通相對與網絡傳輸是比較快速的。是以,程序間的互動使用了如下方式:
(見上傳圖檔)
讓我們看一下代碼實作:
我們通過調用sendAndReceiveOnce()來接收并傳回資料。
這樣實作會導緻什麼問題呢?
1. 最明顯的就是發送資料,和接收資料在同一個線程裡邊,如果運作在主線程,那麼主線程将會在執行receiveOnce()的時候停滞,除非接收到資料或者觸發逾時異常。但之前已經說明了,應用本函數的環境是單機的程序間的通信,是以接收到資料的時間實際上就取決于Server處理并給予響應時間的長短,不存在網絡傳輸的滞留時間。是以,如果在Server響應快速的情況下,用戶端Socket幾乎感覺不到延遲太多。
2. 再一個明顯問題就是,receiveOnce()根據其函數名稱就可以得知,它隻能讀取一次,如果傳回資訊的長度,大于其緩沖區的長度,那它隻能得到部分資料了。
綜合分析以上兩種情況,我們先解決問題2吧,看看如何讀取完整的響應資訊?
下面是我的實作方法:
這個方法就如同它的注釋所說的那樣,它總是嘗試去僅可能多的讀取資訊,但是在觸發了一次逾時之後将會傳回讀取到的位元組。
有人提問:那判斷流是否已經讀完不是看讀到最後會傳回-1麼?
我的回答是:傳回-1取決于處理的流的源頭是什麼,如果是檔案流,這種想法或許是對的,因為檔案流的大小是固定的,持續的讀,總會讀到檔案的末尾(EOF),它總會傳回-1的。
就像下面的檔案流在讀取檔案的實作一樣,我們這樣讀取檔案流:
但是,如果是網絡流,例如TcpSocket,這樣的流是沒有末尾(EOF)的,如果你想讀到-1,或許在遠端被關閉了,而你還在讀取,還是有可能讀到-1的。實際情況是:網絡連接配接狀況很複雜,很有可能遠端沒有正常關閉而是程序死掉了,而是連接配接的線路斷掉了,或者任何一個原因導緻連接配接的通路無法正常傳輸資料。[b]由于在這種情況下,java中BufferedInputStream的read(byte[] b, int off, int len)函數(其它流對象也一樣)總是嘗試讀取更多的資料,如果沒有設定逾時,就會一直堵塞在那裡,像死掉了一樣。而不是像你所期待的那樣,傳回-1。[/b]是以,我們才才用讓它最少經過一次逾時,來嘗試讀取更多的資料。當然,這也是僅僅在網絡狀況足夠好的情況下,或者逾時對于響應結果不會影響太多的情況下的解決方法。
[i](加一個小插曲:前段時間本人曾經在電話裡面被一個面試我的作開發的兄弟考到“怎麼擷取tcpScoket遠端關閉”,我就是闡述了因為以上觀點“檢測遠端是否關閉的最好方法就是向遠端發送資料,看是否發生IO異常”,而那位仁兄一直堅持遠端關閉得到-1是對的,我說不對就反問:如果不是關閉,而是把網線切斷或者網絡不通也能得到-1麼?仁兄語塞,面試後來以尴尬結束。後來反思自己當時實在太輕狂了,沒給仁兄面子。去不去應聘倒無所謂,但是态度還是不是面試時應該有的态度啊。現在想向那位仁兄道歉,也沒機會了)[/i]
那現在又有一個問題了,雖然與遠端的互動出現無法讀取到資料的時候不會一直堵塞在那裡,像死掉了一樣。但是我在使用blockReceive()的時候[b]總是需要等一個逾時的時間[/b]才能傳回!
那我如果設逾時為5秒,操作完了之後,也至少等到5秒才能達到消息的回報。哦,天哪,慢到要死了!!!!
這個問題必須解決,那麼:
讓我們用更聰明的實作方法吧,我們不要阻塞了!!!
我們發現:這個方法真是不錯!我們每次在讀取資料之前,總是先用available()方法擷取流中目前的最大可讀位元組數,然後再讀。否則,我直接傳回。但是為了以防我在讀取資料的時候也出現逾時問題導緻堵塞,我還是小心的加入了逾時的處理,雖然它在絕大部分情況下并不會發生。
好了!現在我們滿懷希望的來調用所謂的“完美”解決方案:
然後,測試,卻發現了一個奇怪的現象:
我們在程式裡面[b]連續調用了兩次sendAndUnblockReceive(),并期待每次發送都會迅速并完整準确的接收它們每次的響應。[/b]但是,沒有效果。事實是:[b]我第一次發送的請求,并沒有接收到它需要的正确響應。而我們第二次發送的請求,卻接收到了第一次的響應,還要第二次的響應,這樣兩條資料!!![/b]
這是為什麼呢?因為:
[b]
我們第一次在發送完資料之後,馬上就調用了unblocReceive()。但是由于這次我們調用的實在太快了,Server那一端沒來的及處理,甚至沒來的及接收,更不用說響應了。是以unblocReceive()裡面我們用available()方法擷取流中目前的最大可讀位元組數為0!!!是以,當然就不會讀取了!!!而第二次再發送時,第一次的響應剛剛到達,是以,unblocReceive()再被第二次調用的時候“盡最大可能”的讀取到了這兩次的響應資訊。
[/b]
唉,看來就沒有更好的方法了麼?或許,還是有的吧!!!
我們先這樣改一下:
強制的讓線程在發送完後sleep一端時間,半秒鐘,給Server足夠的響應時間,然後再去讀取,或許,這樣比那個blockReceive()的實作要好一點吧。
最後來一下總結:
我們在這個TcpScoket中,在發送和讀取使用同一線程的情況下,使用了三種讀取方式:
[b]一次讀取,阻塞式完整讀取,非阻塞式完整讀取[/b]。
這三種讀取方式的優缺點分析如下:
[b]一次讀取receiveOnce()[/b]: 是最快速,并且在緩沖區足夠大的情況下能夠完整讀取的方法,當然如果沒有設定逾時,它仍然用可能存在阻塞。
[b]阻塞式完整讀取blockReceive()[/b]:在傳回資料之前總是至少經過一次逾時以讀取更多資料,是以在網絡狀況足夠好的情況下,速度仍然比較慢。
[b]非阻塞式完整讀取unblocReceive()[/b]: 在嘗試讀取資料之前,首先判斷可以讀取的最大位元組數,如果有資料則嘗試去讀,否則直接傳回。所有是這一種不用考慮緩沖區大小,還能兼顧速度的方法。但是如果遠端響應慢的情況下,依然會錯過讀取資料。
綜合上述三中讀取方式:我們可以在确定傳回資料量較少,而又要求速度快而準确的情況下,使用receiveOnce()。在傳回資料量較多,而又要求速度快而準确的情況下,使用unblocReceive(),不過需要留給遠端足夠的響應時間。在不需要響應速度很快,而需要傳回大量資料,而且準确的情況下使用blockReceive()。
現在我們抛開這些,想一想:
我們真的需要這樣三種讀取方式嗎?需要嗎?
我們為什麼這麼羅裡羅嗦使用這三種方式?
因為,[size=x-large][b]我們把發送資料,和接收資料這兩個功能放在一個線程裡執行了!!![/b][/size]
這才是最主要的問題!
是以,:
[size=x-large][b]盡量不要在使用Socket的流的時候,把發送資料和接收資料的調用放在一個線程裡。[/b][/size]
因為,網絡上的流是不穩定的,是以java在設計流的時候也是盡量去讀取盡可能多的資料,很可能發生堵塞。如果放在一個線程裡面,試圖我發送了就會想當然的又快又準确的接收到,就會像上面的解決方案一樣,用盡招數,仍然束手無策。