6

Below is a C# program demonstrating the problem.

The server starts listening on a socket. The client connects to the server, sends a message, uses Shutdown(SocketShutdown.Send) to close its send half of the connection to let the server know where the end of the message is, and waits for a response from the server. The server reads the message, does some lengthy computation (simulated here with a sleep call), sends a message to the client, and closes the connection.

On Windows, the client's Receive call always fails after exactly 2 minutes with "A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond" even though the timeout is set to infinite.

If I run the program in Linux with Mono, the timeout does not occur even if I set the "lengthy operation" to 10 minutes, but it happens in Windows whether I run it with Mono or .NET. If I set the timeout to 1 second, it times out after 1 second. In other words, it times out in the timeout I set or 2 minutes, whichever is less.

A similar sample program in which the server sends a message to the client, with no message from client to server and no half-close, works as expected with no timeout.

I can get around this by modifying my protocol to use some other method of indicating to the server when a message is complete (perhaps prefixing the message with the length of the message). But I want to know what's going on here. Why does Socket.Receive time out on a half-closed connection when the timeout is set to infinite?

From what I understand, a connection with only its send half closed should be able to continue receiving data indefinitely. It seems unlikely that there would be a bug in such a fundamental part of Windows. Am I doing something wrong?

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

The following program prefixes messages with a 4-byte message length rather than using socket.Shutdown(SocketShutdown.Send) to signal end of message. The timeout does not occur in this program.

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

2 回答 2

5

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

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

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

Linux显然也有超时你可以用

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

我不确定为什么我在 Linux 上没有发生超时。也许是因为它是一个环回连接,因此 DoS 攻击不是问题,或者环回连接使用不使用 tcp_fin_timeout 设置的不同代码?

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

于 2012-11-29T04:38:59.247 回答
0

Socket.ReceiveReceiveTimeout似乎有两分钟的上限。据说这是在注册表中指定的内容,尽管我找不到任何具体证据证明此内容的真实性或要修改的密钥。这可能解释了 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方法基本上shutdown通过 pinvoke 调用底层 WinSock 库(ws2_32.dll)中的方法,进行一些错误处理,然后将套接字设置为断开连接。在没有任何其他信息的情况下,这表明 2 分钟问题是在该 WinSock 调用中创建的。

我试图通过在事件查看器中启用 WinSock 日志记录来诊断这一点,但似乎没有任何明显的迹象表明为什么会发生这种情况。

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

Winsock recv 关机后不工作

使用 Winsock2 将 Send() 和 Recv() 相乘

当客户端仅关闭发送的一半连接时,为什么 HTTP 服务器会关闭连接?

为什么 .Net Socket.Disconnect 需要两分钟?

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

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

于 2012-11-25T10:09:40.707 回答