如何在C#中检查套接字是否已连接/断开连接?

82
你如何检查网络套接字(System.Net.Sockets.Socket)是否仍然连接,即使其他主机在断开连接时没有向您发送数据包(例如因为它不正常地断开连接)?
11个回答

119

正如Paul Turner所回答的那样,Socket.Connected不能在此情况下使用。您需要每次轮询连接以查看连接是否仍处于活动状态。这是我使用的代码:

bool SocketConnected(Socket s)
{
    bool part1 = s.Poll(1000, SelectMode.SelectRead);
    bool part2 = (s.Available == 0);
    if (part1 && part2)
        return false;
    else
        return true;
}

它的工作原理如下:

  • s.Poll 返回 true 如果
    • 连接已关闭、重置、终止或挂起(表示没有活动连接)
    • 连接处于活动状态且有可读取的数据
  • s.Available 返回可供读取的字节数
  • 如果两者都为 true:
    • 没有可用数据进行读取,因此连接不活动

3
请再次阅读文档,它还会在连接关闭或待处理时返回true,因此您不知道它是否处于活动状态,除非您检查另一端是否向您发送了一些数据。 - zendar
8
作为一行代码:return !(socket.Poll(1, SelectMode.SelectRead) && socket.Available == 0) 的意思是:如果在等待时间内有可读数据,则返回 true,否则返回 false。 - kasperhj
5
如果你还没有连接套接字,这个操作将失败。如果你这样做 "Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);" 然后再执行 MessageBox.Show(SocketConnected(s)); 你会得到 True。参见 https://dev59.com/wHE85IYBdhLWcg3wr1ge#14925438。 - Automatico
12
根据 Poll 方法的 MSDN 页面:该方法无法检测某些连接问题,例如断开的网络电缆或远程主机意外关闭。您必须尝试发送或接收数据以检测这些错误。 - some_engineer
5
请注意,此代码不是线程安全的!很容易发生另一个线程在 s.Poll 和 s.Available 之间读取数据,因此 s.Poll 返回 true,但是接着缓冲区就被清空了,导致 part2 也变为 true,并且错误地假定客户端已断开连接。因此,您需要使用 lock {...} 或类似方式来包装此代码中的每个 s.Receive,或者采用最佳方法是什么? - Wolfram
显示剩余4条评论

34

正如zendar所写,使用Socket.PollSocket.Available非常好,但需要考虑到套接字在第一次初始化时可能还未准备好。这是最后(我相信)的信息,由Socket.Connected属性提供。该方法的修订版本将类似于以下内容:

 static bool IsSocketConnected(Socket s)
    {
        return !((s.Poll(1000, SelectMode.SelectRead) && (s.Available == 0)) || !s.Connected);

/* The long, but simpler-to-understand version:

        bool part1 = s.Poll(1000, SelectMode.SelectRead);
        bool part2 = (s.Available == 0);
        if ((part1 && part2 ) || !s.Connected)
            return false;
        else
            return true;

*/
    }

很酷,我做了一个简单的尝试并且成功了,我会进行更复杂的测试。 - fieldChao
使用德摩根定理...返回!(socket.Poll(1, SelectMode.SelectRead) && socket.Available == 0) && socket.Connected; - Latency
@延迟 这个可能是正确的,但它只会进一步模糊陈述,使读者更难理解。编译器无论如何都会对其进行优化,因此这两个陈述在编译后可能是相同的。 - Automatico
@automatico 编译器不会优化旨在成为其相反的逻辑。这可能更容易被其他人阅读,两者是重言式等价的。 - Latency

23

Socket.Connected 属性会告诉您套接字是否被认为已连接。它实际上反映了套接字上执行的最后一个发送/接收操作的状态。

如果套接字已由您自己的操作关闭(处理套接字,调用断开连接的方法),Socket.Connected 将返回 false。如果套接字已被其他方式断开连接,则该属性将在下一次尝试发送或接收信息之前返回 true,此时将抛出 SocketExceptionObjectDisposedException

