大家在编写Socket应用程序时,必须避免设计非常小心以避免出现死锁。例如,在建立连接后,发送端与接收端都尝试发送数据,显然将会导致死锁的发生。在前面中我们介绍了SendQ、RecvQ、Delivered队列,SendQ、RecvQ队列中缓冲区的容量在具体实现时会受到一定的限制。虽然它们使用的实际内存大小会动态的增长和收缩,还是需要一个硬性的限制,以防止行为异常的程序所控制的单独一个TCP连接将系统内存耗尽,如果与TCP的流控制机制结合使用,则可能导致另一种形式的死锁。
在TCP套接字中,一旦接收端的RecvQ队列缓冲区已满,TCP流控制机制就会产生作用,它阻止TCP传输发送端SendQ队列缓冲区的任何数据,直到接收端调用in.read( )方法将RecvQ队列缓冲区中的数据移动到Delivered队列中腾出空间(这里使用流控制机制目的是为了保证发送者不会发送太多数据,而导致超出了接收系统的处理能力)。
发送端可以持续的写出数据,直到SendQ队列缓冲区被填满,然而,如果,在SendQ队列缓冲区已满时调用out.write( )方法,则阻塞等该,直到SendQ队列缓冲区有新的空间为止。换句话说:发送端将数据传输到接收端的RendQ队列中时,如果此时RendQ队列缓冲区已经被填满,那么将会产生阻塞。直到接收端调用in.read( )方法将数据移动到Delivered队列中腾出空间为止。
下面考虑一个场景:
即主机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套接字中,要仔细的设计协议,避免在两端发送大量数据时产生死锁“。
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的程序(服务端)都会阻塞,永远也执行不完! 如下图所示:
方案一:在不同的线程分别执行客户端的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);
}
}
每天进度一点点 —— 作者: 大饼