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

时间:2012-11-20 04:33:55

标签: 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);
                }
            }
        }
    }
}

以下程序使用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);
                }
            }
        }
    }
}

2 个答案:

答案 0 :(得分:4)

此行为是设计使然。当客户端在连接上关闭其一半并且服务器确认关闭时,客户端处于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作为应用层信令机制,而是使用实际的应用层方法。

答案 1 :(得分:0)

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方法,进行一些错误处理,然后将套接字设置为断开连接。在没有任何其他信息的情况下,这表明在该WinSock调用中创建了2分钟的问题。

我尝试在事件查看器中通过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指的是注册表设置,但没有说明它们是什么。