2012-11-20 46 views
5

下面是一个展示问题的C#程序。为什么Socket.Receive超时就当超时时间设置为无限半关闭的连接?

服务器启动,监听套接字。客户端连接到服务器,发送消息时,使用关机(SocketShutdown.Send)关闭其连接的发送一半让服务器知道该消息的结尾是,并等待来自服务器的响应。服务器读取消息,做一些冗长的计算(在这里模拟一个睡眠呼叫),向客户端发送消息并关闭连接。

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

如果我使用Mono在Linux中运行程序,即使将“冗长操作”设置为10分钟,也不会发生超时,但是它发生在Windows中是否使用Mono或.NET运行。如果我将超时设置为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); 
       } 
      } 
     } 
    } 
} 
+0

什么是赏金?你回答自己的问题不是吗? – jeremy

+0

是的,发布赏金后几天没有得到真正的答案。 –

回答

4

此行为是设计使然。当客户端在连接上关闭一半并且服务器确认关闭时,客户端处于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两分钟后关闭。

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

Linux apparently has a timeout as well您可以检查与

$执行cat/proc/SYS /网/ IPv4中的值/ tcp_fin_timeout

我不知道为什么超时没有在Linux上出现了我。也许是因为它是一个回送连接,因此DoS攻击不是问题,或者回送连接使用不使用tcp_fin_timeout设置的不同代码?

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

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的事实可能是非常明显的。

尽管OP指的是注册表设置,但并未说明它们是什么,但上述列表中的最后一个链接看起来与2分钟情况下的标记相当接近。

+0

意外的超时似乎只发生在使用socket.Shutdown(SocketShutdown.Send)来表示消息结束时。我添加了一个以4字节消息长度为前缀的消息的示例,并且不使用socket.Shutdown(SocketShutdown.Send)。这个例子在2分钟后没有**超时。 –