即时检测客户端从服务器套接字断开连接

87

如何检测客户端与我的服务器断开连接?

我在我的 AcceptCallBack 方法中有以下代码

static Socket handler = null;
public static void AcceptCallback(IAsyncResult ar)
{
  //Accept incoming connection
  Socket listener = (Socket)ar.AsyncState;
  handler = listener.EndAccept(ar);
}
我需要找到一种尽快发现客户端从handler Socket断开连接的方法。
我已经尝试过:
1. handler.Available; 2. handler.Send(new byte[1], 0, SocketFlags.None); 3. handler.Receive(new byte[1], 0, SocketFlags.None);
上述方法适用于连接到服务器并希望检测服务器断开连接的情况,但是它们不适用于作为服务器并希望检测客户端断开连接的情况。
任何帮助将不胜感激。

11
@Samuel: 在这篇文章中,TCP和连接标签是非常相关的,因为TCP维护连接(而其他网络协议如UDP则不会)。 - Noldorin
3
我博客中有更多关于心跳解决方案的内容:检测半开(丢失)连接 - Stephen Cleary
这里描述的解决方案对我很有效:https://dev59.com/xHM_5IYBdhLWcg3waSb9#29151608 - Rawk
15个回答

125

由于没有可用的事件来信号socket何时失去连接,因此您将不得不以对您可接受的频率对其进行轮询。

使用这个扩展方法,您可以拥有一个可靠的方法来检测socket是否已经断开连接。

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

33
除非连接的另一端实际上关闭/关闭套接字,否则此方法将无效。在超时期间之前,未插入网络/电源电缆将不会被注意到。立即通知断开连接的唯一方法是使用心跳功能来持续检查连接。 - Kasper Holdum
8
@Smart Alec: 实际上,你应该按照上面展示的示例来使用。如果更改顺序,可能会出现竞态条件:如果 socket.Available 返回0并且在调用 socket.Poll 之前刚好接收到一个数据包,则 Poll 将返回true并且方法将返回 false,尽管套接字实际上仍然处于正常状态。 - vgru
5
这通常运作良好,99%的时间都没问题,但有时会出现虚假的断开连接。 - Matthew Finlay
1
这在电缆拔出的情况下无法工作。来自 MSDN 关于轮询的内容:“此方法无法检测某些连接问题,例如断开的网络电缆或远程主机非正常关闭。您必须尝试发送或接收数据以检测这些错误。” - Lev
5
就像Matthew Finlay所注意到的那样,有时候这会错误地报告已断开连接,因为Poll方法和检查Available属性之间仍然存在竞争条件。一个数据包可能准备好了但还没有被读取,因此Available属性为0——但是一毫秒后,就有数据可以被读取了。更好的选择是尝试使用SocketFlags.Peek标志接收一个字节。或者实现某种形式的心跳,并将连接状态保持在较高级别。或者依靠发送/接收方法(和回调函数,如果使用异步版本)的错误处理机制。 - mbargiel
显示剩余7条评论

29

有人提到了TCP套接字的keepAlive功能。以下是对其进行了很好的描述:

http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html

