1. 程式人生 > >【Java TCP/IP Soket】— 剖析TCP中的死鎖

【Java TCP/IP Soket】— 剖析TCP中的死鎖

前言

大家在編寫Socket應用程式時,必須避免設計非常小心以避免出現死鎖。例如,在建立連線後,傳送端與接收端都嘗試傳送資料,顯然將會導致死鎖的發生。在前面中我們介紹了SendQ、RecvQ、Delivered佇列,SendQ、RecvQ佇列中緩衝區的容量在具體實現時會受到一定的限制。雖然它們使用的實際記憶體大小會動態的增長和收縮,還是需要一個硬性的限制,以防止行為異常的程式所控制的單獨一個TCP連線將系統記憶體耗盡,如果與TCP的流控制機制結合使用,則可能導致另一種形式的死鎖。

TCP中死鎖分析

在TCP套接字中,一旦接收端的RecvQ佇列緩衝區已滿,TCP流控制機制就會產生作用,它阻止TCP傳輸傳送端SendQ佇列緩衝區的任何資料,直到接收端呼叫in.read( )方法將RecvQ佇列緩衝區中的資料移動到Delivered佇列中騰出空間(這裡使用流控制機制目的是為了保證傳送者不會發送太多資料,而導致超出了接收系統的處理能力)。

傳送端可以持續的寫出資料,直到SendQ佇列緩衝區被填滿,然而,如果,在SendQ佇列緩衝區已滿時呼叫out.write( )方法,則阻塞等該,直到SendQ佇列緩衝區有新的空間為止。換句話說:傳送端將資料傳輸到接收端的RendQ佇列中時,如果此時RendQ佇列緩衝區已經被填滿,那麼將會產生阻塞。直到接收端呼叫in.read( )方法將資料移動到Delivered佇列中騰出空間為止。

TCP中死鎖場景

下面考慮一個場景:

即主機A與主機B之間的一個連線,假設主機A和主機B上的SendQ、RecvQ佇列緩衝區大小為500個位元組,現在考慮一下 ”兩個主機(A、B)中的程式試圖同時傳送1500個位元組時的情況“。主機A上程式中的前500個位元組已經傳輸到主機A中,另外的500個位元組也已經複製到SendQ佇列中,餘下的500個位元組將無法傳送(因此,在主機A的程式中呼叫out.write( )方法將會阻塞),直到主機B中的RecvQ佇列緩衝區中有空間空出來。然後,主機B上的程式也遇到了同樣的情況。因此,在兩個主機上程式的out.write( )方法的呼叫都將阻塞(永遠都無法完成)。

  

以上的場景告訴我們:”在TCP套接字中,要仔細的設計協議,避免在兩端傳送大量資料時產生死鎖“。

TCP中死鎖例子

1.示例

下面,我們通過一個例子來模擬一下上述場景中的死鎖問題,程式碼如下:

/*
 * 服務端
 */
public class TestServer
{
    public static void main(String[] args) throws IOException
    {
        System.out.println("服務端啟動......");
        ServerSocket server = new ServerSocket(8888);

        Socket client = server.accept();
        OutputStream output = client.getOutputStream();
        InputStream input = client.getInputStream();

        byte[] temp = new byte[10];
        int realLen = 0;
        while ((realLen = input.read(temp)) != -1)
        {
            System.out.println("【服務端】正在傳送資料......");
            output.write(temp, 0, realLen);
        }
        System.out.println("【客戶端】傳送資料完畢!");
        output.flush();

        client.close();
    }
}
/*
 * 客戶端
 */
public class TestClient
{
    public static void main(String[] args) throws UnknownHostException,
            IOException
    {
        System.out.println("客戶端啟動......");
        Socket client = new Socket(InetAddress.getLocalHost(), 8888);

        OutputStream out = client.getOutputStream();
        InputStream in = client.getInputStream();
        FileInputStream fileIn = new FileInputStream(new File("D:\\雜亂\\桌面.jpg"));

        byte[] fileTemp = new byte[1024];
        int realFileLen = 0;
        while ((realFileLen = fileIn.read(fileTemp)) != -1)
        {
            System.out.println("【客戶端】正在傳送資料......");
            out.write(fileTemp, 0, realFileLen);
        }
        System.out.println("【客戶端】傳送資料完畢!");
        out.flush();
        client.shutdownOutput();

        ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
        int realLen = 0;
        byte[] temp = new byte[10];
        while ((realLen = in.read(temp)) != -1)
        {
            byteArray.write(temp, 0, realLen);
        }
        byte[] recvByte = byteArray.toByteArray();

        System.out.println("客戶端接收訊息成功,訊息長度:" + recvByte.length);

    }
}
執行上邊的例子,我們發現:客戶端與服務端程式都會被阻塞(永遠也執行不完)。

2.剖析

下面我們分析一下阻塞原因:

我們把TestClient客戶端類所在的主機為A,把TestServer服務端類所在的主機為B。在主機A與主機B之間建立了一條連線。現在主機A中的程式不斷的向主機B的RecvQ佇列中傳輸資料,而這時,主機B也不斷的將RecvQ佇列中的資料移動到DeliveredQ佇列中,並把DeliveredQ佇列中的資料移動到SendQ佇列中,然後再把SendQ佇列中的資料傳輸到主機A中的RecvQ佇列中(注意:這個時候主機A還在不斷的向主機B傳送資料)—— 這樣就會造成 ” 主機A中的RecvQ佇列被填滿,主機B上的程式呼叫out.write( )方法傳送資料時就會阻塞,然而不幸的是:這個時候主機A依然不斷的向主機B中的RecvQ佇列中傳輸資料,直到RecvQ佇列被填滿,然後主機A上的程式呼叫out.write( )方法傳送資料是就會阻塞“,所以,主機A的程式(客戶端)與主機B的程式(服務端)都會阻塞,永遠也執行不完! 如下圖所示:

     

