【问题标题】:Why is Socket.Receive timing out on a half-closed connection when timeout is set to infinite?当超时设置为无限时,为什么 Socket.Receive 在半关闭连接上超时?
【发布时间】:2012-11-20 04:33:54
【问题描述】:

下面是一个演示该问题的 C# 程序。

服务器开始监听一个套接字。客户端连接到服务器,发送消息,使用 Shutdown(SocketShutdown.Send) 关闭其发送的一半连接,让服务器知道消息的结尾在哪里,并等待服务器的响应。服务器读取消息,进行一些冗长的计算(此处模拟睡眠调用),向客户端发送消息,然后关闭连接。

在 Windows 上,客户端的 Receive 调用总是在 2 分钟后失败,并显示“连接尝试失败,因为连接方在一段时间后没有正确响应,或者建立连接失败,因为连接的主机没有响应”,即使超时设置为无限。

如果我在 Linux 中使用 Mono 运行程序,即使我将“长时间操作”设置为 10 分钟也不会发生超时,但无论我使用 Mono 还是 .NET 运行它都会在 Windows 中发生。如果我将超时设置为 1 秒,它会在 1 秒后超时。换句话说,它在我设置的超时时间或 2 分钟内超时,以较小者为准。

一个类似的示例程序,其中服务器向客户端发送消息,没有从客户端到服务器的消息,也没有半关闭,按预期工作,没有超时。

我可以通过修改我的协议来解决这个问题,以使用其他方法在消息完成时向服务器指示(可能在消息前面加上消息的长度)。但我想知道这里发生了什么。为什么当超时设置为无限时,Socket.Receive在半关闭连接上超时?

据我了解,仅发送半关闭的连接应该能够无限期地继续接收数据。 Windows 的这样一个基本部分似乎不太可能存在错误。我做错了吗?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.Net;
using System.Threading.Tasks;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // Start server thread
            Thread serverThread = new Thread(ServerStart);
            serverThread.IsBackground = true;
            serverThread.Start();

            // Give the server some time to start listening
            Thread.Sleep(2000);

            ClientStart();
        }

        static int PortNumber = 8181;

        static void ServerStart()
        {
            TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any, PortNumber));
            listener.Start();
            while (true)
            {
                TcpClient client = listener.AcceptTcpClient();
                Task connectionHandlerTask = new Task(ConnectionEntryPoint, client);
                connectionHandlerTask.Start();
            }
            listener.Stop();
        }

        static void ConnectionEntryPoint(object clientObj)
        {
            using (TcpClient client = (TcpClient)clientObj)
            using (NetworkStream stream = client.GetStream())
            {
                // Read from client until client closes its send half.
                byte[] requestBytes = new byte[65536];
                int bufferPos = 0;
                int lastReadSize = -1;
                while (lastReadSize != 0)
                {
                    lastReadSize = stream.Read(requestBytes, bufferPos, 65536 - bufferPos);
                    bufferPos += lastReadSize; 
                }
                client.Client.Shutdown(SocketShutdown.Receive);
                string message = Encoding.UTF8.GetString(requestBytes, 0, bufferPos);

                // Sleep for 2 minutes, 30 seconds to simulate a long-running calculation, then echo the client's message back
                byte[] responseBytes = Encoding.UTF8.GetBytes(message);
                Console.WriteLine("Waiting 2 minutes 30 seconds.");
                Thread.Sleep(150000);

                try
                {
                    stream.Write(responseBytes, 0, responseBytes.Length);
                }
                catch (SocketException ex)
                {
                    Console.WriteLine("Socket exception in server: {0}", ex.Message);
                }
            }
        }

        static void ClientStart()
        {
            using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                // Set receive timeout to infinite.
                socket.ReceiveTimeout = -1;

                // Connect to server
                socket.Connect(IPAddress.Loopback, PortNumber);

                // Send a message to the server, then close the send half of the client's connection
                // to let the server know it has the entire message.
                string requestMessage = "Hello";
                byte[] requestBytes = Encoding.UTF8.GetBytes(requestMessage);
                socket.Send(requestBytes);
                socket.Shutdown(SocketShutdown.Send);

                // Read the server's response. The response is done when the server closes the connection.
                byte[] responseBytes = new byte[65536];
                int bufferPos = 0;
                int lastReadSize = -1;

                Stopwatch timer = Stopwatch.StartNew();
                try
                {
                    while (lastReadSize != 0)
                    {
                        lastReadSize = socket.Receive(responseBytes, bufferPos, 65536 - bufferPos, SocketFlags.None);
                        bufferPos += lastReadSize;
                    }

                    string responseMessage = Encoding.UTF8.GetString(responseBytes, 0, bufferPos);
                    Console.WriteLine(responseMessage);
                }
                catch (SocketException ex)
                {
                    // Timeout always occurs after 2 minutes. Why?
                    timer.Stop();
                    Console.WriteLine("Socket exception in client after {0}: {1}", timer.Elapsed, ex.Message);
                }
            }
        }
    }
}

