使用任务取消令牌停止 TCP 侦听器

Stop a TCP Listener using Task Cancellation Token

我无法使用取消令牌来停止 TCP 侦听器。第一个代码摘录是一个示例,我可以在另一个 class 的方法中成功停止测试 while 循环。所以我不明白为什么我不能将这种类似的逻辑应用到 TCP Listener Class。花了很多天阅读关于这个主题的复杂答案,找不到合适的解决方案。

我的软件应用程序要求 TCP 侦听器必须让用户能够从服务器端而非客户端停止它。如果用户想重新配置此侦听器的端口号,那么他们目前必须关闭软件才能 Windows 关闭底层套接字,这不好,因为会影响其他服务 运行 在我的应用程序中。

代码的第一个摘录只是一个例子,我可以从 运行 停止 while 循环,这工作正常,但除了 faat 之外没有那么相关,我希望这对我的工作有用TCP 侦听器:

 public void Cancel(CancellationToken cancelToken) // EXAMPLE WHICH IS WORKING
    {
        Task.Run(async () => 
        {
            while (!cancelToken.IsCancellationRequested)
            {
                await Task.Delay(500);
                log.Info("Test Message!");
            }
        }, cancelToken);
    }

下面是我苦苦挣扎的实际 TCP 侦听器代码

 public void TcpServerIN(string inboundEncodingType, string inboundIpAddress, string inboundLocalPortNumber, CancellationToken cancelToken)
    {
        TcpListener listener = null;

        Task.Run(() =>
        {
            while (!cancelToken.IsCancellationRequested)
            {
                try
                {
                    IPAddress localAddr = IPAddress.Parse(inboundIpAddress);
                    int port = int.Parse(inboundLocalPortNumber);
                    listener = new TcpListener(localAddr, port);
                    // Start listening for client requests.
                    listener.Start();
                    log.Info("TcpListenerIN listener started");

                    // Buffer for reading data
                    Byte[] bytes = new Byte[1024];
                    String data = null;

                    // Enter the listening loop.
                    while (true)
                    {
                        // Perform a blocking call to accept client requests.
                        TcpClient client = listener.AcceptTcpClient();

                        // Once each client has connected, start a new task with included parameters.
                        var task = Task.Run(() =>
                        {
                            // Get a stream object for reading and writing
                            NetworkStream stream = client.GetStream();

                            data = null;
                            int i;

                            // Loop to receive all the data sent by the client.
                            while ((i = stream.Read(bytes, 0, bytes.Length)) != 0)
                            {
                                // Select Encoding format set by string inboundEncodingType parameter.
                                if (inboundEncodingType == "UTF8") { data = Encoding.UTF8.GetString(bytes, 0, i); }
                                if (inboundEncodingType == "ASCII") { data = Encoding.ASCII.GetString(bytes, 0, i); }

                                // Use this if you want to echo each message directly back to TCP Client
                                //stream.Write(msg, 0, msg.Length);

                                // If any TCP Clients are connected then pass the appended string through
                                // the rules engine for processing, if not don't send.
                                if ((listConnectedClients != null) && (listConnectedClients.Any()))
                                {
                                    // Pass the appended message string through the SSSCRulesEngine
                                    SendMessageToAllClients(data);
                                }
                            }
                            // When the remote client disconnetcs, close/release the socket on the TCP Server.
                            client.Close();
                        });
                    }
                }
                catch (SocketException ex)
                {
                    log.Error(ex);
                }
                finally
                {
                    // If statement is required to prevent an en exception thrown caused by the user
                    // entering an invalid IP Address or Port number.
                    if (listener != null)
                    {
                        // Stop listening for new clients.
                        listener.Stop();
                    }
                }
            }
            MessageBox.Show("CancellationRequested");
            log.Info("TCP Server IN CancellationRequested");
        }, cancelToken);
    }

有趣的是,没有人返回任何解决方案,诚然,我花了很长时间才找到解决方案。在使用如下例所示的同步阻塞模式时停止 TCP 侦听器的关键是向 TCP 侦听器本身注册取消令牌,以及在取消令牌被触发时可能已经连接的 TCP 客户端。 (请参阅标记为重要的评论)

示例代码在您自己的环境中可能略有不同,我已经提取了一些我的项目独有的代码膨胀,但您会明白我们在这里所做的事情。在我的项目中,此 TCP 服务器使用 NET Core 5.0 IHosted 服务作为后台服务启动。我下面的代码改编自 MS Docs 上的注释:https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.tcplistener?view=net-5.0