您可以在异常发生后检查该属性,但在此之前是不可靠的。


12

如果您拔掉网络电缆,或服务器崩溃,或路由器崩溃,或者忘记支付互联网账单,接受的答案似乎不起作用。设置TCP保持活动选项以提高可靠性。

public static class SocketExtensions
{
    public static void SetSocketKeepAliveValues(this Socket instance, int KeepAliveTime, int KeepAliveInterval)
    {
        //KeepAliveTime: default value is 2hr
        //KeepAliveInterval: default value is 1s and Detect 5 times

        //the native structure
        //struct tcp_keepalive {
        //ULONG onoff;
        //ULONG keepalivetime;
        //ULONG keepaliveinterval;
        //};

        int size = Marshal.SizeOf(new uint());
        byte[] inOptionValues = new byte[size * 3]; // 4 * 3 = 12
        bool OnOff = true;

        BitConverter.GetBytes((uint)(OnOff ? 1 : 0)).CopyTo(inOptionValues, 0);
        BitConverter.GetBytes((uint)KeepAliveTime).CopyTo(inOptionValues, size);
        BitConverter.GetBytes((uint)KeepAliveInterval).CopyTo(inOptionValues, size * 2);

        instance.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null);
    }
}



// ...
Socket sock;
sock.SetSocketKeepAliveValues(2000, 1000);

时间值设置了自数据上次发送以来的超时时间。接着它会尝试发送和接收一个保持连接(keep-alive)数据包。如果失败了,则在指定的间隔内重试10次(从Vista开始硬编码的数字)。如果这些尝试都失败了,则认为连接已经失效。

因此,以上值将导致2+10*1 = 12秒的检测时间。在此之后,任何对该套接字的读/写/轮询操作都应该失败。


