【问题标题】:Detecting client TCP disconnection while using NetworkStream class在使用 NetworkStream 类时检测客户端 TCP 断开连接
【发布时间】:2009-12-13 07:22:50
【问题描述】:

我的一个朋友来找我一个问题:在连接的服务器端使用NetworkStream类时,如果客户端断开连接,NetworkStream检测不到。

精简后,他的 C# 代码如下所示:

List<TcpClient> connections = new List<TcpClient>();
TcpListener listener = new TcpListener(7777);
listener.Start();

while(true)
{
    if (listener.Pending())
    {
        connections.Add(listener.AcceptTcpClient());
    }
    TcpClient deadClient = null;
    foreach (TcpClient client in connections)
    {
        if (!client.Connected)
        {
            deadClient = client;
            break;
        }
        NetworkStream ns = client.GetStream();
        if (ns.DataAvailable)
        {
            BinaryFormatter bf = new BinaryFormatter();
            object o = bf.Deserialize(ns);
            ReceiveMyObject(o);
        }
    }
    if (deadClient != null)
    {
        deadClient.Close();
        connections.Remove(deadClient);
    }
    Thread.Sleep(0);
}

该代码有效,因为客户端可以成功连接并且服务器可以读取发送给它的数据。但是,如果远程客户端调用 tcpClient.Close(),则服务器不会检测到断开连接 - client.Connected 保持为真,而 ns.DataAvailable 为假。

对 Stack Overflow 的搜索提供了一个的答案 - 因为没有调用 Socket.Receive,所以套接字没有检测到断开连接。很公平。我们可以解决这个问题:

foreach (TcpClient client in connections)
{
    client.ReceiveTimeout = 0;
    if (client.Client.Poll(0, SelectMode.SelectRead))
    {
        int bytesPeeked = 0;
        byte[] buffer = new byte[1];
        bytesPeeked = client.Client.Receive(buffer, SocketFlags.Peek);
        if (bytesPeeked == 0)
        {
            deadClient = client;
            break;
        }
        else
        {
            NetworkStream ns = client.GetStream();
            if (ns.DataAvailable)
            {
                BinaryFormatter bf = new BinaryFormatter();
                object o = bf.Deserialize(ns);
                ReceiveMyObject(o);
            }
        }
    }
}

(为简洁起见,我省略了异常处理代码。)

此代码有效,但是,我不会将此解决方案称为“优雅”。我知道的问题的另一个优雅解决方案是为每个 TcpClient 生成一个线程,并允许 BinaryFormatter.Deserialize (née NetworkStream.Read) 调用阻塞,这将正确检测断开连接。不过,这确实会产生为每个客户端创建和维护线程的开销。

我觉得我错过了一些秘密的、很棒的答案,它可以保持原始代码的清晰性,但避免使用额外的线程来执行异步读取。虽然,也许 NetworkStream 类从来没有为这种用途而设计。有人能解释一下吗?

更新:只是想澄清一下,我有兴趣看看 .NET 框架是否有一个解决方案可以涵盖 NetworkStream 的这种使用(即轮询避免阻塞) - 显然可以做到; NetworkStream 可以很容易地包装在提供该功能的支持类中。看起来很奇怪,该框架本质上要求您使用线程来避免阻塞 NetworkStream.Read,或者窥视套接字本身以检查断开连接 - 几乎就像它是一个错误。或者可能缺少某个功能。 ;)

【问题讨论】:

    标签: sockets tcpclient networkstream disconnect


    【解决方案1】:

    服务器是否期望通过同一连接发送多个对象? IF 所以我看不出这段代码是如何工作的,因为没有发送分隔符来表示第一个对象的开始位置和下一个对象的结束位置。

    如果只发送一个对象,然后连接关闭,那么原始代码将起作用。

    必须启动网络操作才能确定连接是否仍处于活动状态。我要做的是,不是直接从网络流中反序列化,而是缓冲到 MemoryStream 中。这将使我能够检测到连接何时丢失。我还会使用消息框架来分隔流上的多个响应。

            MemoryStream ms = new MemoryStream();
    
            NetworkStream ns = client.GetStream();
            BinaryReader br = new BinaryReader(ns);
    
            // message framing. First, read the #bytes to expect.
            int objectSize = br.ReadInt32();
    
            if (objectSize == 0)
                  break; // client disconnected
    
            byte [] buffer = new byte[objectSize];
            int index = 0;
    
            int read = ns.Read(buffer, index, Math.Min(objectSize, 1024);
            while (read > 0)
            {
                 objectSize -= read;
                 index += read;
                 read = ns.Read(buffer, index, Math.Min(objectSize, 1024);
            }
    
            if (objectSize > 0)
            {
                 // client aborted connection in the middle of stream;
                 break;
            } 
            else
            {
                BinaryFormatter bf = new BinaryFormatter();
                using(MemoryStream ms = new MemoryStream(buffer))
                {
                     object o = bf.Deserialize(ns);
                     ReceiveMyObject(o);
                }
            }
    

    【讨论】:

    • 这允许通过解释读取调用的结果来检测断开连接。然而,我的好奇心源于框架明显缺乏“另一种”方法来做到这一点,特别是考虑到它为原始实现提供的优雅。此外,原始代码正确描述了对象,因此可以发送多条消息。我认为这是由于 BinaryFormatter 序列化对象的方式 - 它本质上知道何时“完成”。 (不过,您提供的代码确实需要分隔符。)
    • 你能告诉我你希望如何在框架中实现它吗?框架只是套接字顶部的一个包装器,它是一种传输机制。其他语义需要由应用程序实现。此外,BinaryFormatter 将缓冲区作为反序列化的输入,因此您需要为其提供获取对象所需的确切缓冲区。否则它将无法按预期工作。
    • 你是对的 - 反序列化确实需要一个精确的缓冲区,否则它会痛苦地死去,而且确保流完整不是 BinaryFormatter 的责任;在这种情况下,也不是NetworkStream的。我想也许我一直在以错误的方式思考这个问题,这不是框架的责任,而是我的责任。 ;)
    【解决方案2】:

    是的,但是如果您在获得尺寸之前失去了连接怎么办?即在以下行之前:

    // message framing. First, read the #bytes to expect. 
    
    int objectSize = br.ReadInt32(); 
    

    ReadInt32() 会无限期阻塞线程。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-01-24
      • 1970-01-01
      • 2014-04-14
      • 2012-05-17
      • 1970-01-01
      • 2015-11-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多