【Java TCP/IP Socket】深入剖析socket——TCP通信中由于底层队列填满而造成的死锁问题(含代码)
基础准备
首先需要明白数据传输的底层实现机制,在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,客户端从文件中读取字节,发送到服务端,服务端将受到的文件压缩后反馈给客户端。
这里先给出代码,客户端代码如下:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket; public class CompressClientNoDeadlock { public static final int BUFSIZE = 256; // Size of read buffer public static void main(String[] args) throws IOException { if (args.length != 3) // Test for correct # of args
throw new IllegalArgumentException("Parameter(s): <Server> <Port> <File>"); String server = args[0]; // Server name or IP address
int port = Integer.parseInt(args[1]); // Server port
String filename = args[2]; // File to read data from // Open input and output file (named input.gz)
final FileInputStream fileIn = new FileInputStream(filename);
FileOutputStream fileOut = new FileOutputStream(filename + ".gz"); // Create socket connected to server on specified port
final Socket sock = new Socket(server, port); // Send uncompressed byte stream to server
Thread thread = new Thread() {
public void run() {
try {
SendBytes(sock, fileIn);
} catch (Exception ignored) {}
}
};
thread.start(); // Receive compressed byte stream from server
InputStream sockIn = sock.getInputStream();
int bytesRead; // Number of bytes read
byte[] buffer = new byte[BUFSIZE]; // Byte buffer
while ((bytesRead = sockIn.read(buffer)) != -1) {
fileOut.write(buffer, 0, bytesRead);
System.out.print("R"); // Reading progress indicator
}
System.out.println(); // End progress indicator line sock.close(); // Close the socket and its streams
fileIn.close(); // Close file streams
fileOut.close();
} public static void SendBytes(Socket sock, InputStream fileIn)
throws IOException { OutputStream sockOut = sock.getOutputStream();
int bytesRead; // Number of bytes read
byte[] buffer = new byte[BUFSIZE]; // Byte buffer
while ((bytesRead = fileIn.read(buffer)) != -1) {
sockOut.write(buffer, 0, bytesRead);
System.out.print("W"); // Writing progress indicator
}
sock.shutdownOutput(); // Done sending
}
}
死锁问题的产生原因在客户端上,因此,服务端的具体代码我们不再给出,服务端采取边读边写的策略。
下面我们边对上面可能产生的问题进行分析。对该示例而言,当需要传递的文件容量不是很大时,程序运行正常,也能得到预期的结果,但如果尝试运行该客户端并传递给它一个大文件,改文件压缩后仍然很大(在此,大的精确定义取决于程序运行的系统,不过压缩后依然超过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()方法调用放到一个线程中:
Thread thread = new Thread() {
public void run() {
try {
SendBytes(sock, fileIn);
} catch (Exception ignored) {}
}
};
thread.start();
当然,解决这个问题也可以不使用多线程,而是使用NIO机制(Channel和Selector)。
转自:http://blog.csdn.net/ns_code/article/details/15939993
【Java TCP/IP Socket】深入剖析socket——TCP通信中由于底层队列填满而造成的死锁问题(含代码)的更多相关文章
- 深入浅出--iOS的TCP/IP协议族剖析&&Socket
深入浅出--iOS的TCP/IP协议族剖析&&Socket 简介 该篇文章主要回顾--TCP/IP协议族中的TCP/UDP.HTTP:还有Socket.(--该文很干,酝酿了许久! ...
- iOS的TCP/IP协议族剖析&&Socket
原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0 简介 该篇文章主要回顾--TCP/IP协议族中的TCP/UDP.HTTP:还有S ...
- 深入浅出-TCP/IP协议族剖析&&Socket
Posted by 微博@Yangsc_o 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0 #简介 该篇文章主要回顾–TCP/I ...
- TCP/IP、Http、Socket、XMPP-从入门到深入
TCP/IP.Http.Socket.XMPP-从入门到深入 终极iOS程序猿 2016-12-29 18:27 为了便于大家理解和记忆,我们先对这几个概念进行的介绍,然后分析他们的不同,再进行详细的 ...
- TCP/IP、Http、Socket的区别
1.标准网络层次 网络由下往上分为:物理层.数据链路层.网络层.传输层.会话层.表示层和应用层. 下面的图表试图显示不同的TCP/IP和其他的协议在最初OSI模型中的位置: 7 应用层 例如HTTP. ...
- 【PHPsocket编程专题(理论篇)】初步理解TCP/IP、Http、Socket.md
前言 我们平时说的最多的socket是什么呢,实际上socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API).那TCP/IP又是什么呢?TCP/IP是ISO/OS ...
- TCP/IP、Http、Socket的区别与关系
--TCP/IP.Http.Socket的区别与关系 --------------------------------------2014/05/14 网络由下往上分为 物理层.数据链路层.网络层.传 ...
- TCP/IP Http 和Https socket之间的区别
TCP/IP Http 和Https socket之间的区别 TCP/IP是个协议组,它分为网络层,传输层和应用层, 在网络层有IP协议.ICMP协议.ARP协议.RARP协议和BOOTP协议. ...
- Python Web学习笔记之TCP/IP、Http、Socket的区别
经常在笔试.面试或者工作的时候听到这些协议,虽然以前没怎么涉及过,但至少知道这些是和网络编程密不可分的知识,作为一个客户端开发程序员,如果可以懂得网络编程的话,他的作用和能力肯定会提升一个档次.原因很 ...
随机推荐
- Mac OSX用 dd 命令,浇灌ISO镜像到USB驱动器
Mac OSX用 dd 命令,浇灌ISO镜像到USB驱动器 字数244 阅读197 评论0 喜欢0 把ISO镜像转换为一个可启动的USB设备.一种可行的方法是通过OS X的Terminal “浇灌”到 ...
- python基础——9(迭代器、生成器)
一.迭代器 1.概念 器:包含了多个值的容器 迭代:循环反馈(一次从容器中取出一个值) 迭代器:从装有多个值的容器中一次取出一个值给外界 s = 'abcdef' ls = [1,2,3,4,5] 遍 ...
- linux下防火墙iptables原理及使用
iptables简介 netfilter/iptables(简称为iptables)组成Linux平台下的包过滤防火墙,与大多数的Linux软件一样,这个包过滤防火墙是免费的,它可以代替昂贵的商业防火 ...
- bluej
他山之石,可以攻玉!吾辈之道,披荆斩棘! 个人源码地址: https://gitee.com/blue_phantom
- 自己项目使用了ARC,项目中使用第三方库继续引用ARC
Objective-C引入ARC(Automatic Reference Counting)后,我们经常会面对这样一种困境:自己的项目使用了ARC,却发现要使用的第三方类库是non-ARC的:又或者自 ...
- 三、harbor部署之SSL
1 签名证书与自签名证书 签名证书:由权威颁发机构颁发给服务器或者个人用于证明自己身份的东西. 自签名证书:由服务器自己颁发给自己,用于证明自己身份的东西,非权威颁发机构发布. 2 openssl简介 ...
- Dell Omsa在Linux服务器上安装部署
前言 本页详述了在一台Linux(RHEL6.4 x86_64)服务器上部署安装OMSA的通用做法,包括OMSA软件的获取方法和安装步骤. 演示环境: PowerEdge R620, RHEL 6.4 ...
- Dell Idrac Normal Settings
racadm安装请查看:http://www.cnblogs.com/zyd112/p/7611022.html racadm语法(远程执行命令):racadm -r <racIpAddr> ...
- 【LeetCode】Intersection of Two Linked Lists(相交链表)
这道题是LeetCode里的第160道题. 题目讲的: 编写一个程序,找到两个单链表相交的起始节点. 如下面的两个链表: 在节点 c1 开始相交. 示例 1: 输入:intersectVal = 8, ...
- Leetcode 357.计算各个位数不同的数字个数
计算各个位数不同的数字个数 给定一个非负整数 n,计算各位数字都不同的数字 x 的个数,其中 0 ≤ x < 10n . 示例: 输入: 2 输出: 91 解释: 答案应为除去 11,22,33 ...