我是这样使用它的:在套接字连接后,我调用这个函数来设置keepAlive。 keepAliveTime参数指定在没有活动的情况下,直到发送第一个keep-alive数据包的超时时间(以毫秒为单位)。keepAliveInterval参数指定如果没有收到确认,连续发送keep-alive数据包之间的时间间隔(以毫秒为单位)。

    void SetKeepAlive(bool on, uint keepAliveTime, uint keepAliveInterval)
    {
        int size = Marshal.SizeOf(new uint());

        var inOptionValues = new byte[size * 3];

        BitConverter.GetBytes((uint)(on ? 1 : 0)).CopyTo(inOptionValues, 0);
        BitConverter.GetBytes((uint)keepAliveTime).CopyTo(inOptionValues, size);
        BitConverter.GetBytes((uint)keepAliveInterval).CopyTo(inOptionValues, size * 2);

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

我也在使用异步阅读:

socket.BeginReceive(packet.dataBuffer, 0, 128,
                    SocketFlags.None, new AsyncCallback(OnDataReceived), packet);

在回调函数中,捕获了超时引发的SocketException,当套接字在保持活动数据包后没有收到ACK信号时会出现该异常。

public void OnDataReceived(IAsyncResult asyn)
{
    try
    {
        SocketPacket theSockId = (SocketPacket)asyn.AsyncState;

        int iRx = socket.EndReceive(asyn);
    }
    catch (SocketException ex)
    {
        SocketExceptionCaught(ex);
    }
}

这样,我就能够安全地检测TCP客户端和服务器之间的断开连接。


8
是的,将保持连接时间和间隔设置为较低值,任何轮询/发送应在保持连接时间加10倍间隔毫秒后失败。自Vista以来,这10次重试似乎已经硬编码了?即使断开电缆也可以使用,不像其他大多数答案。这应该是被接受的答案。 - toster-cx
我已经实现了你的解决方案,但是不知道如何暂时停止keepalive函数。socket.BeginReceive正在干扰我的正常TCP通信。在程序非空闲状态下如何禁用它而不断开套接字连接? - Albert Tobing

16
这是不可能的。除非你正在使用回环电缆在两台计算机之间进行连接,否则你与服务器之间没有物理连接。
当连接被优雅地关闭时,另一端会收到通知。但如果连接以其他方式断开(比如用户的连接掉线),那么服务器将无法得知,直到它超时(或尝试写入连接并且确认超时)。这就是TCP的工作原理,你必须接受它。
因此,“即时”是不现实的。你所能做的最好的事情就是在超时期限内完成,这取决于代码运行的平台。
编辑: 如果你只想要优雅的连接,那么为什么不从客户端向服务器发送“DISCONNECT”命令呢?

1
谢谢,但我实际上是指优雅的软件断开连接,而不是物理断开连接。我正在运行Windows。 - Smart Alec
1
如果他只是在寻找优雅的断开连接,那么他不需要发送任何东西。他的读取将返回流的末尾。 - user207421

6
"这就是TCP的工作原理,你必须接受它。"
是的,你说得对。我已经意识到这是生活中不可避免的现象。即使是使用此协议(甚至其他协议)的专业应用程序也会展示相同的行为。我甚至在在线游戏中看到过这种情况:你的朋友说“再见”,但他似乎还在线上1-2分钟,直到服务器“清理房间”。
你可以使用这里建议的方法或者实现“心跳”,如先前所述。我选择了前者。但如果我选择了后者,我只需要让服务器每隔一段时间向每个客户端发送一个字节的“ping”请求,并查看是否有超时或无响应。你甚至可以使用后台线程来精确计时实现这一点。如果你真的很担心,甚至可以在某种选项列表(枚举标志或其他内容)中组合实现。但只要确实更新了服务器,稍微延迟一下更新也没什么大不了的。这是互联网,没有人期望它是魔法! :) "

3

将心跳机制实现到您的系统中可能是一种解决方案。这仅在客户端和服务器都在您的控制下时才可能实现。您可以使用一个DateTime对象来跟踪从套接字接收到最后一组字节的时间,并假设在一定时间间隔内没有响应的套接字已丢失。这只有在您实现了心跳/自定义保持活动功能时才有效。


2

我发现另一个解决方法非常有用!

如果您使用异步方法从网络套接字读取数据(我的意思是使用BeginReceive - EndReceive方法),每当连接终止时,就会出现以下情况之一:要么发送了一条没有数据的消息(您可以通过Socket.Available查看 - 即使触发了BeginReceive,其值也将为零),要么在此调用中Socket.Connected的值变为false(然后不要尝试使用EndReceive)。

我发布了我使用的函数,我认为您可以更好地理解我的意思:


private void OnRecieve(IAsyncResult parameter) 
{
    Socket sock = (Socket)parameter.AsyncState;
    if(!sock.Connected || sock.Available == 0)
    {
        // Connection is terminated, either by force or willingly
        return;
    }

    sock.EndReceive(parameter);
    sock.BeginReceive(..., ... , ... , ..., new AsyncCallback(OnRecieve), sock);

    // To handle further commands sent by client.
    // "..." zones might change in your code.
}

2
这对我很有用,关键是您需要使用轮询来分析套接字状态的单独线程。在与套接字执行故障检测的同一线程中执行此操作会失败。
//open or receive a server socket - TODO your code here
socket = new Socket(....);

//enable the keep alive so we can detect closure
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);

//create a thread that checks every 5 seconds if the socket is still connected. TODO add your thread starting code
void MonitorSocketsForClosureWorker() {
    DateTime nextCheckTime = DateTime.Now.AddSeconds(5);

    while (!exitSystem) {
        if (nextCheckTime < DateTime.Now) {
            try {
                if (socket!=null) {
                    if(socket.Poll(5000, SelectMode.SelectRead) && socket.Available == 0) {
                        //socket not connected, close it if it's still running
                        socket.Close();
                        socket = null;    
                    } else {
                        //socket still connected
                    }    
               }
           } catch {
               socket.Close();
            } finally {
                nextCheckTime = DateTime.Now.AddSeconds(5);
            }
        }
        Thread.Sleep(1000);
    }
}

