【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( )方法的调用都将阻塞(永远都无法完成)。

   【Java TCP/IP Soket】— 剖析TCP中的死锁_第1张图片

以上的场景告诉我们:”在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的程序(服务端)都会阻塞,永远也执行不完! 如下图所示:

      【Java TCP/IP Soket】— 剖析TCP中的死锁_第2张图片

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);

    }
}

每天进度一点点  —— 作者: 大饼

你可能感兴趣的:(Java,Socket)