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

Posted

技术标签:

【中文标题】使用任务取消令牌停止 TCP 侦听器【英文标题】:Stop a TCP Listener using Task Cancellation Token 【发布时间】:2019-09-10 17:00:45 【问题描述】:

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

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

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

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

【问题讨论】:

你有while(true) /* block and don’t check cancel token */ 。唯一的出路是通过异常或返回。也许你的内部循环应该检查令牌? 之前试过了,没什么区别。 CancellationToken 被取消时,您必须关闭侦听器套接字。如果您使用阻塞 API(例如,AcceptTcpClient),那么当CancellationToken 被取消时,您需要关闭侦听器套接字从另一个线程 不知道该怎么做,在我的情况下,不同的线程和不同的类。 TCP 侦听器将用于多个实例,因此我需要一种方法来独立关闭每个实例。 【参考方案1】:

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

示例代码在您自己的环境中可能会略有不同,我已经提取了一些我的项目独有的代码膨胀,但您将了解我们在这里所做的事情。在我的项目中,这个 TCP 服务器是使用 NET Core 5.0 IHosted Services 作为后台服务启动的。我下面的代码改编自 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();
                
            );
        

【讨论】:

以上是关于使用任务取消令牌停止 TCP 侦听器的主要内容,如果未能解决你的问题,请参考以下文章

一旦您调用取消注册位置侦听器,Gps 状态侦听器就会停止运行

使用 Spring 启动和停止 JMS 侦听器

在关闭其关联的套接字时取消侦听任务

Firebase:如何停止收听快照

使用 .net 5 与任务(不是线程)的 TCP 连接

quartz2.3.0job任务监听器,监听任务执行前后取消手动处理方法