以下程序使用 4 字节消息长度作为消息前缀,而不是使用 socket.Shutdown(SocketShutdown.Send) 来表示消息结束。此程序中不会发生超时。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Threading;

namespace WithoutShutdown
{
    class Program
    {
        static void Main(string[] args)
        {
            // Start server thread
            Thread serverThread = new Thread(ServerStart);
            serverThread.IsBackground = true;
            serverThread.Start();

            // Give the server some time to start listening
            Thread.Sleep(2000);

            ClientStart();
        }

        static int PortNumber = 8181;

        static void ServerStart()
        {
            TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any, PortNumber));
            listener.Start();
            while (true)
            {
                TcpClient client = listener.AcceptTcpClient();
                Task connectionHandlerTask = new Task(ConnectionEntryPoint, client);
                connectionHandlerTask.Start();
            }
            listener.Stop();
        }

        static void SendMessage(Socket socket, byte[] message)
        {
            // Send a 4-byte message length followed by the message itself
            int messageLength = message.Length;
            byte[] messageLengthBytes = BitConverter.GetBytes(messageLength);
            socket.Send(messageLengthBytes);
            socket.Send(message);
        }

        static byte[] ReceiveMessage(Socket socket)
        {
            // Read 4-byte message length from the client
            byte[] messageLengthBytes = new byte[4];
            int bufferPos = 0;
            int lastReadSize = -1;
            while (bufferPos < 4)
            {
                lastReadSize = socket.Receive(messageLengthBytes, bufferPos, 4 - bufferPos, SocketFlags.None);
                bufferPos += lastReadSize;
            }
            int messageLength = BitConverter.ToInt32(messageLengthBytes, 0);

            // Read the message
            byte[] messageBytes = new byte[messageLength];
            bufferPos = 0;
            lastReadSize = -1;
            while (bufferPos < messageLength)
            {
                lastReadSize = socket.Receive(messageBytes, bufferPos, messageLength - bufferPos, SocketFlags.None);
                bufferPos += lastReadSize;
            }

            return messageBytes;
        }

        static void ConnectionEntryPoint(object clientObj)
        {
            using (TcpClient client = (TcpClient)clientObj)
            {
                byte[] requestBytes = ReceiveMessage(client.Client);
                string message = Encoding.UTF8.GetString(requestBytes);

                // Sleep for 2 minutes, 30 seconds to simulate a long-running calculation, then echo the client's message back
                byte[] responseBytes = Encoding.UTF8.GetBytes(message);
                Console.WriteLine("Waiting 2 minutes 30 seconds.");
                Thread.Sleep(150000);

                try
                {
                    SendMessage(client.Client, responseBytes);
                }
                catch (SocketException ex)
                {
                    Console.WriteLine("Socket exception in server: {0}", ex.Message);
                }
            }
        }

        static void ClientStart()
        {
            using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                // Set receive timeout to infinite.
                socket.ReceiveTimeout = -1;

                // Connect to server
                socket.Connect(IPAddress.Loopback, PortNumber);

                // Send a message to the server
                string requestMessage = "Hello";
                byte[] requestBytes = Encoding.UTF8.GetBytes(requestMessage);
                SendMessage(socket, requestBytes);

                // Read the server's response.
                Stopwatch timer = Stopwatch.StartNew();
                try
                {
                    byte[] responseBytes = ReceiveMessage(socket);
                    string responseMessage = Encoding.UTF8.GetString(responseBytes);
                    Console.WriteLine(responseMessage);
                }
                catch (SocketException ex)
                {
                    // Timeout does not occur in this program because it does not call socket.Shutdown(SocketShutdown.Send)
                    timer.Stop();
                    Console.WriteLine("Socket exception in client after {0}: {1}", timer.Elapsed, ex.Message);
                }
            }
        }
    }
}

【问题讨论】:

  • 赏金有什么用?你回答了你自己的问题不是吗?
  • 是的,在发布赏金几天后并没有得到任何真正的答案。

标签: c# sockets winsock


【解决方案1】:

