TcpListener: how to stop listening while awaiting AcceptTcpClientAsync()?

依然范特西╮ 提交于 2019-12-03 02:04:56

While there is a fairly complicated solution based on a blog post by Stephen Toub, there's much simpler solution using builtin .NET APIs:

var cancellation = new CancellationTokenSource();
await Task.Run(() => listener.AcceptTcpClientAsync(), cancellation.Token);

// somewhere in another thread
cancellation.Cancel();

This solution won't kill the pending accept call. But the other solutions don't do that either and this solution is at least shorter.

Update: A more complete example that shows what should happen after the cancellation is signaled:

var cancellation = new CancellationTokenSource();
var listener = new TcpListener(IPAddress.Any, 5555);
listener.Start();
try
{
    while (true)
    {
        var client = await Task.Run(
            () => listener.AcceptTcpClientAsync(),
            cancellation.Token);
        // use the client, pass CancellationToken to other blocking methods too
    }
}
finally
{
    listener.Stop();
}

// somewhere in another thread
cancellation.Cancel();

Update 2: Task.Run only checks the cancellation token when the task starts. To speed up termination of the accept loop, you might wish to register cancellation action:

cancellation.Token.Register(() => listener.Stop());

Since there's no proper working example here, here is one:

Assuming you have in scope both cancellationToken and tcpListener, then you can do the following:

using (cancellationToken.Register(() => tcpListener.Stop()))
{
    try
    {
        var tcpClient = await tcpListener.AcceptTcpClientAsync();
        // … carry on …
    }
    catch (InvalidOperationException)
    {
        // Either tcpListener.Start wasn't called (a bug!)
        // or the CancellationToken was cancelled before
        // we started accepting (giving an InvalidOperationException),
        // or the CancellationToken was cancelled after
        // we started accepting (giving an ObjectDisposedException).
        //
        // In the latter two cases we should surface the cancellation
        // exception, or otherwise rethrow the original exception.
        cancellationToken.ThrowIfCancellationRequested();
        throw;
    }
}

Worked for me: Create a local dummy client to connect to the listener, and after the connection gets accepted just don't do another async accept (use the active flag).

// This is so the accept callback knows to not 
_Active = false;

TcpClient dummyClient = new TcpClient();
dummyClient.Connect(m_listener.LocalEndpoint as IPEndPoint);
dummyClient.Close();

This might be a hack, but it seems prettier than other options here :)

Define this extension method:

public static class Extensions
{
    public static async Task<TcpClient> AcceptTcpClientAsync(this TcpListener listener, CancellationToken token)
    {
        try
        {
            return await listener.AcceptTcpClientAsync();
        }
        catch (Exception ex) when (token.IsCancellationRequested) 
        { 
            throw new OperationCanceledException("Cancellation was requested while awaiting TCP client connection.", ex);
        }
    }
}

Before using the extension method to accept client connections, do this:

token.Register(() => listener.Stop());

Calling StopListening (which disposes the socket) is correct. Just swallow that particular error. You cannot avoid this since you somehow need to stop the pending call anyway. If not you leak the socket and the pending async IO and the port stays in use.

I used the following solution when continually listening for new connecting clients:

public async Task ListenAsync(IPEndPoint endPoint, CancellationToken cancellationToken)
{
    TcpListener listener = new TcpListener(endPoint);
    listener.Start();

    // Stop() typically makes AcceptSocketAsync() throw an ObjectDisposedException.
    cancellationToken.Register(() => listener.Stop());

    // Continually listen for new clients connecting.
    try
    {
        while (true)
        {
            cancellationToken.ThrowIfCancellationRequested();
            Socket clientSocket = await listener.AcceptSocketAsync();
        }
    }
    catch (OperationCanceledException) { throw; }
    catch (Exception) { cancellationToken.ThrowIfCancellationRequested(); }
}
  • I register a callback to call Stop() on the TcpListener instance when the CancellationToken gets canceled.
  • AcceptSocketAsync typically immediately throws an ObjectDisposedException then.
  • I catch any Exception other than OperationCanceledException though to throw a "sane" OperationCanceledException to the outer caller.

I'm pretty new to async programming, so excuse me if there's an issue with this approach - I'd be happy to see it pointed out to learn from it!

Cancel token has a delegate which you can use to stop the server. When the server is stopped, any listening connection calls will throw a socket exception.

See the following code:

public class TcpListenerWrapper
{
    // helper class would not be necessary if base.Active was public, c'mon Microsoft...
    private class TcpListenerActive : TcpListener, IDisposable
    {
        public TcpListenerActive(IPEndPoint localEP) : base(localEP) {}
        public TcpListenerActive(IPAddress localaddr, int port) : base(localaddr, port) {}
        public void Dispose() { Stop(); }
        public new bool Active => base.Active;
    }

    private TcpListenerActive server

    public async Task StartAsync(int port, CancellationToken token)
    {
        if (server != null)
        {
            server.Stop();
        }

        server = new TcpListenerActive(IPAddress.Any, port);
        server.Start(maxConnectionCount);
        token.Register(() => server.Stop());
        while (server.Active)
        {
            try
            {
                await ProcessConnection();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }

    private async Task ProcessConnection()
    {
        using (TcpClient client = await server.AcceptTcpClientAsync())
        {
            // handle connection
        }
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!