1
以上回答可以总结如下:
Socket.Connected属性根据最后一次读取或接收状态确定套接字状态,因此在手动关闭连接或远程端优雅地关闭套接字(关闭)之前,它无法检测当前的断开状态。
因此,我们可以使用以下函数来检查连接状态:
   bool IsConnected(Socket socket)
    {
        try
        {
            if (socket == null) return false;
            return !((socket.Poll(5000, SelectMode.SelectRead) && socket.Available == 0) || !socket.Connected);
        }
        catch (SocketException)
        {
            return false;
        }

        //the above code is short exp to :
        /* try
         {
             bool state1 = socket.Poll(5000, SelectMode.SelectRead);
             bool state2 = (socket.Available == 0);
             if ((state1 && state2) || !socket.Connected)
                 return false;
             else
                 return true;
         }
         catch (SocketException)
         {
             return false;
         }
        */
    }

此外,上述检查需要考虑轮询响应时间(阻塞时间)。 正如微软文档所说:此轮询方法“无法检测到像断开网络电缆或远程主机不正常关闭这样的问题”。 同时,如上所述,套接字.poll和套接字.avaiable之间存在竞争条件,可能会导致虚假断开连接。
如微软文档所述,最好的方法是尝试发送或接收数据以检测这些类型的错误。 下面的代码来自微软文档:
 // This is how you can determine whether a socket is still connected.
 bool IsConnected(Socket client)
 {
    bool blockingState = client.Blocking; //save socket blocking state.
    bool isConnected = true;
    try
    {
       byte [] tmp = new byte[1]; 
       client.Blocking = false;
       client.Send(tmp, 0, 0); //make a nonblocking, zero-byte Send call (dummy)
       //Console.WriteLine("Connected!");
    }
    catch (SocketException e)
    {
       // 10035 == WSAEWOULDBLOCK
       if (e.NativeErrorCode.Equals(10035))
       {
           //Console.WriteLine("Still Connected, but the Send would block");
       }
       else
       {
           //Console.WriteLine("Disconnected: error code {0}!", e.NativeErrorCode);
           isConnected  = false;
       }
   }
   finally
   {
       client.Blocking = blockingState;
   }
   //Console.WriteLine("Connected: {0}", client.Connected);
   return isConnected  ;
 }

//以下是来自微软文档的评论*

  • socket.Connected 属性获取 Socket 的连接状态,该状态是最后一次 I/O 操作的状态。当它返回 false 时,Socket 要么从未连接过,要么已经断开连接。
  • Connected 不是线程安全的;当 Socket 从另一个线程断开连接时,它可能会在操作中止后返回 true
  • Connected 属性的值反映了最近操作的连接状态。
  • 如果您需要确定当前连接状态,请进行非阻塞、零字节的发送调用。如果调用成功返回或抛出 WAEWOULDBLOCK 错误代码(10035),则套接字仍然连接; //否则,套接字已不再连接。

1

在接受的答案中,mbargielmycelo的评论得到了扩展,以下内容可与服务器端的非阻塞套接字一起使用,以通知客户端是否已关闭。

这种方法不会受到影响接受的答案中Poll方法所遇到的竞争条件的影响。

// Determines whether the remote end has called Shutdown
public bool HasRemoteEndShutDown
{
    get
    {
        try
        {
            int bytesRead = socket.Receive(new byte[1], SocketFlags.Peek);

            if (bytesRead == 0)
                return true;
        }
        catch
        {
            // For a non-blocking socket, a SocketException with 
            // code 10035 (WSAEWOULDBLOCK) indicates no data available.
        }

        return false;
    }
}

该方法基于一个事实,即在远程端关闭其套接字并且我们已经读取了所有数据后,Socket.Receive方法会立即返回零。来自Socket.Receive documentation:

如果远程主机使用Shutdown方法关闭Socket连接,并且所有可用数据都已接收,则Receive方法将立即完成并返回零字节。

如果您处于非阻塞模式,并且协议堆栈缓冲区中没有可用数据,则Receive方法将立即完成并引发SocketException。

第二点解释了需要try-catch的原因。
使用SocketFlags.Peek标志会使任何接收到的数据保持不变,以便使用单独的接收机制进行读取。
上述方法也适用于阻塞套接字,但请注意,代码将在接收调用上阻塞(直到接收到数据或接收超时过期,再次导致SocketException)。

1

这里的示例代码 http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.connected.aspx 展示了如何在不发送数据的情况下确定Socket是否仍处于连接状态。

如果您在服务器程序中调用了Socket.BeginReceive(),然后客户端“优雅”地关闭了连接,则会调用接收回调函数,并且EndReceive()将返回0字节。这些0字节意味着客户端“可能”已经断开连接。然后,您可以使用MSDN示例代码中展示的技术来确定连接是否已关闭。


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