How to check if TcpClient Connection is closed?

Viewed 69244

I'm playing around with the TcpClient and I'm trying to figure out how to make the Connected property say false when a connection is dropped.

I tried doing

NetworkStream ns = client.GetStream();
ns.Write(new byte[1], 0, 0);

But it still will not show me if the TcpClient is disconnected. How would you go about this using a TcpClient?

10 Answers

As of 2019, in a cross-platform and async environment, I use the code below to continuosly check that the TCP channel is open. This check fires e.g. if the ethernet cable is pulled on my Windows machine, or if the Wifi is disabled on my Android device.

private async Task TestConnectionLoop()
{
    byte[] buffer = new byte[1];
    ArraySegment<byte> arraySegment = new ArraySegment<byte>(buffer, 0, 0);
    SocketFlags flags = SocketFlags.None;

    while (!_cancellationSource.Token.IsCancellationRequested)
    {
        try
        {
            await _soc.SendAsync(arraySegment, flags);
            await Task.Delay(500);
        }
        catch (Exception e)
        {
            _cancellationSource.Cancel();

            // Others can listen to the Cancellation Token or you 
            // can do other actions here
        }
    }
}

Please note that I have found GSF.Communication wrapper for System.Net.Sockets.TcpClient to be helpful because it has a CurrentState property that indicates whether the socket is open/connected or closed/disconnected. You can find details on the NuGet package here:

https://github.com/GridProtectionAlliance/gsf

Here is how you could setup a simple TCP socket and test whether it is connected:

    GSF.Communication.TcpClient tcpClient;

    void TestTcpConnectivity() 
    {
        tcpClient = new GSF.Communication.TcpClient();
        string myTCPServer = "localhost";
        string myTCPport = "8080";
        tcpClient.MaxConnectionAttempts = 5;
        tcpClient.ConnectionAttempt += s_client_ConnectionAttempt;
        tcpClient.ReceiveDataComplete += s_client_ReceiveDataComplete;
        tcpClient.ConnectionException += s_client_ConnectionException;
        tcpClient.ConnectionEstablished += s_client_ConnectionEstablished;
        tcpClient.ConnectionTerminated += s_client_ConnectionTerminated;
        
        tcpClient.ConnectionString = "Server=" + myTCPServer + ":" + myTCPport;
        tcpClient.Initialize();
        tcpClient.Connect();        

        Thread.Sleep(250);
        
        if (tcpClient.CurrentState == ClientState.Connected)
        {
            Debug.WriteLine("Socket is connected");
            // Do more stuff 
        } 
        else if (tcpClient.CurrentState == ClientState.Disconnected)
        {
            Debug.WriteLine(@"Socket didn't connect");
            // Do other stuff or try again to connect 
        }
    }
    
    void s_client_ConnectionAttempt(object sender, EventArgs e)
    {
        Debug.WriteLine("Client is connecting to server.");
    }

    void s_client_ConnectionException(object sender, EventArgs e)
    {
        Debug.WriteLine("Client exception - {0}.", e.Argument.Message);
    }

    void s_client_ConnectionEstablished(object sender, EventArgs e)
    {
        Debug.WriteLine("Client connected to server.");
    }

    void s_client_ConnectionTerminated(object sender, EventArgs e)
    {
        Debug.WriteLine("Client disconnected from server.");
    }

    void s_client_ReceiveDataComplete(object sender, GSF.EventArgs<byte[], int> e)
    {
        Debug.WriteLine(string.Format("Received data - {0}.", tcpClient.TextEncoding.GetString(e.Argument1, 0, e.Argument2)));
    }       

I recommend the code from the answer of the user 'Uriel' above. His code in principle works great:

TcpClient client = new TcpClient(host, port);

IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties();
TcpConnectionInformation[] tcpConnections = ipProperties.GetActiveTcpConnections().Where(x => x.LocalEndPoint.Equals(client.Client.LocalEndPoint) && x.RemoteEndPoint.Equals(client.Client.RemoteEndPoint)).ToArray();

but it has a bug:
IPEndPoint.Equals() is used here to search the retrieved list of TCP connections for the one connection that has the same endpoints as the socket TcpClient.Client of the used TCP client.
The idea and concept are fine, but in real life may fail because of the coexistence of IPv4 and IPv6: Current operating systems like Windows 10 support IPv4 and IPv6, and sockets may be created with IPv6 addresses even if addresses like "100.111.1.251" in the IPv4 format were configured:

// Creation of TCP client:
m_tcpClient = new TcpClient ();
m_tcpClient.Connect ("100.111.1.251", 54321);

