1. 程式人生 > >Socket 通訊中由 read 返回值造成的的死鎖問題(socket 阻塞)

Socket 通訊中由 read 返回值造成的的死鎖問題(socket 阻塞)

詳細見原文。


示例

在第一章中,作者給出了一個 TCP Socket 通訊的例子——反饋伺服器,即伺服器端直接把從客戶端接收到的資料原原本本地反饋回去。

問題的引出

明確問題

  • 客戶端與伺服器端在接收和傳送資料時,read()和write()方法不一定要對應,比如,其中一方可以一次傳送多個位元組的資料,而另一方可以一個位元組一個位元組地接收,也可以一個位元組一個位元組地方送,而多個位元組多個位元組地接收。因為TCP協議會將資料分成多個塊進行傳送,而後在另一端會從多個塊進行接收,再組合在一起,它並不僅能確定read()和write()方法中所傳送資訊的界限。

  • read()方法會在沒有資料可讀時發生阻塞,直到有新的資料可讀。

注意客戶端中下面部分程式碼。

while (totalBytesRcvd < data.length) {  
    if ((bytesRcvd = in.read(data, totalBytesRcvd,data.length - totalBytesRcvd)) == -1)  
        throw new SocketException("Connection closed prematurely");  
    totalBytesRcvd += bytesRcvd;  
}  // data array is full 

客戶端從 Socket 套接字中讀取資料,直到收到的資料的位元組長度和原來發送的資料的位元組長度相同為止,這裡的前提是已經知道了要從伺服器端接收的資料的大小,如果現在我們不知道要反饋回來的資料的大小,那麼我們只能用 read 方法不斷讀取,直到 read()返回 -1,說明接收到了所有的資料。我這裡採用一個位元組一個位元組讀取的方式,程式碼改為如下:

while((bytesRcvd = in.read())!= -1){  
    data[totalBytesRcvd] = (byte)bytesRcvd;  
    totalBytesRcvd++;  
}  

這時問題就來了

問題的分析

客戶端沒有資料打印出來,初步推斷應該是 read()方法始終沒有返回 -1,導致程式一直無法往下執行,我在客客戶端執行視窗中按下 CTRL+C,強制結束執行,在伺服器端丟擲如下異常:

Exception in thread "main" java.net.SocketException: Connection reset
        at java.net.SocketInputStream.read(Unknown Source)
        at java.net.SocketInputStream.read(Unknown Source)
        at TCPEchoServer.main(TCPEchoServer.java:32)

異常顯示,問題出現在服務端的 32 行,沒有資源可讀,現在很有可能便是由於 read()方法始終沒有返回 -1 所致,為了驗證,我在客戶端讀取位元組的程式碼中加入了一行列印讀取的單個字元的程式碼,如下:

while((bytesRcvd = in.read())!= -1){  
    data[totalBytesRcvd] = (byte)bytesRcvd;  
    System.out.println((char)data[totalBytesRcvd]);  
    totalBytesRcvd++;  
}  

很明顯,客戶端程式在打印出最有一個位元組後不再往下執行,沒有執行其後面的 System.out.println("Received: " + new String(data));這行程式碼,這是因為 read()方法已經將資料讀完,沒有資料可讀,但又沒有返回 -1,因此在此處產生了阻塞。這便造成了 TCP Socket 通訊的死鎖問題。

問題的解決

問題就出現在 read()方法上,這裡的重點是 read()方法何時返回-1,在一般的檔案讀取中,這代表流的結束,亦即讀取到了檔案的末尾,但是在 Socket 套接字中,這樣的概念很模糊,因為套接字中資料的末尾並沒有所謂的結束標記,無法通過其自身表示傳輸的資料已經結束,那麼究竟什麼時候 read()會返回 -1 呢?答案是:當 TCP 通訊連線的一方關閉了套接字時。

再次分析改過後的程式碼,客戶端用到了read()返回 -1 這個條件,而服務端也用到了,只有二者有一方關閉了 Socket,另一方的 read()方法才會返回 -1,而在客戶端列印輸出前,二者都沒有關閉 Socket,因此,二者的 read()方法都不會返回 -1,程式便阻塞在此處,都不往下執行,這便造成了死鎖。

反過來,再看書上的給出的程式碼,在客戶端程式碼的 while 迴圈中,我們的條件是totalBytesRcvd < data.length,而不是(bytesRcvd = in.read())!= -1,這樣,客戶端在收到與其傳送相同的位元組數之後便會退出 while 迴圈,再往下執行,便是關閉套接字,此時服務端的 read()方法檢測到客戶端的關閉,便會返回 -1,從而繼續往下執行,也將套接字關閉。因此,不會產生死鎖。

那麼,如果在客戶端不知道反饋回來的資料的情況下,該如何避免死鎖呢?Java 的 Socket 類提供了 shutdownOutput()和 shutdownInput()另個方法,用來分別只關閉 Socket 的輸出流和輸入流,而不影響其對應的輸入流和輸出流,那麼我們便可以在客戶端傳送完資料後,呼叫 shutdownOutput()方法將套接字的輸出流關閉,這樣,服務端的 read()方法便會返回 -1,繼續往下執行,最後關閉服務端的套接字,而後客戶端的 read()()方法也會返回 -1,繼續往下執行,直到關閉套接字。

客戶端改變後的程式碼部分如下:

out.write(data);  // Send the encoded string to the server  
socket.shutdownOutput();  

總結

由於 read()方法只有在另一端關閉套接字的輸出流時,才會返回 -1,而有時候由於我們不知道所要接收資料的大小,因此不得不用 read()方法返回 -1 這一判斷條件,那麼此時,合理的程式設計應該是先關閉網路輸出流(亦即套接字的輸出流),再關閉套接字。



原文地址:http://wiki.jikexueyuan.com/project/java-socket/socket-read-deadlock.html