C#检测TCP断开连接

26
我有两个简单的应用程序:
  • 一个服务器应用程序,在特定的TCP端口上等待客户端连接。然后听取他说的话,发送一些反馈并断开该客户端的连接。

  • 一个表单应用程序,连接到服务器应用程序,然后说些什么,等待反馈并从服务器断开连接,然后在表单中显示反馈。

虽然服务器应用程序似乎表现正确(我已经使用Telnet进行了测试,我看到反馈并且我看到断开连接直接发生在反馈之后),但是表单应用程序似乎没有注意到来自服务器的断开连接。(TcpClient.Connected似乎保持为true,即使服务器已经断开连接)
我的问题是:为什么TcpClient.Connected保持为true,如何知道服务器是否已经断开连接?
以下是我的完整代码:
表单应用程序:
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace Sender
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void sendButton_Click(object sender, EventArgs e)
        {
            TcpClient tcpClient = new TcpClient();
            tcpClient.Connect(IPAddress.Parse("127.0.0.1"), 81);
            responseLabel.Text = "waiting for response...";
            responseLabel.Invalidate();

            // write request
            NetworkStream networkStream = tcpClient.GetStream();
            byte[] buffer = (new ASCIIEncoding()).GetBytes("Hello World! ");
            networkStream.Write(buffer, 0, buffer.Length);
            networkStream.Flush();

            // read response
            Thread readThread = new Thread(new ParameterizedThreadStart(ReadResponse));
            readThread.Start(tcpClient);
        }

        void ReadResponse(object arg)
        {
            TcpClient tcpClient = (TcpClient)arg;
            StringBuilder stringBuilder = new StringBuilder();
            NetworkStream networkStream = tcpClient.GetStream();
            bool timeout = false;
            DateTime lastActivity = DateTime.Now;
            while (tcpClient.Connected && !timeout)
            {
                if (networkStream.DataAvailable)
                {
                    lastActivity = DateTime.Now;
                    while (networkStream.DataAvailable)
                    {
                        byte[] incomingBuffer = new byte[1024];
                        networkStream.Read(incomingBuffer, 0, 1024);
                        char[] receivedChars = new char[1024];
                        (new ASCIIEncoding()).GetDecoder().GetChars(incomingBuffer, 0, 1024, receivedChars, 0);
                        stringBuilder.Append(receivedChars);
                    }
                }
                else
                {
                    if (DateTime.Now > lastActivity.AddSeconds(60))
                        timeout = true;
                }
                System.Threading.Thread.Sleep(50);
            }
            Invoke((MethodInvoker)delegate
            {
                responseLabel.Text = "Response from Listener:\n" + stringBuilder.ToString();
                responseLabel.Invalidate();
            });

            if (timeout)
            {
                Console.Write("A timeout occured\n");
                networkStream.Close();
                tcpClient.Close();
            }
        }

    }
}

服务器应用程序:

using System.Net;
using System.Net.Sockets;
using System.Text;
using System;
using System.Threading;

namespace Listener
{
    class Program
    {
        static void Main(string[] args)
        {
            var tcpListener = new TcpListener(IPAddress.Any, 81);
            tcpListener.Start();
            Thread clientThread = new Thread(new ParameterizedThreadStart(Listen));
            clientThread.Start(tcpListener);
        }

        static void Listen(object arg)
        {
            TcpListener tcpListener = (TcpListener)arg;
            while (true)
            {
                TcpClient tcpClient = tcpListener.AcceptTcpClient();
                Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClient));
                clientThread.Start(tcpClient);
            }
        }

        static void HandleClient(object arg)
        {
            TcpClient tcpClient = (TcpClient)arg;
            StringBuilder stringBuilder = new StringBuilder();
            ASCIIEncoding encoder = new ASCIIEncoding();
            DateTime lastActivity = DateTime.Now;

            // read request
            NetworkStream networkStream = tcpClient.GetStream();
            int timeout = 5; // gives client some time to send data after connecting
            while (DateTime.Now < lastActivity.AddSeconds(timeout) && stringBuilder.Length==0)
            {
                if (!networkStream.DataAvailable)
                {
                    System.Threading.Thread.Sleep(50);
                }
                else
                {
                    while (networkStream.DataAvailable)
                    {
                        lastActivity = DateTime.Now;
                        byte[] incomingBuffer = new byte[1024];
                        networkStream.Read(incomingBuffer, 0, 1024);
                        char[] receivedChars = new char[1024];
                        encoder.GetDecoder().GetChars(incomingBuffer, 0, 1024, receivedChars, 0);
                        stringBuilder.Append(receivedChars);
                    }
                }
            }
            string request = stringBuilder.ToString();

            // write response
            string response = "The listener just received: " + request;
            byte[] outgoingBuffer = encoder.GetBytes(response);
            networkStream.Write(outgoingBuffer, 0, outgoingBuffer.Length);
            networkStream.Flush();

            networkStream.Close();
            tcpClient.Close();
        }
    }

}

请查看此答案:https://dev59.com/xHM_5IYBdhLWcg3waSb9#19706302 - oo_dev
3个回答

23
TcpClient/NetworkStream在连接关闭时无法得到通知。 唯一可用的选项是在写入流时捕获异常。
几年前,我们改用套接字而不是TcpClient。与TcpClient相比,套接字更易于使用。
有几种方法可供使用。
其中之一是Poll。

