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

发布于 2024-08-14 17:55:32 字数 2476 浏览 7 评论 0原文

我的一个朋友向我提出了一个问题:当在连接的服务器端使用 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ée NetworkStream.Read)调用阻塞,这将正确检测断开连接。不过,这确实会产生为每个客户端创建和维护线程的开销。

我感觉我错过了一些秘密的、很棒的答案,它可以保留原始代码的清晰度,但避免使用额外的线程来执行异步读取。不过,也许 NetworkStream 类从来就不是为这种用途而设计的。任何人都可以透露一些信息吗?

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

A friend of mine came to me with a problem: when using the NetworkStream class on the server end of the connection, if the client disconnects, NetworkStream fails to detect it.

Stripped down, his C# code looked like this:

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);
}

The code works, in that clients can successfully connect and the server can read data sent to it. However, if the remote client calls tcpClient.Close(), the server does not detect the disconnection - client.Connected remains true, and ns.DataAvailable is false.

A search of Stack Overflow provided an answer - since Socket.Receive is not being called, the socket is not detecting the disconnection. Fair enough. We can work around that:

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);
            }
        }
    }
}

(I have left out exception handling code for brevity.)

This code works, however, I would not call this solution "elegant". The other elegant solution to the problem I am aware of is to spawn a thread per TcpClient, and allow the BinaryFormatter.Deserialize (née NetworkStream.Read) call to block, which would detect the disconnection correctly. Though, this does have the overhead of creating and maintaining a thread per client.

I get the feeling that I'm missing some secret, awesome answer that would retain the clarity of the original code, but avoid the use of additional threads to perform asynchronous reads. Though, perhaps, the NetworkStream class was never designed for this sort of usage. Can anyone shed some light?

Update: Just want to clarify that I'm interested to see if the .NET framework has a solution that covers this use of NetworkStream (i.e. polling and avoiding blocking) - obviously it can be done; the NetworkStream could easily be wrapped in a supporting class that provides the functionality. It just seemed strange that the framework essentially requires you to use threads to avoid blocking on NetworkStream.Read, or, to peek on the socket itself to check for disconnections - almost like it's a bug. Or a potential lack of a feature. ;)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

小伙你站住 2024-08-21 17:55:32

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

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

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

Is the server expecting to be sent multiple objects over the same connection? IF so I dont see how this code will work, as there is no delimiter being sent that signifies where the first object starts and the next object ends.

If only one object is being sent and the connection closed after, then the original code would work.

There has to be a network operation initiated in order to find out if the connection is still active or not. What I would do, is that instead of deserializing directly from the network stream, I would instead buffer into a MemoryStream. That would allow me to detect when the connection was lost. I would also use message framing to delimit multiple responses on the stream.

        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);
            }
        }
这个俗人 2024-08-21 17:55:32

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

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

int objectSize = br.ReadInt32(); 

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

Yeah but what if you lose a connection before getting the size? i.e. right before the following line:

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

int objectSize = br.ReadInt32(); 

ReadInt32() will block the thread indefinitely.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文