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

前端 未结 7 737
醉话见心
醉话见心 2020-12-29 07:14

I don\'t know how to properly close a TcpListener while an async method await for incoming connections. I found this code on SO, here the code :

public class         


        
相关标签:
7条回答
  • 2020-12-29 07:27

    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
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-29 07:35

    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.

    0 讨论(0)
  • 2020-12-29 07:37

    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;
        }
    }
    
    0 讨论(0)
  • 2020-12-29 07:39

    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());
    
    0 讨论(0)
  • 2020-12-29 07:45

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

    0 讨论(0)
  • 2020-12-29 07:46

    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!

    0 讨论(0)
提交回复
热议问题