2

有朋友来找我的一个问题:在连接的服务器端使用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,或者窥视套接字本身以检查断开连接 - 几乎就像它是一个错误。或者可能缺少某个功能。;)

4

2 回答 2

2

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

// message framing. First, read the #bytes to expect. 

int objectSize = br.ReadInt32(); 

ReadInt32()将无限期地阻塞线程。

于 2010-08-14T12:37:10.583 回答
2

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

如果只发送一个对象并在之后关闭连接,则原始代码将起作用。

必须启动网络操作才能确定连接是否仍处于活动状态。我要做的是,不是直接从网络流中反序列化,而是缓冲到 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);
            }
        }
于 2009-12-13T15:45:26.267 回答