深入剖析Socket---TCP通信底层队列填满的死锁问题

转载 2013年12月03日 16:21:07
基础准备

    首先需要明白数据传输的底层实现机制,在http://blog.csdn.net/ns_code/article/details/15813809这篇博客中有详细的介绍,在上面的博客中,我们提到了SendQ和RecvQ缓冲队列,这两个缓冲区的容量在具体实现时会受一定的限制,虽然它们使用的实际内存大小会动态地增长和收缩,但还是需要一个硬性的限制,以防止行为异常的程序所控制的单一TCP连接将系统的内存全部消耗。正式由于缓冲区的容量有限,它们可能会被填满,事实也正是如此,如果与TCP的流量控制机制结合使用,则可能导致一种形式的死锁。

      一旦RecvQ已满,TCP流控制机制就会产生作用(使用流控制机制的目的是为了保证发送者不会传输太多数据,从而超出了接收系统的处理能力),它将阻止传输发送端主机的SendQ中的任何数据,直到接收者调用输入流的read()方法将RecvQ中的数据移除一部分到Delivered中,从而腾出了空间。发送端可以持续地写出数据,直到SendQ队列被填满,如果SendQ队列已满时调用输出流的write()方法,则会阻塞等待,直到有一些字节被传输到RecvQ队列中,如果此时RecvQ队列也被填满了,所有的操作都将停止,直到接收端调用了输入流的read()方法将一些字节传输到了Delivered队列中。

   引出问题

       我们假设SendQ队列和RecvQ队列的大小分别为SQS和RQS。将一个大小为n的字节数组传递给发送端write()方法调用,其中n > SQS,直到有至少n-SQS字节的数据传递到接收端主机的RecvQ队列后,该方法才返回。如果n的大小超过了SQS+RQS,write()方法将在接收端从输入流读取了至少n-(SQS+RQS)字节后才会返回。如果接收端没有调用read()方法,大数据量的发送是无法成功的。特别是连接的两端同时分别调用它的输出流的write()方法,而他们的缓冲区大小又大于SQS+RQS时,将会发生死锁:两个write操作都不能完成,两个程序都将永远保持阻塞状态。

     下面考虑一个具体的例子,即主机A上的程序和主机B上的程序之间的TCP连接。假设A和B上的SQS和RQS都是500字节,下图展示了两个程序试图同时发送1500字节时的情况。主机A上的程序中的前500字节已经传输到另一端,另外500字节已经复制到了主机A的SendQ队列中,余下的500字节则无法发送,write()方法将无法返回,直到主机B上程序的RecvQ队列有空间空出来,然而不幸的是B上的程序也遇到了同样的情况,而二者都没有及时调用read()方法从自己的RecvQ队列中读取数据到Delivered队列中。因此,两个程序的write()方法调用都永远无法返回,产生死锁。因此,在写程序时,要仔细设计协议,以避免在两个方向上传输大量数据时产生死锁。

   示例分析

     回顾前面几篇博客中的TCP通信的示例代码,基本都是只调用一次write()方法将所有的数据写出,而且我们测试的数据量也不大。考虑一个压缩字节的Demo,客户端从文件中读取字节,发送到服务端,服务端将受到的文件压缩后反馈给客户端。

     这里先给出代码,客户端代码如下:

  1. import java.io.FileInputStream;  
  2. import java.io.FileOutputStream;  
  3. import java.io.IOException;  
  4. import java.io.InputStream;  
  5. import java.io.OutputStream;  
  6. import java.net.Socket;  
  7.   
  8. public class CompressClientNoDeadlock {  
  9.   
  10.   public static final int BUFSIZE = 256;  // Size of read buffer   
  11.   
  12.   public static void main(String[] args) throws IOException {  
  13.   
  14.     if (args.length != 3)  // Test for correct #  of args   
  15.       throw new IllegalArgumentException("Parameter(s): <Server> <Port> <File>");  
  16.   
  17.     String server = args[0];               // Server name or IP address   
  18.     int port = Integer.parseInt(args[1]);  // Server port   
  19.     String filename = args[2];             // File to read data from   
  20.   
  21.     // Open input and output file (named input.gz)   
  22.     final FileInputStream fileIn = new FileInputStream(filename);  
  23.     FileOutputStream fileOut = new FileOutputStream(filename + ".gz");  
  24.     
  25.     // Create socket connected to server on specified port   
  26.     final Socket sock = new Socket(server, port);  
  27.   
  28.     // Send uncompressed byte stream to server   
  29.     Thread thread = new Thread() {  
  30.       public void run() {  
  31.         try {  
  32.           SendBytes(sock, fileIn);  
  33.         } catch (Exception ignored) {}  
  34.       }  
  35.     };  
  36.     thread.start();  
  37.   
  38.     // Receive compressed byte stream from server   
  39.     InputStream sockIn = sock.getInputStream();  
  40.     int bytesRead;                      // Number of bytes read   
  41.     byte[] buffer = new byte[BUFSIZE];  // Byte buffer   
  42.     while ((bytesRead = sockIn.read(buffer)) != -1) {  
  43.       fileOut.write(buffer, 0, bytesRead);  
  44.       System.out.print("R");   // Reading progress indicator   
  45.     }  
  46.     System.out.println();      // End progress indicator line   
  47.   
  48.     sock.close();     // Close the socket and its streams   
  49.     fileIn.close();   // Close file streams   
  50.     fileOut.close();  
  51.   }  
  52.   
  53.   public static void SendBytes(Socket sock, InputStream fileIn)  
  54.       throws IOException {  
  55.   
  56.     OutputStream sockOut = sock.getOutputStream();  
  57.     int bytesRead;                      // Number of bytes read   
  58.     byte[] buffer = new byte[BUFSIZE];  // Byte buffer   
  59.     while ((bytesRead = fileIn.read(buffer)) != -1) {  
  60.       sockOut.write(buffer, 0, bytesRead);  
  61.       System.out.print("W");   // Writing progress indicator   
  62.     }  
  63.     sock.shutdownOutput();     // Done sending   
  64.   }  
  65. }  

   

     死锁问题的产生原因在客户端上,因此,服务端的具体代码我们不再给出,服务端采取边读边写的策略。

     下面我们边对上面可能产生的问题进行分析。对该示例而言,当需要传递的文件容量不是很大时,程序运行正常,也能得到预期的结果,但如果尝试运行该客户端并传递给它一个大文件,改文件压缩后仍然很大(在此,大的精确定义取决于程序运行的系统,不过压缩后依然超过2MB的文件应该就可以使改程序产生死锁问题),那么客户端将打印出一堆W后停止,而且不会打印出任何R,程序也不会终止。

     为什么会产生这种情况呢?我们来看程序,客户端很明显是一边读取本地文件中的数据,一边调用输出流的write()方法,将数据送入客户端主机的SendQ队列,直到文件中的数据被读取完,客户端才调用输入流的read()方法,读取服务端发送回来的数据。

     考虑这种情况:客户端和服务端的SendQ队列和RecvQ队列中都有500字节的数据空间,而客户端发送了一个10000字节的文件,同时假设对于这个文件,服务端读取1000字节并返回500字节,即压缩比为2:1,当客户端发送了2000字节后,服务端将最终全部读取这些字节,并发回1000字节,由于客户端此时并没有调用输入流的read()方法从客户端主机的RecvQ队列中移出数据到Delivered,因此,此时客户端的RecvQ队列和服务端的SendQ队列都被填满了,此时客户端还在继续发送数据,又发送了1000字节的数据,并且被服务端全部读取,但此时服务端的write操作尝试都已被阻塞,不能继续发送数据给客户端,当客户端再发送了另外的1000字节数据后,客户端的SendQ队列和服务端的RecvQ队列都将被填满,后续的客户端write操作也将阻塞,从而形成死锁。


   解决方案

     如何解决这个问题呢?造成死锁产生的原因是因为客户端在发送数据的同时,没有及时读取反馈回来的数据,从而使数据都阻塞在了底层的传输队列中。

     方案一是在编写客户端程序时,使客户端一边循环调用输出流的read()方法向服务端发送数据,一边循环调用输入流的read()方法读取从服务端反馈回来的数据,但这也不能完全保证不会产生死锁。

     更好的解决方案是在不同的线程中执行客户端的write循环和read循环一个线程从文件中反复读取未压缩的字节并将其发送给服务器,直到文件的结尾,然后调用该套接字的shutdownOutput()方法。另一个线程从服务端的输入流中不断读取压缩后的字节,并将其写入输出文件,直到到达了输入流的结尾(服务器关闭了套接字)。这样,便可以实现一边发送,一边读取,而且如果一个线程阻塞了,另一个线程仍然可以独立执行。这样我们可以对客户端代码进行简单的修改,将SendByes()方法调用放到一个线程中:

  1. Thread thread = new Thread() {  
  2.   public void run() {  
  3.     try {  
  4.       SendBytes(sock, fileIn);  
  5.     } catch (Exception ignored) {}  
  6.   }  
  7. };  
  8. thread.start();  

     当然,解决这个问题也可以不使用多线程,而是使用NIO机制(Channel和Selector)。

