如何检查TcpClient连接是否关闭?

37

我正在尝试使用TcpClient,并试图找出如何在连接断开时使Connected属性显示为false。

我尝试过:

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

但是它仍然无法显示TcpClient是否已断开连接。如何使用TcpClient解决这个问题?

10个回答

58

我不建议你为了测试socket而进行尝试写操作。也不要依赖于.NET的Connected属性。

如果你想知道远程终端点是否仍处于活动状态,可以使用TcpConnectionInformation:

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();

if (tcpConnections != null && tcpConnections.Length > 0)
{
    TcpState stateOfConnection = tcpConnections.First().State;
    if (stateOfConnection == TcpState.Established)
    {
        // Connection is OK
    }
    else 
    {
        // No active tcp Connection to hostName:port
    }

}
client.Close();

相关链接:
MSDN上的TcpConnectionInformation
MSDN上的IPGlobalProperties
TcpState状态说明
Wikipedia上的Netstat


这是在TcpClient上的扩展方法。

public static TcpState GetState(this TcpClient tcpClient)
{
  var foo = IPGlobalProperties.GetIPGlobalProperties()
    .GetActiveTcpConnections()
    .SingleOrDefault(x => x.LocalEndPoint.Equals(tcpClient.Client.LocalEndPoint));
  return foo != null ? foo.State : TcpState.Unknown;
}

5
这是一个很棒的回答。您唯一可以改进它的方法是将测试作为Socket的扩展方法呈现,以返回套接字状态。 - Peter Wone
1
不错。我真的很想知道是否有更快的方法来完成它。 - Arsen Zahray
1
使用 FirstOrDefault 替代 SingleOrDefault - mozkomor05
3
更改此行代码为:.SingleOrDefault(x => x.LocalEndPoint.Equals(tcpClient.Client.LocalEndPoint) && x.RemoteEndPoint.Equals(tcpClient.Client.RemoteEndPoint)); - Wagner Pereira
1
警告:如果网络支持IPv6,则IPEndPoint.Equals()可能会失败!连接将使用“IPv4映射的IPv6地址”,套接字的两个IP端点在TcpClient.Client中将分别为InterNetworkV6(IPv6),但从GetActiveTcpConnections()检索到的端点将为InterNetwork(IPv4)。//我无法在此处添加屏幕截图,因此我已经添加了一个答案:https://dev59.com/xHM_5IYBdhLWcg3waSb9#72701813 - Tobias Knauss
显示剩余6条评论

9
据我所知/记得,除了读取或写入套接字之外,没有其他方法可以测试套接字是否已连接。
我从未使用过TcpClient,但是Socket类在调用Read时会返回0,如果远程端已经正常关闭。如果远程端没有正常关闭[我认为],则会出现超时异常,抱歉我记不清楚类型了。
像“if(socket.Connected){socket.Write(...)}”这样的代码会创建竞争条件。你最好只是调用socket.Write并处理异常和/或断开连接即可。

是的。套接字层应使用异常进行管理。抛出的IOException将内部异常设置为SocketException,其中包含了检测超时或远程关闭套接字所需的所有信息。 - Luca

8

Peter Wone和uriel的解决方案非常好。但您还需要检查远程终点,因为您可以在本地终点上拥有多个打开的连接。

    public static TcpState GetState(this TcpClient tcpClient)
    {
        var foo = IPGlobalProperties.GetIPGlobalProperties()
          .GetActiveTcpConnections()
          .SingleOrDefault(x => x.LocalEndPoint.Equals(tcpClient.Client.LocalEndPoint)
                             && x.RemoteEndPoint.Equals(tcpClient.Client.RemoteEndPoint)
          );

        return foo != null ? foo.State : TcpState.Unknown;
    }

3

我创建了这个函数并且它可以检查客户端是否仍然与服务器连接。

/// <summary>
/// THIS FUNCTION WILL CHECK IF CLIENT IS STILL CONNECTED WITH SERVER.
/// </summary>
/// <returns>FALSE IF NOT CONNECTED ELSE TRUE</returns>
public bool isClientConnected()
{
    IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties();

    TcpConnectionInformation[] tcpConnections = ipProperties.GetActiveTcpConnections();

    foreach (TcpConnectionInformation c in tcpConnections)
    {
        TcpState stateOfConnection = c.State;

        if (c.LocalEndPoint.Equals(ClientSocket.Client.LocalEndPoint) && c.RemoteEndPoint.Equals(ClientSocket.Client.RemoteEndPoint))
        {
            if (stateOfConnection == TcpState.Established)
            {
                return true;
            }
            else
            {
                return false;
            }

        }

    }

    return false;


}

2

@uriel的答案对我非常有帮助,但我需要用C++/CLI编写它,这并不是完全容易的。这是(大致等效的)C++/CLI代码,并为了保险起见添加了一些鲁棒性检查。

using namespace System::Net::Sockets;
using namespace System::Net::NetworkInformation;