MS Docs 和我下面的示例之间的主要区别是我想允许多个 TCP 客户端连接,因此每次新的 TCP 客户端连接时我都会启动一个新的内部任务。

        /// <summary>
        /// </summary>
        /// <param name="server"></param>
        /// <param name="port"></param>
        /// <param name="logger"></param>
        /// <param name="cancelToken"></param>
        public void TcpServerRun(
            int pluginId,
            string pluginName,
            string encoding,
            int bufferForReadingData,
            string ipAddress,
            int port,
            bool logEvents,
            IServiceScopeFactory _scopeFactory,
            CancellationToken cancelToken)
        {
            IPAddress localAddrIN = IPAddress.Parse(ipAddress);
            TcpListener listener = new TcpListener(localAddrIN, port);

            Task.Run(() =>
            {
                // Dispose the DbContext instance when the task has completed. 'using' = dispose when finished...
                using var scope = _scopeFactory.CreateScope();
                var logger = scope.ServiceProvider.GetRequiredService<ILogger<TcpServer>>();

                try
                {
                    listener.Start();
                    cancelToken.Register(listener.Stop); // THIS IS IMPORTANT!

                    string logData = "TCP Server with name [" + pluginName + "] started Succesfully";
                    // Custom Logger - you would use your own logging method here...
                    WriteLogEvent("Information", "TCP Servers", "Started", pluginName, logData, null, _scopeFactory);


                    while (!cancelToken.IsCancellationRequested)
                    {
                        TcpClient client = listener.AcceptTcpClient();

                        logData = "A TCP Client with IP Address [" + client.Client.RemoteEndPoint.ToString() + "] connected to the TCP Server with name: [" + pluginName + "]";
                        // Custom Logger - you would use your own logging method here...
                        WriteLogEvent("Information", "TCP Servers", "Connected", pluginName, logData, null, _scopeFactory);

                        // Once each client has connected, start a new task with included parameters.
                        var task = Task.Run(async () =>
                        {
                            // Get a stream object for reading and writing
                            NetworkStream stream = client.GetStream();

                            // Buffer for reading data
                            Byte[] bytes = new Byte[bufferForReadingData]; // Bytes variable

                            String data = null;
                            int i;

                            cancelToken.Register(client.Close); // THIS IS IMPORTANT!

                            // Checks CanRead to verify that the NetworkStream is readable. 
                            if (stream.CanRead)
                            {
                                // Loop to receive all the data sent by the client.
                                while ((i = stream.Read(bytes, 0, bytes.Length)) != 0 & !cancelToken.IsCancellationRequested)
                                {
                                    data = Encoding.ASCII.GetString(bytes, 0, i);

                                    logData = "TCP Server with name [" + pluginName + "] received data [" + data + "] from a TCP Client with IP Address [" + client.Client.RemoteEndPoint.ToString() + "]";
                                    // Custom Logger - you would use your own logging method here...
                                    WriteLogEvent("Information", "TCP Servers", "Receive", pluginName, logData, null, _scopeFactory);
                                }
                                // Shutdown and end connection
                                client.Close();

                                logData = "A TCP Client disconnected from the TCP Server with name: [" + pluginName + "]";
                                // Custom Logger - you would use your own logging method here...
                                WriteLogEvent("Information", "TCP Servers", "Disconnected", pluginName, logData, null, _scopeFactory);
                            }
                        }, cancelToken);
                    }
                }
                catch (SocketException ex)
                {
                    // When the cancellation token is called, we will always encounter 
                    // a socket exception for the listener.AcceptTcpClient(); blocking
                    // call in the while loop thread. We want to catch this particular exception
                    // and mark the exception as an accepted event without logging it as an error.
                    // A cancellation token is passed usually when the running thread is manually stopped
                    // by the user from the UI, or will occur when the IHosted service Stop Method
                    // is called during a system shutdown.
                    // For all other unexpected socket exceptions we provide en error log underneath
                    // in the else statement block.
                    if (ex.SocketErrorCode == SocketError.Interrupted)
                    {
                        string logData = "TCP Server with name [" + pluginName + "]  was stopped due to a CancellationTokenSource cancellation. This event is triggered when the SMTP Server is manually stopped from the UI by the user or during a system shutdown.";
                        WriteLogEvent("Information", "TCP Servers", "Stopped", pluginName, logData, null, _scopeFactory);

                    }
                    else
                    {
                        string logData = "TCP Server with name [" + pluginName + "] encountered a socket exception error and exited the running thread.";
                        WriteLogEvent("Error", "TCP Servers", "Socket Exception", pluginName, logData, ex, _scopeFactory);
                    }
                }
                finally
                {
                    // Call the Stop method to close the TcpListener.
                    // Closing the listener does not close any exisiting connections,
                    // simply stops listening for new connections, you are responsible
                    // closing the existing connections which we achieve by registering
                    // the cancel token with the listener.
                    listener.Stop();
                }
            });
        }