相关文章推荐

[经验总结]调用WinSock的closesocket函数出现死锁的解决办法

这两天调试一个网络应用程序,出现一个很诡异的问题:程序在关闭连接时失去响应。用Process Explorer工具查看该程序的各个线程,发现一个工作线程的调用栈类似这样: stopProc ==> c...

C++ 锁,socket死锁

我们常常对需要多线程共同访问的资源进行加锁,但当在同一个线程中时,一个锁还没离开之前,还可以加一道锁。。。 例:                 CRITICAL_SECTION cs; I...

微信公众号【获取openid和用户信息】

梦想是一场华美的旅途,每个人在找到它之前,都只是孤独的少年。Index.aspx.cs代码: public partial class Index : System.Web.UI.Page {...
  • WuLex
  • WuLex
  • 2016-10-15 01:02
  • 11483

【Java TCP/IP Socket】深入剖析socket——数据传输的底层实现

转载请注明出处:     底层数据结构     如果不理解套接字的具体实现所关联的数据结构和底层协议的工作细节,就很难抓住网络编程的精妙之处,对于TCP套接字来说,更是如此。套接字所关联的底层的数...

netstat Recv-Q和Send-Q

通过netstat -anp可以查看机器的当前连接状态: Active Internet connections (servers and established) Proto Rec...

Linux打印出netstat -anp 里的Send_Q发送堵的TCP连接

在Linux的终端执行 netstat -anp |grep 9300|awk '$3>50 {print $1,$2,$3,$4,$5,$6}' 终端输出显示Send_Q>50的发送消息队列 ...
  • yjh314
  • yjh314
  • 2016-06-22 09:05
  • 1395

C++死锁解决心得

一、 概述 C++多线程开发中,容易出现死锁导致程序挂起的现象。 关于死锁的信息,见百度百科http://baike.baidu.com/view/121723.htm。 解决步骤分为三步:...

socket—TCP通信死锁问题

基础准备     首先需要明白数据传输的底层实现机制,在http://blog.csdn.net/ns_code/article/details/15813809这篇博客中有详细的介绍,在上面的...

Socket 通信中由 read 返回值造成的的死锁问题(socket 阻塞)

Socket 通信中由 read 返回值造成的的死锁问题(socket 阻塞)
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)