http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.poll.aspx

您还可以对Write本身的结果进行检查。它会给出实际写入的字节数。

Connected属性本身只反映了最后一次操作时的状态。其文档说明:“Connected属性的值反映了连接状态的最近操作。如果您需要确定连接的当前状态,请进行非阻塞的零字节发送调用。如果调用成功返回或抛出WAEWOULDBLOCK错误代码(10035),则套接字仍然连接;否则,套接字不再连接。”

http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.connected.aspx


4
我还不是很理解。为什么要轮询套接字(我认为这实际上是尝试使用套接字并检查其是否失败,从而导致开销),而服务器已经告诉客户端它已经断开连接了呢?即使直接用 telnet 也能看到连接已断开并警告我,因此这条消息一定是从服务器发送的,操作系统将该消息传递给应用程序...你能帮我理解吗? - nl-x
3
为了明确一点:所以c#/.NET不会记住连接方是否已经优雅地断开连接吗?即使连接方之前已经优雅地断开连接,它也会轮询连接来知道连接是否已断开? - nl-x
5
接受回答,但我仍然不完全同意。RFC http://tools.ietf.org/html/rfc793#page-38(情况2)指出,用户(应用程序?)将被告知已收到FIN(断开连接通告)。这个基本的数据必须在.NET的某个地方存在... - nl-x
1
@HermitDave 是的,我也看到了。这几乎与您之前建议的相同。尽管根据我的先前评论:我真的不明白为什么.NET不会跟踪优雅的TCP断开连接状态。如果另一方宣布将断开连接,那么至少应该有一个事件来处理这种情况。 - nl-x
16
如果 (tcpClient.Client.Poll(1, SelectMode.SelectRead) && !networkStream.DataAvailable),则表明连接已关闭:当使用SelectRead时,只有在以下情况下Poll()会返回true:(1)我们正在监听,但这不是我们的情况。(2)新数据可用,所以我们在调用Poll()后检查networkStream.DataAvailable,因为它可能在Poll()期间发生变化。(3)连接确实已关闭。 - nl-x
显示剩余10条评论

2
有一种检测TCP连接正确状态的另一种方法,不需要使用Socket。您可以保留您的TcpClient并使用IPGlobalProperties
这个想法来自于这个答案
注意:在.NET 5中存在一个错误,当调用IPGlobalProperties.GetActiveTcpConnections()时会导致内存泄漏。这个错误在.NET Framework中不存在,并且已经在.NET 6中修复了。(https://github.com/dotnet/runtime/issues/64735
public class CTcpipConnection
{
  public enum EnumState
  {
    NotInitialized,
    NotReady,
    Idle,
    Connecting,
    Connected,
    Disconnecting
  }

  public string IpAddress { get; }  // will be set in ctor
  public int    LocalPort { get; }  // will be set in Connect() and Disconnect()
  public int    Port      { get; }  // will be set in ctor

  EnumState GetState ()
  {
    var ipGlobProp   = IPGlobalProperties.GetIPGlobalProperties ();
    var tcpConnInfos = ipGlobProp.GetActiveTcpConnections ();
    TcpConnectionInformation tcpConnInfo = null;
    for (int index = 0; index < i_tcpConnInfos.Length; index++)
    {
      if (i_tcpConnInfos[index].LocalEndPoint.Port  == LocalPort
       && i_tcpConnInfos[index].RemoteEndPoint.Port == Port)
      {
        tcpConnInfo = i_tcpConnInfos[index];
        break;
      }
    }

    if (tcpConnInfo == null)
      return EnumState.Idle;

    var tcpState = tcpConnInfo.State;
    switch (tcpState)
    {
    case TcpState.Listen:
    case TcpState.SynSent:
    case TcpState.SynReceived:
      return EnumState.Connecting;

    case TcpState.Established:
      return EnumState.Connected;

    case TcpState.FinWait1:
    case TcpState.FinWait2:
    case TcpState.CloseWait:
    case TcpState.Closing:
    case TcpState.LastAck:
      return EnumState.Disconnecting;

    default:
      return EnumState.NotReady;
    }
  }

  private void Connect ()
  {
    m_tcpclient.Connect (IpAddress, Port);
    m_netstream = m_tcpclient.GetStream ();
    var ipendpoint = m_tcpclient.Client.LocalEndPoint as IPEndPoint;
    LocalPort = ipendpoint.Port;
  }

  private void Disconnect ()
  {
    if (m_netstream != null)
    {
      m_netstream.Flush ();
      m_netstream.Close (500);
    }

    m_tcpclient.Close ();
    LocalPort   = 0;
    m_tcpclient = new TcpClient ();
  }

}

0

如果你使用无限超时进行阻塞读取,例如:

                NetworkStream.ReadTimeout = -1

在这种情况下,当连接丢失时,Read方法将返回零。

                // Reading everything from network to memory stream

                var s = new MemoryStream();

                var buf = new byte[client.ReceiveBufferSize];
                                        
                do
                {
                    var n = stream.Read(buf, 0, buf.Length);
                    if(n == 0)
                    {
                        return; // connection is lost
                    }
                    s.Write(buf, 0, n);
                }
                while(s.Length < packetsize);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接