【问题标题】:Continously Reading from Multiple TCP Connections on CF连续读取 CF 上的多个 TCP 连接
【发布时间】:2017-11-10 09:33:08
【问题描述】:

我有一个简单的 TCP 服务器,它能够侦听并接受一个端口上的多个连接。然后它不断地等待从其连接中读取数据。为方便起见,它使用称为 ConnectedClient 的 TcpClient 的包装类,并使用 ConnectedClients 的列表(字典)来跟踪所有连接。基本上是这样的:

/* this method waits to accept connections indefinitely until it receives 
   the signal from the GUI thread to stop. When a connection is accepted, it 
   adds the connection to the list and calls a method called ProcessClient, 
   which returns almost immediately.*/
public void waitForConnections() {
        // this method has access to a TcpListener called listener that was started elsewhere
        try {
            while (!_abort) {
                TcpClient socketClient = listener.AcceptTcpClient();

                //Connected client constructor takes the TcpClient as well as a callback that it uses to print status messages to the GUI if   
                ConnectedClient client = new ConnectedClient(socketClient, onClientUpdate);
                clients.Add(client.id, client);
                ProcessClient(client);
            }
        }
        catch (Exception e) {
            onStatusUpdate("Exception Occurred: " + e.Message);
        }
    }

    /* This method doesn't do much other than call BeginRead on the connection */
    private void ProcessClient(ConnectedClient client) {
        try {
            // wrapper class contains an internal buffer for extracting data as well as a TcpClient
            NetworkStream stream = client.tcpClient.GetStream();
            stream.BeginRead(client.buffer, 0, client.tcpClient.ReceiveBufferSize, new AsyncCallback(StreamReadCompleteCallback), client);
        }
        catch (Exception ex) {
            onStatusUpdate(ex.Message);
        }
    }

在我的回调函数 StreamReadCompleteCallback 中,我调用 EndRead,检查 EndRead 的返回值以检测连接是否已关闭。如果返回值大于零,我提取/处理读取的数据并在同一客户端上再次调用 BeginRead。如果返回值为零,则连接已关闭,我将删除连接(从列表中删除、关闭 TcpClient 等)。

    private void StreamReadCompleteCallback(IAsyncResult ar) {
        ConnectedClient client = (ConnectedClient)ar.AsyncState;

        try {
            NetworkStream stream = client.tcpClient.GetStream();

            int read = stream.EndRead(ar);
            if (read != 0) {
                // data extraction/light processing of received data
                client.Append(read);
                stream.BeginRead(client.buffer, 0, client.tcpClient.ReceiveBufferSize, new AsyncCallback(StreamReadCompleteCallback), client);
            }
            else {
                DisconnectClient(client);
            }
        }
        catch (Exception ex) {
            onStatusUpdate(ex.Message);
        }
    }

所有这些都可以正常工作,我可以接受连接并从多个客户端设备等读取数据。

我的问题是:这种从连接的客户端连续读取的方法会导致每个连接都有一个工作线程正在等待 BeginRead 返回。

所以如果我有 10 个连接,我就有 10 个 BeginReads 进行。

让这么多工作线程坐在那里等待阅读似乎很浪费。还有其他更好的方法来实现这一点吗?如果我有大量活动连接,我最终会耗尽内存来添加连接。

是否有一个线程来轮询每个连接的 DataAvailable 属性,直到出现某些东西,然后让一个线程读取/处理是一种解决方案?

或者创建所有这些工作线程没有我想的那么重要?

