Question3r
Question3r

Reputation: 3742

TcpListener blocks thread while listening for incoming messages from TcpClients

I created a new .NET Core worker service project. This service should also act as a TCP socket server and listen for incoming messages. So based on the code sample from the docs this is what I'm basically doing

public class Worker : BackgroundService
{
    public override async Task StartAsync(CancellationToken cancellationToken)
    {
        TcpListener tcpListener = new TcpListener(IPAddress.Parse("127.0.0.1"), 1234);
        tcpListener.Start();
        
        try
        {
            while (true)
            {
                TcpClient tcpClient = await tcpListener.AcceptTcpClientAsync();
                NetworkStream tcpClientStream = tcpClient.GetStream();
                using StreamReader streamReader = new StreamReader(tcpClientStream);
                string messageText = await streamReader.ReadToEndAsync();
        
                // ... do things with messageText ...
            }
        }
        catch (Exception exception)
        {
            // ... error handling ...
        }

        await base.StartAsync(cancellationToken);
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(1000, stoppingToken);
        }
    }
}

The ExecuteAsync method won't get called anymore because the code is stuck in the while loop in the StartAsync method. When I comment out the whole overriding StartAsync method everything works fine, the ExecuteAsync method gets called.

Is it possible to get rid of the blocking while loop and make use of event handlers or something like that?

Upvotes: 0

Views: 990

Answers (1)

Andy
Andy

Reputation: 13517

A BackgroundService's typical pattern it to simply override ExecuteAsync and wait for the stoppingToken to signal and then exit immediately.

Take a look at the source code for BackgroundService.

If you want to use a "start/stop" pattern, then implement IHostedService.

If you want to use BackgroundService, then it should be laid out as so:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    TcpListener tcpListener = new TcpListener(IPAddress.Parse("127.0.0.1"), 1234);
    tcpListener.Start();
    
    try
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            TcpClient tcpClient = await tcpListener.AcceptTcpClientAsync(); // READ BELOW ABOUT THIS
            NetworkStream tcpClientStream = tcpClient.GetStream();
            using StreamReader streamReader = new StreamReader(tcpClientStream);
            string messageText = await streamReader.ReadToEndAsync();
    
            // ... do things with messageText ...
        }
    }
    catch (Exception exception)
    {
        // ... error handling ...
    }
}

Now, the biggest problem here is that AccpetTcpClientAsync doesn't have cancellation capabilities. So, you may have to implement something like this.

So then you could do:

var tcpClient = await tcpListener.AcceptTcpClientAsync().WithCancellation(stoppingToken);

And just swallow the OperationCancelledExeption that would be thrown in that case.

Edited To Add

Another option is that you could skip the async/await versions all together and use the Begin../End... (BeginAcceptTcpClient(), in this case). I tend to go down that path a lot. It's very convenient to have an event fire when something connects. Keep in mind your TcpListener would need to be on the class scope at that point so it doesn't dispose.

Upvotes: 2

Related Questions