TCP中死鎖解決辦法

   方案一:在不同的執行緒分別執行客戶端的write( ) 和 read( )

   好處:可以邊讀邊寫,建立的緩衝區比較小,不會造成客戶端中RecvQ佇列被填滿 , 相應的服務端中RecvQ佇列也不會被填滿;

   壞處:客戶端中得使用不同的執行緒,分別進行write() 和 read( )操作;

/*
 * 服務端
 */
public class TestServer
{
    public static void main(String[] args) throws IOException
    {
        System.out.println("服務端啟動......");
        ServerSocket server = new ServerSocket(8888);

        Socket client = server.accept();
        OutputStream output = client.getOutputStream();
        InputStream input = client.getInputStream();

        byte[] temp = new byte[10];
        int realLen = 0;
        while ((realLen = input.read(temp)) != -1)
        {
            System.out.println("【服務端】正在傳送資料......");
            output.write(temp, 0, realLen);
        }
        System.out.println("【客戶端】傳送資料完畢!");
        output.flush();

        client.close();
    }
}

/*
 * 客戶端
 */
public class TestClient
{
    public static void main(String[] args) throws UnknownHostException,
            IOException
    {
        System.out.println("客戶端啟動......");
        final Socket client = new Socket(InetAddress.getLocalHost(), 8888);

        final OutputStream out = client.getOutputStream();
        InputStream in = client.getInputStream();
        final FileInputStream fileIn = new FileInputStream(new File(
                "D:\\雜亂\\桌面.jpg"));

        // 使用一個子執行緒傳送資料
        Thread handlerExecute = new Thread()
        {
            @Override
            public void run()
            {
                try
                {
                    byte[] fileTemp = new byte[1024];
                    int realFileLen = 0;

                    while ((realFileLen = fileIn.read(fileTemp)) != -1)
                    {
                        System.out.println("【客戶端】正在傳送資料......");
                        out.write(fileTemp, 0, realFileLen);
                    }
                    System.out.println("【客戶端】傳送資料完畢!");

                    out.flush();
                    client.shutdownOutput();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        };

        handlerExecute.start();

        // 使用主執行緒接收資料
        ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
        int realLen = 0;
        byte[] temp = new byte[10];
        // 服務端採用邊讀邊寫的方式
        while ((realLen = in.read(temp)) != -1)
        {
            byteArray.write(temp, 0, realLen);
        }
        byte[] recvByte = byteArray.toByteArray();

        System.out.println("客戶端接收訊息成功,訊息長度:" + recvByte.length);

    }
}

    方案二:  服務端不採用 “邊讀邊寫” 的方式,而是採用 “全部讀完在全部發送” 的方式

    好處:這樣,服務端和客戶端的RecvQ佇列就不會出現被填滿的情況;

    壞處:不適用於大檔案的傳輸,這樣建立的緩衝區會比較大,很可能會造成資料的丟失;

/*
 * 服務端
 */
public class TestServer
{
    public static void main(String[] args) throws IOException
    {
        System.out.println("服務端啟動......");
        ServerSocket server = new ServerSocket(8888);

        Socket client = server.accept();
        OutputStream output = client.getOutputStream();
        InputStream input = client.getInputStream();

        ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
        byte[] temp = new byte[10];
        int realLen = 0;
        while ((realLen = input.read(temp)) != -1)
        {
            byteArray.write(temp, 0, realLen);
        }

        byte[] recvByte = byteArray.toByteArray();

        System.out.println("【服務端】正在傳送資料......");
        output.write(recvByte);
        output.flush();
        System.out.println("【客戶端】傳送資料完畢!");

        client.close();
    }
}

/*
 * 客戶端
 */
public class TestClient
{
    public static void main(String[] args) throws UnknownHostException,
            IOException
    {
        System.out.println("客戶端啟動......");
        final Socket client = new Socket(InetAddress.getLocalHost(), 8888);

        final OutputStream out = client.getOutputStream();
        InputStream in = client.getInputStream();
        final FileInputStream fileIn = new FileInputStream(new File(
                "D:\\雜亂\\桌面.jpg"));

        byte[] fileTemp = new byte[1024];
        int realFileLen = 0;

        while ((realFileLen = fileIn.read(fileTemp)) != -1)
        {
            System.out.println("【客戶端】正在傳送資料......");
            out.write(fileTemp, 0, realFileLen);
        }
        System.out.println("【客戶端】傳送資料完畢!");

        out.flush();
        client.shutdownOutput();

        ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
        int realLen = 0;
        byte[] temp = new byte[10];
        while ((realLen = in.read(temp)) != -1)
        {
            byteArray.write(temp, 0, realLen);
        }
        byte[] recvByte = byteArray.toByteArray();

        System.out.println("客戶端接收訊息成功,訊息長度:" + recvByte.length);

    }
}

每天進度一點點  —— 作者: 大餅