【问题讨论】:

    标签: c# multithreading tcp server


    【解决方案1】:

    这种从连接的客户端连续读取的方法导致每个连接都有一个等待 BeginRead 返回的工作线程

    不,它没有。事实上,使用 BeginRead() 或其他异步替代方法之一来处理 Socket 对象上的 I/O 是最具可扩展性的使用方法。

    是否会有一个线程轮询每个连接的 DataAvailable 属性,直到出现某些东西,然后让一个线程读取/处理是一种解决方案?

    没有。这将是可怕的。通过DataAvailableSelect() 轮询套接字效率极低,仅检查套接字状态就迫使大量CPU 时间投入。操作系统提供了很好的异步机制来处理这个问题;轮询实现会忽略这一点并自行完成所有工作。

    或者创建所有这些工作线程没有我想的那么重要?

    您并没有创建您认为的线程。当您使用异步 API 时,它们会利用窗口中称为 I/O 完成端口 的功能。 I/O 完成端口与 I/O 操作相关联,线程可以在端口上等待。但是一个线程可以处理大量操作的等待,因此有十个未完成的读取操作实际上并不会导致创建十个不同的线程。

    .NET 管理一个线程池来处理这些操作,作为ThreadPool 类的一部分进行管理。您可以监视该类以查看 IOCP 池的行为(与用于QueueUserWorkItem() 的工作线程池不同)。

    .NET 将根据需要分配新的 IOCP 对象和线程来服务您的网络 I/O 操作。您可以放心,它将以合理、有效的方式进行。

    在非常大的范围内,与读取操作相关的对象的垃圾收集开销可能会发挥作用。在这种情况下,您可以使用ReceiveAsync() 方法,该方法允许您将自己的状态对象池重用于操作,这样您就不会不断地创建和丢弃对象。

    另一个可能出现的问题是内存碎片,尤其是在大对象堆中(取决于您使用的缓冲区的大小)。当您在套接字上启动读取操作时,必须固定缓冲区,以防止 .NET 压缩它所在的堆。

    但这些问题并不是避免使用异步 API 的理由(事实上,第二个问题无论如何都会发生)。它们只是需要注意的事情。使用异步 API 实际上是最好的方法。

    也就是说,BeginReceive() 是“老派”。它可以工作,但您可以将BeginReceive() 操作包装在Task 中(请参阅Task.FromAsync() 和TPL 和Traditional .NET Framework Asynchronous Programming),或者您可以将整个Socket 包装在NetworkStream 对象中(具有@987654336 @ 和类似方法),这将允许您以更易读的方式编写异步代码,而无需使用显式回调方法。对于网络 I/O 总是以与 UI 交互达到高潮的场景,允许您使用async/await 这样做,再次以更易读、更易于编写的方式。

    【讨论】:

    • 我在一个紧凑的框架上开发,不能使用任何 async/await/tasks/任何有趣的东西。据我所知,就异步操作而言,BeginRead、EndRead 和线程是我唯一的选择。 BeginReceive 是否类似于 BeginRead 但在套接字上?
    • 您的问题根本没有提供任何迹象表明您正在使用 CF。也就是说,是的...BeginReceive()BeginRead() 是等效的操作。我不知道 CF 是否具有作为桌面操作系统的全套 IOCP 功能,但我相信异步 API 仍然比其他替代方案更可取。
    • 好点。编辑以反映这一点。我正在尝试验证 IOCP,但我目前在调试器中看到的只是当我调用 BeginRead 时,窗格中会出现一个新的工作线程。关于如何解决这个问题的任何建议?
    • IOCP 的重要性主要在处理大型服务器时受到关注。至少有几十个连接,如果不是更多的话。 CF 显然不是任何 类型的大型服务器实现的合适平台,因此这不应该是您的主要关注点。第一次调用BeginRead() 时可能会看到一个新线程,但这很正常,因为确实需要填充IOCP 线程池。我怀疑你会看到十次读取的十个线程,但即使你看到了,我相信 CF 知道它在做什么。如果您看到真正需要解决的问题,请担心。
    • 这是我最初的想法,但似乎每个BeginRead 都会添加一个新线程,而不管线程/读取/连接的数量如何。也许 IOCP 在 CF 上的性能不如预期,因为该平台对于大型服务器本质上是不可行的?在这种情况下,我认为最好的选择是设置连接数的硬性上限。
    猜你喜欢
    • 1970-01-01
    • 2018-06-28
    • 2016-11-19
    • 2017-08-05
    • 1970-01-01
    • 2015-02-05
    • 1970-01-01
    • 1970-01-01
    • 2016-02-01
    相关资源
    最近更新 更多