【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);
}
}
每天進度一點點 —— 作者: 大餅