当超时设置为无限时,为什么Socket.Receive会在半封闭连接上超时?

Gre*_*jda 6 c# sockets winsock

下面是一个C#程序,演示了这个问题.

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

在Windows上,客户端的接收呼叫总是在2分钟后失败,因为"连接尝试失败,因为连接方在一段时间后没有正确响应,或者建立的连接失败,因为连接的主机未能响应",即使超时是设置为无限.

如果我使用Mono在Linux中运行该程序,即使我将"冗长操作"设置为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);
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

以下程序使用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);
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Gre*_*jda 5

此行为是设计使然。当客户端关闭了连接的一半并且服务器确认关闭时,客户端处于FIN_WAIT_2状态,等待服务器关闭连接。http://support.microsoft.com/kb/923200指出FIN_WAIT_2超时为2分钟。如果连接处于FIN_WAIT_2状态时在2分钟内未收到任何数据,则客户端将强制关闭连接(使用RST)。

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

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

Linux显然也有超时,您可以使用

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

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

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