1. 程式人生 > >【Java TCP/IP Socket】TCP Socket通信中由read返回值造成的的死鎖問題(含代碼)

【Java TCP/IP Socket】TCP Socket通信中由read返回值造成的的死鎖問題(含代碼)

ray inpu 網絡 數據 code public 文件讀取 情況 從服務器

書上示例

在第一章《基本套接字》中,作者給出了一個TCP Socket通信的例子——反饋服務器,即服務器端直接把從客戶端接收到的數據原原本本地反饋回去。

書上客戶端代碼如下:

import java.net.Socket;  
import java.net.SocketException;  
import java.io.IOException;  
import java.io.InputStream;  
import java.io.OutputStream;  
  
public class TCPEchoClient {  
  
    
public static void main(String[] args) throws IOException { if ((args.length < 2) || (args.length > 3)) // Test for correct # of args throw new IllegalArgumentException("Parameter(s): <Server> <Word> [<Port>]"); String server = args[0]; //
Server name or IP address // Convert argument String to bytes using the default character encoding byte[] data = args[1].getBytes(); int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7; // Create socket that is connected to server on specified port
Socket socket = new Socket(server, servPort); System.out.println("Connected to server...sending echo string"); InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); out.write(data); // Send the encoded string to the server // Receive the same string back from the server int totalBytesRcvd = 0; // Total bytes received so far int bytesRcvd; // Bytes received in last 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 System.out.println("Received: " + new String(data)); socket.close(); // Close the socket and its streams } }

書上的服務器端代碼如下:

import java.net.*;  // for Socket, ServerSocket, and InetAddress  
import java.io.*;   // for IOException and Input/OutputStream  
  
public class TCPEchoServer {  
  
    private static final int BUFSIZE = 32;   // Size of receive buffer  
  
    public static void main(String[] args) throws IOException {  
  
    if (args.length != 1)  // Test for correct # of args  
        throw new IllegalArgumentException("Parameter(s): <Port>");  
  
    int servPort = Integer.parseInt(args[0]);  
  
    // Create a server socket to accept client connection requests  
    ServerSocket servSock = new ServerSocket(servPort);  
  
    int recvMsgSize;   // Size of received message  
    byte[] receiveBuf = new byte[BUFSIZE];  // Receive buffer  
  
    while (true) { // Run forever, accepting and servicing connections  
        Socket clntSock = servSock.accept();     // Get client connection  
  
        SocketAddress clientAddress = clntSock.getRemoteSocketAddress();  
        System.out.println("Handling client at " + clientAddress);  
        
        InputStream in = clntSock.getInputStream();  
        OutputStream out = clntSock.getOutputStream();  
  
  
        // Receive until client closes connection, indicated by -1 return  
        while ((recvMsgSize = in.read(receiveBuf)) != -1) {  
            out.write(receiveBuf, 0, recvMsgSize);  
        }  
  
        clntSock.close();  // Close the socket.  We are done with this client!  
    }  
    /* NOT REACHED */  
  }  
}  

示例程序當然運行無誤,運行結果如下:

技術分享圖片技術分享圖片

問題的引出

首先明確幾點:

1、客戶端與服務器端在接收和發送數據時,read()和write()方法不一定要對應,比如,其中一方可以一次發送多個字節的數據,而另一方可以一個字節一個字節地接收,也可以一個字節一個字節地方送,而多個字節多個字節地接收。因為TCP協議會將數據分成多個塊進行發送,而後在另一端會從多個塊進行接收,再組合在一起,它並不僅能確定read()和write()方法中所發送信息的界限。

2、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://blog.csdn.net/ns_code/article/details/14642873

【Java TCP/IP Socket】TCP Socket通信中由read返回值造成的的死鎖問題(含代碼)