首页 > 解决方案 > TCP 服务器连接导致处理器过载

问题描述

我有一个 TCP/IP 服务器,它应该允许连接在通过它发送消息时保持打开状态。但是,似乎有些客户端会为每条消息打开一个新连接,这会导致 CPU 使用率达到最大值。我尝试通过添加超时来解决此问题,但似乎偶尔仍会出现问题。我怀疑我的解决方案不是最佳选择,但我不确定会是什么。

下面是我删除了日志记录、错误处理和处理的基本代码。

private void StartListening()
{            
    try
    {
        _tcpListener = new TcpListener( IPAddress.Any, _settings.Port );
        _tcpListener.Start();
        while (DeviceState == State.Running)
        {
            var incomingConnection = _tcpListener.AcceptTcpClient();
            var processThread = new Thread( ReceiveMessage );
            processThread.Start( incomingConnection );
        }
    }
    catch (Exception e)
    {
        //  Unfortunately, a SocketException is expected when stopping AcceptTcpClient
        if (DeviceState == State.Running) { throw; }
    }
    finally { _tcpListener?.Stop(); }
}

我相信实际的问题是正在创建多个进程线程,但没有被关闭。下面是 ReceiveMessage 的代码。

    private void ReceiveMessage( object IncomingConnection )
    {
        var buffer = new byte[_settings.BufferSize];
        int bytesReceived = 0;
        var messageData = String.Empty;
        bool isConnected = true;

        using (TcpClient connection = (TcpClient)IncomingConnection)
        using (NetworkStream netStream = connection.GetStream())
        {
            netStream.ReadTimeout = 1000;
            try
            {
                while (DeviceState == State.Running && isConnected)
                {
                    //  An IOException will be thrown and captured if no message comes in each second. This is the
                    //  only way to send a signal to close the connection when shutting down. The exception is caught,
                    //  and the connection is checked to confirm that it is still open. If it is, and the Router has
                    //  not been shut down, the server will continue listening.
                    try { bytesReceived = netStream.Read( buffer, 0, buffer.Length ); }
                    catch (IOException e)
                    {
                        if (e.InnerException is SocketException se && se.SocketErrorCode == SocketError.TimedOut)
                        {
                            bytesReceived = 0;
                            if(GlobalSettings.IsLeaveConnectionOpen)
                                isConnected = GetConnectionState(connection);
                            else
                                isConnected = false;
                        }
                        else
                            throw;
                    }

                    if (bytesReceived > 0)
                    {
                        messageData += Encoding.UTF8.GetString( buffer, 0, bytesReceived );
                        string ack = ProcessMessage( messageData );
                        var writeBuffer = Encoding.UTF8.GetBytes( ack );
                        if (netStream.CanWrite) { netStream.Write( writeBuffer, 0, writeBuffer.Length ); }
                        messageData = String.Empty;
                    }
                }
            }
            catch (Exception e) { ... }
            finally { FileLogger.Log( "Closing the message stream.", Verbose.Debug, DeviceName ); }
        }
    }

对于大多数客户端,代码运行正常,但有一些似乎为每条消息创建了一个新连接。我怀疑问题在于我如何处理 IOException。对于失败的系统,代码似乎在第一条消息进来 30 秒后才到达 finally 语句,并且每条消息都会创建一个新的 ReceiveMessage 线程。因此,日志将显示传入的消息,其中 30 秒将开始显示有关消息流被关闭的多条消息。

以下是我检查连接的方法,以防这很重要。

public static bool GetConnectionState( TcpClient tcpClient )
{
    var state = IPGlobalProperties.GetIPGlobalProperties()
        .GetActiveTcpConnections()
        .FirstOrDefault( x => x.LocalEndPoint.Equals( tcpClient.Client.LocalEndPoint )
        && x.RemoteEndPoint.Equals( tcpClient.Client.RemoteEndPoint ) );
    return state != null ? state.State == TcpState.Established : false;
}

标签: c#.nettcptcplistener

解决方案


你在很多层面上重新发明了轮子(以更糟糕的方式):

  1. 你正在做伪阻塞套接字。再加上在像 Linux 这样没有真正线程的操作系统中为每个连接创建一个全新的线程,可能会很快变得昂贵。相反,您应该创建一个没有读取超时 (-1) 的纯阻塞套接字,然后只听它。与 UDP 不同,TCP 将捕获被客户端终止的连接,而无需您轮询它。

  2. 您似乎在执行上述操作的原因是您重新发明了标准的 Keep-Alive TCP 机制。它已经编写好并且可以有效地工作,只需使用它。作为奖励,标准的 Keep-Alive 机制在客户端,而不是服务器端,因此对您的处理更少。

编辑:还有 3. 你真的需要缓存你辛苦创建的线程。如果您有那么多长期连接且每个线程只有一个套接字通信,则系统线程池将不够用,但您可以构建自己的可扩展线程池。您还可以使用 在一个线程上共享多个套接字select,但这会大大改变您的逻辑。


推荐阅读