// Query of the local and remote IP endpoints in Visual Studio Immediate Window:
?m_tcpClient.Client.LocalEndPoint
{[::ffff:100.111.1.254]:55412}
    Address: {::ffff:100.111.1.254}
    AddressFamily: InterNetworkV6
    Port: 55412
?m_tcpClient.Client.RemoteEndPoint
{[::ffff:100.111.1.251]:54321}
    Address: {::ffff:100.111.1.251}
    AddressFamily: InterNetworkV6
    Port: 54321

// Query of the addresses of the local and remote IP endpoints in Visual Studio Immediate Window:
?((IPEndPoint)m_tcpClient.Client.LocalEndPoint).Address
{::ffff:100.111.1.254}
    Address: '((IPEndPoint)m_tcpClient.Client.LocalEndPoint).Address.Address' threw an exception of type 'System.Net.Sockets.SocketException'
    AddressFamily: InterNetworkV6
    IsIPv4MappedToIPv6: true
    IsIPv6LinkLocal: false
    IsIPv6Multicast: false
    IsIPv6SiteLocal: false
    IsIPv6Teredo: false
    ScopeId: 0
?((IPEndPoint)m_tcpClient.Client.RemoteEndPoint).Address
{::ffff:100.111.1.251}
    Address: '((IPEndPoint)m_tcpClient.Client.RemoteEndPoint).Address.Address' threw an exception of type 'System.Net.Sockets.SocketException'
    AddressFamily: InterNetworkV6
    IsIPv4MappedToIPv6: true
    IsIPv6LinkLocal: false
    IsIPv6Multicast: false
    IsIPv6SiteLocal: false
    IsIPv6Teredo: false
    ScopeId: 0

AddressFamily: InterNetworkV6 and IsIPv4MappedToIPv6: true indicate that the IP address in the local IP endpoint is an IPv6 address, although an IPv4 address was used to establish the connection. This obviously is because the socket is created in "dual-mode" or as "dual-stack":
https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses
https://docs.microsoft.com/en-us/dotnet/api/system.net.ipaddress.isipv4mappedtoipv6
https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2
https://www.ibm.com/docs/en/zos/2.2.0?topic=addresses-ipv4-mapped-ipv6

IPGlobalProperties.GetActiveTcpConnections() on the other side seems to always return IPEndPoint objects with IPv4 addresses:

?IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections()[48].LocalEndPoint
{100.111.1.254:55412}
    Address: {100.111.1.254}
    AddressFamily: InterNetwork
    Port: 55412

The consequence is that IPEndPoint.Equals() may return false even if two compared EndPoint objects refer to the same IP endpoint.
The solution for this problem is writing your own Equals() method, like:

public static class EndPointHelper
{
    private static readonly AddressFamily[] addressFamilies =
    {
        AddressFamily.InterNetwork,
        AddressFamily.InterNetworkV6
    };

    public static bool Equals (EndPoint? endPoint1, EndPoint? endPoint2)
    {
        if (endPoint1 is IPEndPoint ipEndPoint1 &&
            endPoint2 is IPEndPoint ipEndPoint2)
        {
            if (ipEndPoint1.AddressFamily != ipEndPoint2.AddressFamily &&
                addressFamilies.Contains(ipEndPoint1.AddressFamily) &&
                addressFamilies.Contains(ipEndPoint2.AddressFamily))
            {
                var ipAddress1AsV6 = ipEndPoint1.Address.MapToIPv6();
                var ipAddress2AsV6 = ipEndPoint2.Address.MapToIPv6();

                return ipAddress1AsV6.Equals(ipAddress2AsV6)
                    && ipEndPoint1.Port.Equals(ipEndPoint2.Port);
            }
        }

        return object.Equals (i_endPoint1, i_endPoint2);
    }
}

Furthermore, there is a bug in .NET 5, which makes the whole solution above unusable: IPGlobalProperties.GetActiveTcpConnections() has a memory leak (see https://github.com/dotnet/runtime/issues/64735), which will not be fixed in .NET 5 any more, because it has run out of support. The bug is not present in .NET 6. If you are tied to .NET 5, you will have to work around it by remembering the connection state yourself in a local variable (e.g. EnumState m_cachedState). Set this variable after each related operation, e.g. after a Connect() you would have to set it to EnumState.Connected.
This method of course will not detect when a connection was closed by the other side, so you have to cyclically check if the connection was closed, using this code:

var  socket    = m_tcpClient.Client;
bool state     = socket.Poll (100, SelectMode.SelectRead);
int  available = socket.Available;
return state && available == 0 // Condition for externally closed connection. The external close will not be recognized until all received data has been read.
           ? EnumState.Idle
           : m_cachedState;
Related