TcpState GetTcpConnectionState(TcpClient ^ tcpClient)
{
    TcpState tcpState = TcpState::Unknown;

    if (tcpClient != nullptr)
    {
        // Get all active TCP connections
        IPGlobalProperties ^ ipProperties = IPGlobalProperties::GetIPGlobalProperties();
        array<TcpConnectionInformation^> ^ tcpConnections = ipProperties->GetActiveTcpConnections();

        if ((tcpConnections != nullptr) && (tcpConnections->Length > 0))
        {
            // Get the end points of the TCP connection in question
            EndPoint ^ localEndPoint = tcpClient->Client->LocalEndPoint;
            EndPoint ^ remoteEndPoint = tcpClient->Client->RemoteEndPoint;

            // Run through all active TCP connections to locate TCP connection in question
            for (int i = 0; i < tcpConnections->Length; i++)
            {
                if ((tcpConnections[i]->LocalEndPoint->Equals(localEndPoint)) && (tcpConnections[i]->RemoteEndPoint->Equals(remoteEndPoint)))
                {
                    // Found active TCP connection in question
                    tcpState = tcpConnections[i]->State;
                    break;
                }
            }
        }
    }

    return tcpState;
}

bool TcpConnected(TcpClient ^ tcpClient)
{
    bool bTcpConnected = false;

    if (tcpClient != nullptr)
    {
        if (GetTcpConnectionState(tcpClient) == TcpState::Established)
        {
            bTcpConnected = true;
        }
    }
    return bTcpConnected;
}

希望这能对某些人有所帮助。


1
我建议使用上面'Uriel'的答案中的代码。他的代码原则上很好用:
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();

但它有一个bug:
在这里使用IPEndPoint.Equals()来搜索检索到的TCP连接列表,以查找与所使用的TCP客户端TcpClient.Client的端点相同的连接。
这个想法和概念很好,但在现实生活中可能会因为IPv4和IPv6的共存而失败:像Windows 10这样的当前操作系统支持IPv4和IPv6,并且即使配置了像"100.111.1.251"这样的IPv4格式的地址,也可能创建带有IPv6地址的套接字。
// 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”和“IsIPv4MappedToIPv6: true”表示本地IP端点中的IP地址为IPv6地址,尽管建立连接时使用的是IPv4地址。这显然是因为套接字是以“双模式”或“双栈”方式创建的:
https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses
https://learn.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() 看起来总是返回带有IPv4地址的 IPEndPoint 对象:
?IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections()[48].LocalEndPoint
{100.111.1.254:55412}
    Address: {100.111.1.254}
    AddressFamily: InterNetwork
    Port: 55412

结果是,即使两个比较的EndPoint对象引用相同的IP端点,IPEndPoint.Equals()可能返回false。
解决此问题的方法是编写自己的Equals()方法,例如:
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);
    }
}

此外,在.NET 5中存在一个错误,使得上面的整个解决方案无法使用:IPGlobalProperties.GetActiveTcpConnections() 存在内存泄漏问题(请参见https://github.com/dotnet/runtime/issues/64735),这个问题在 .NET 5中将不再修复,因为已经停止支持。这个错误在 .NET 6中不存在。如果你被限制在使用 .NET 5,则必须通过在本地变量中记住连接状态来解决它(例如,使用 EnumState m_cachedState)。在每个相关操作之后设置此变量,例如,在 Connect() 之后,您必须将其设置为 EnumState.Connected
当然,这种方法无法检测到连接是否由另一端关闭,因此您必须循环检查连接是否关闭,使用以下代码:

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;

1
请注意,我发现 GSF.Communication 包装器对于 System.Net.Sockets.TcpClient 很有帮助,因为它具有 CurrentState 属性,指示套接字是打开/连接还是关闭/断开连接。您可以在此处找到有关 NuGet 包的详细信息:

https://github.com/GridProtectionAlliance/gsf

以下是如何设置一个简单的TCP套接字并测试其连接是否成功:

    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)));
    }       

1
截至2019年,在跨平台异步环境中,我使用以下代码不断检查TCP通道是否打开。例如,如果以太网电缆在我的Windows机器上被拔出,或者如果我的Android设备上禁用了Wifi,则会触发此检查。
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
        }
    }
}

0
在我的情况下,我正在向服务器发送一些命令(在同一台计算机上运行的虚拟机中),并等待响应。然而,如果服务器在等待期间意外停止,我就不会收到任何通知。我尝试了其他帖子提出的可能性,但都没有起作用(它总是说服务器仍然连接着)。对我来说,唯一有效的方法是向流写入0字节:
var client = new TcpClient();
//... open the client

var stream = client.GetStream();

//... send something to the client

byte[] empty = { 0 };
//wait for response from server
while (client.Available == 0)
{
    //throws a SocketException if the connection is closed by the server
    stream.Write(empty, 0, 0);
    Thread.Sleep(10);
}

0

试试这个,对我有效

private void timer1_Tick(object sender, EventArgs e)
    {
        if (client.Client.Poll(0, SelectMode.SelectRead))
            {
                if (!client.Connected) sConnected = false;
                else
                {
                    byte[] b = new byte[1];
                    try
                    {
                        if (client.Client.Receive(b, SocketFlags.Peek) == 0)
                        {
                            // Client disconnected
                            sConnected = false;
                        }
                    }
                    catch { sConnected = false; }
                }
            }
        if (!sConnected)
        {
          //--Basically what you want to do afterwards
            timer1.Stop();
            client.Close();
            ReConnect();
        }

    }

我使用了计时器,因为我想要在固定的时间间隔内检查连接状态,而不是在循环中使用监听代码[我感觉这会减缓发送-接收过程]


我偶然发现了这个回答,只是好奇为什么它被踩了? - nagates

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