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