使用NetworkStream类时检测客户端TCP断开连接

时间:2009-12-13 07:22:51

标签: sockets tcpclient networkstream disconnect

我的一位朋友遇到了一个问题:当在连接的服务器端使用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仍为true,并且ns.DataAvailable为false。

搜索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éeNetworkStream.Read)调用阻塞,这将正确检测断开连接。但是,这确实会产生为每个客户端创建和维护一个线程的开销。

我感觉我错过了一些秘密的,令人敬畏的答案,它将保留原始代码的清晰度,但避免使用额外的线程来执行异步读取。或许,NetworkStream类可能从未设计过这种用法。任何人都能解释一下吗?

更新:只是想澄清我有兴趣看看.NET框架是否有解决方案来涵盖NetworkStream的这种使用(即轮询避免阻塞) - 显然可以做到; NetworkStream可以轻松地包含在提供功能的支持类中。框架本质上要求你使用线程来避免阻塞NetworkStream.Read,或者查看套接字本身来检查断开连接 - 这几乎就像是一个bug。或者可能缺乏某项功能。 ;)

2 个答案:

答案 0 :(得分:1)

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

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

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

答案 1 :(得分:1)

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

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

int objectSize = br.ReadInt32(); 

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