此行为是设计使然。当客户端在连接上关闭了它的一半并且服务器确认关闭时,客户端处于 FIN_WAIT_2 状态,等待服务器关闭连接。 http://support.microsoft.com/kb/923200 声明 FIN_WAIT_2 超时 2 分钟。如果连接处于 FIN_WAIT_2 状态时,2 分钟内没有收到数据,客户端会强制关闭连接(带有 RST)。

默认情况下,在 Windows Server 2003 中,TCP 连接必须在 TCP 连接状态设置为 FIN_WAIT_2 两分钟后关闭。

This old Apache article 提出了超时的原因:恶意或行为不端的应用程序可以通过永不关闭连接的一端无限期地将连接的另一端保持在 FIN_WAIT_2 中,从而占用操作系统资源。

Linux apparently has a timeout as well你可以检查值

$ cat /proc/sys/net/ipv4/tcp_fin_timeout

我不确定为什么我在 Linux 上没有发生超时。可能是因为它是一个环回连接,因此不需要考虑 DoS 攻击,或者环回连接使用了不使用 tcp_fin_timeout 设置的不同代码?

底线:操作系统有充分的理由使连接超时。避免将 Shutdown 用作应用层信号机制,而应使用实际的应用层方法。

【讨论】:

    【解决方案2】:

    Socket.Receive 似乎对ReceiveTimeout 有两分钟的上限。据说这是在注册表中指定的内容,尽管我找不到任何具体证据证明此内容的真实性或要修改的密钥。这或许可以解释 Linux 与 Windows 上的不同行为。

    我为此研究了各种解决方案,但最简单的(也是唯一有效的)可能是让服务器端每隔几秒发送一次心跳。本质上,这只是确保您永远不会遇到两分钟超时。

    string boundary = string.Format("--{0}--", Guid.NewGuid());
    byte[] boundaryBytes = Encoding.ASCII.GetBytes(boundary);
    
    //Every 15 seconds write a byte to the stream.
    for (int i = 0; i < 10; i++)
    {
        stream.WriteByte(0);
        Thread.Sleep(15000);
    }
    
    //Indicate where the end of the heartbeat bytes is.
    stream.Write(boundaryBytes, 0, boundaryBytes.Length);
    
    //Same code as before.
    try
    {
        stream.Write(responseBytes, 0, responseBytes.Length);
    }
    catch (SocketException ex)
    {
        Console.WriteLine("Socket exception in server: {0}", ex.Message);
    }
    

    我在这里所做的是模拟一个长时间运行的任务(它总共会休眠 2.5 分钟),但每 15 秒它会向流中写入一个字节以防止超时。

    这样做的问题是,您在响应开始时会遇到一堆不需要的垃圾。这就是boundaryBytes 的用武之地:有了这些,您可以清楚地将不需要的位与实际结果区分开来。重要的是客户必须预先知道边界是什么。

    编辑:

    我从您下面的评论中看到,删除 socket.Shutdown(SocketShutdown.Send) 似乎可以解决问题。我自己也想过这个问题,但并没有真正研究它。

    我不明白为什么调用这个方法会有它的效果。进行一些反编译,Shutdown 方法基本上通过 pinvoke 调用底层 WinSock 库(ws2_32.dll)中的shutdown 方法,进行一些错误处理,然后将套接字设置为断开连接。在没有任何其他信息的情况下,这表明 2 分钟问题是在 WinSock 调用中产生的。

    我尝试在事件查看器中通过enabling WinSock logging 对此进行诊断,但似乎没有任何明显的迹象表明为什么会发生这种情况。

    在 WinSock 级别进行更多研究发现了以下问题:

    Winsock recv not working after shutdown

    Multipe Send()'s and Recv()'s using Winsock2

    Why HTTP server close the connection when client close only the send half of the connection?

    Why does .Net Socket.Disconnect take two minutes?

    常见的主题似乎是socket.Shutdown(SocketShutdown.Send) 不是一个好主意,如果您打算之后使用套接字进行接收。此方法将socket.Connected 属性设置为false 的事实可能很能说明问题。

    上面列表中的最后一个链接在 2 分钟的情况下似乎相当接近标记,尽管 OP 指的是注册表设置但没有说明它们是什么。

    【讨论】:

    • 意外超时似乎仅在使用 socket.Shutdown(SocketShutdown.Send) 发出消息结束信号时发生。我添加了一个示例,该示例使用 4 字节消息长度作为消息前缀,并且不使用 socket.Shutdown(SocketShutdown.Send)。该示例在 2 分钟后不会超时。
    猜你喜欢
    • 2017-10-30
    • 2012-09-06
    • 1970-01-01
    • 2012-04-16
    • 2012-06-14
    • 2013-11-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多