我通过强制断开套接字(例如从当前端口的工具)来断开连接,但没有自动重新连接。 - Eitan
2
它不会自动重新连接。这只是一种“更好”的方式,可以在不发送任何数据的情况下检测到连接中断。 - toster-cx
3
结合Zondar的建议(https://dev59.com/wHE85IYBdhLWcg3wr1ge#2661876),使用**KeepAlivesValues**的组合使我能够检测出毫秒级的间歇。 - Julio Nobre

6
public static class SocketExtensions
{
    private const int BytesPerLong = 4; // 32 / 8
    private const int BitsPerByte = 8;

    public static bool IsConnected(this Socket socket)
    {
        try
        {
            return !(socket.Poll(1000, SelectMode.SelectRead) && socket.Available == 0);
        }
        catch (SocketException)
        {
            return false;
        }
    }


    /// <summary>
    /// Sets the keep-alive interval for the socket.
    /// </summary>
    /// <param name="socket">The socket.</param>
    /// <param name="time">Time between two keep alive "pings".</param>
    /// <param name="interval">Time between two keep alive "pings" when first one fails.</param>
    /// <returns>If the keep alive infos were succefully modified.</returns>
    public static bool SetKeepAlive(this Socket socket, ulong time, ulong interval)
    {
        try
        {
            // Array to hold input values.
            var input = new[]
            {
                (time == 0 || interval == 0) ? 0UL : 1UL, // on or off
                time,
                interval
            };

            // Pack input into byte struct.
            byte[] inValue = new byte[3 * BytesPerLong];
            for (int i = 0; i < input.Length; i++)
            {
                inValue[i * BytesPerLong + 3] = (byte)(input[i] >> ((BytesPerLong - 1) * BitsPerByte) & 0xff);
                inValue[i * BytesPerLong + 2] = (byte)(input[i] >> ((BytesPerLong - 2) * BitsPerByte) & 0xff);
                inValue[i * BytesPerLong + 1] = (byte)(input[i] >> ((BytesPerLong - 3) * BitsPerByte) & 0xff);
                inValue[i * BytesPerLong + 0] = (byte)(input[i] >> ((BytesPerLong - 4) * BitsPerByte) & 0xff);
            }

            // Create bytestruct for result (bytes pending on server socket).
            byte[] outValue = BitConverter.GetBytes(0);

            // Write SIO_VALS to Socket IOControl.
            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
            socket.IOControl(IOControlCode.KeepAliveValues, inValue, outValue);
        }
        catch (SocketException)
        {
            return false;
        }

        return true;
    }
}
  1. 将SocketExtensions类复制到您的项目中
  2. 在您的套接字上调用SetKeepAlive - socket.SetKeepAlive(1000, 2);
  3. 添加一个定时器来检查IsConnected函数

5

我基于这篇MSDN文章编写了一个扩展方法,用于确定套接字是否仍然连接。

public static bool IsConnected(this Socket client)
{
    bool blockingState = client.Blocking;

    try
    {
        byte[] tmp = new byte[1];

        client.Blocking = false;
        client.Send(tmp, 0, 0);
        return true;
    }
    catch (SocketException e)
    {
        // 10035 == WSAEWOULDBLOCK
        if (e.NativeErrorCode.Equals(10035))
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    finally
    {
        client.Blocking = blockingState;
    }
}

2
看起来是一个不错的方法,但对我来说并不起作用。如果我在建立连接后只是拔掉网络电缆,这种方法会一直返回_true_。 - Rev
我在 MSDN 示例页面上看到了它,尝试了一下,但是没有用。 - fieldChao
这段代码不具备可移植性 - 10035 是一个 WinSock 错误码。 - Johan Franzén

3
Alexander Loggerzendar的答案中指出,要完全确定连接是否成功,您需要发送一些内容。如果您的连接方根本不读取此套接字上的任何内容,则可以使用以下代码。
bool SocketConnected(Socket s)
{
  // Exit if socket is null
  if (s == null)
    return false;
  bool part1 = s.Poll(1000, SelectMode.SelectRead);
  bool part2 = (s.Available == 0);
  if (part1 && part2)
    return false;
  else
  {
    try
    {
      int sentBytesCount = s.Send(new byte[1], 1, 0);
      return sentBytesCount == 1;
    }
    catch
    {
      return false;
    }
  }
}

即使如此,直到检测到网络电缆断裂或类似故障,可能仍需要几秒钟的时间。


非常好的答案!谢谢。 - RendoJack
这确实更好地检测到了关闭的连接,但是你必须小心,确保你的服务器会回显此发送的数据。当我这样做时,下一个完整事务中接收到的数据就被污染了。 - hal9000

2

根据NibblyPigzendar的建议,我编写了以下代码,在我进行的每个测试中都能正常工作。最终我需要使用ping和poll两个命令。ping会让我知道电缆是否已断开,或者物理层是否出现故障(路由器关闭等)。但有时在重新连接后我会得到一个RST,ping是正常的,但tcp状态却不是。

#region CHECKS THE SOCKET'S HEALTH
    if (_tcpClient.Client.Connected)
    {
            //Do a ping test to see if the server is reachable
            try
            {
                Ping pingTest = new Ping()
                PingReply reply = pingTest.Send(ServeripAddress);
                if (reply.Status != IPStatus.Success) ConnectionState = false;
            } catch (PingException) { ConnectionState = false; }

            //See if the tcp state is ok
            if (_tcpClient.Client.Poll(5000, SelectMode.SelectRead) && (_tcpClient.Client.Available == 0))
            {
                ConnectionState = false;
            }
        }
    }
    else { ConnectionState = false; }
#endregion

1

最好的方法就是让客户端每隔X秒发送一个PING,服务器在一段时间内没有收到PING后就假定客户端已经断开连接。

当我使用套接字时,我遇到了与你相同的问题,这是我唯一能做的方法。套接字的connected属性从来都不正确。

最终,我转而使用WCF,因为它比套接字更可靠。


-3

就像@toster-cx所说,只需使用KeepAlive,并使用Socket Connected状态来检查Socket是否仍然连接。将接收超时设置为与keepalive相同的超时时间。如果有更多问题,我很乐意帮助!


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