Search code examples
tcplistenercancellationtokensource

Stop a TCP Listener using Task Cancellation Token


I am unable to use cancellation tokens to stop a TCP Listener. The first code extract is an example where I can successfully stop a test while loop in a method from another class. So I don't understand why I cant apply this similar logic to the TCP Listener Class. Spent many days reading convoluted answers on this topic and cannot find a suitable solution.

My software application requires that the TCP Listener must give the user the ability to stop it from the server end, not the client. If a user wants to re-configure the port number for this listener then they would currently have to shutdown the software in order for Windows to close the underlying socket, this is no good as would affect the other services running in my app.

This first extract of code is just an example where I am able to stop a while loop from running, this works OK but is not that relevant other than the faat I would expect this to work for my TCP Listener:

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

Now below is the actual TCP Listener code I am struggling with

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

Solution

  • Interesting to see that no one had come back with any solutions, admittedly it took me a long while to figure out a solution. The key to stopping the TCP Listener when using a synchronous blocking mode like the example below is to register the Cancellation Token with the TCP Listener itself, as well the TCP Client that may have already been connected at the time the Cancellation Token was fired. (see comments that are marked as IMPORTANT)

    The example code may differ slightly in your own environment and I have extracted some code bloat that is unique to my project, but you'll get the idea in what we're doing here. In my project this TCP Server is started as a background service using NET Core 5.0 IHosted Services. My code below was adapted from the notes on MS Docs: https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.tcplistener?view=net-5.0

    The main difference between the MS Docs and my example below is I wanted to allow multiple TCP Clients to connect hence the reason why I start up a new inner Task each time a new TCP Client connects.

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