23

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 Server
{
    private TcpListener _Server;
    private bool _Active;

    public Server()
    {
        _Server = new TcpListener(IPAddress.Any, 5555);
    }

    public async void StartListening()
    {
        _Active = true;
        _Server.Start();
        await AcceptConnections();
    }

    public void StopListening()
    {
        _Active = false;
        _Server.Stop();
    }

    private async Task AcceptConnections()
    {
        while (_Active)
        {
            var client = await _Server.AcceptTcpClientAsync();
            DoStuffWithClient(client);
        }
    }

    private void DoStuffWithClient(TcpClient client)
    {
        // ...
    }

}

And the Main :

    static void Main(string[] args)
    {
        var server = new Server();
        server.StartListening();

        Thread.Sleep(5000);

        server.StopListening();
        Console.Read();
    }

An exception is throwed on this line

        await AcceptConnections();

when I call Server.StopListening(), the object is deleted.

So my question is, how can I cancel AcceptTcpClientAsync() for closing TcpListener properly.

porges
  • 30,133
  • 4
  • 83
  • 114
Bastiflew
  • 1,136
  • 3
  • 18
  • 31
  • 1
    found an answer on SO : [http://stackoverflow.com/questions/14524209/what-is-the-correct-way-to-cancel-an-async-operation-that-doesnt-accept-a-cance][1] [1]: http://stackoverflow.com/questions/14524209/what-is-the-correct-way-to-cancel-an-async-operation-that-doesnt-accept-a-cance Thanks – Bastiflew Oct 07 '13 at 09:33
  • Why don't you use a try {} to catch the exception? – Martin Meeser Oct 13 '14 at 09:26

8 Answers8

11

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;
    }
}
porges
  • 30,133
  • 4
  • 83
  • 114
  • I get `System.ObjectDisposedException` at `tcpListener.AcceptTcpClientAsync()` because it is in a while loop. So it's not a good enough solution. – Nicolas VERHELST Dec 31 '21 at 03:32
  • This looks like the best solution. Instead of handling the OperationCanceledException exception on a blocked method, you handle a ObjectDisposedException from the blocking AcceptTcpClientAsync method. You'll have to create a new TcpListener each time it's cancelled. – C.M. Apr 22 '22 at 20:51
6

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());
Wiso86
  • 28
  • 1
  • 4
Robert Važan
  • 3,399
  • 2
  • 25
  • 31
  • 7
    This leaks the socket, the async call and leaves the port in use forever. – usr Jan 11 '15 at 17:05
  • @usr It is assumed that the accept loop subsequently exits and some finally block cleans up everything by calling TcpListener.Stop(). – Robert Važan Jan 11 '15 at 20:51
  • No need for this. Just call Stop and all activity will cease. As you said Stop needs to be called anyway sooner or later. – usr Jan 11 '15 at 20:54
  • @usr You can pass around CancellationToken and call Cancel() or you can pass around TcpListener and call Stop(). CancellationToken approach has two advantages: (1) CancellationToken is already supported in many library interfaces and (2) you can attach other cleanup actions to CancellationToken besides the TcpListener. – Robert Važan Jan 11 '15 at 20:58
  • 2
    Yeah it's ubiquitous but you must call Stop anyway. It's not either or. It's Stop or Stop and CT. Always stop. And that *will* cause an exception even with CT that must be dealt with. – usr Jan 11 '15 at 20:59
  • 1
    @usr Actually, CancellationToken won't cause any socket exception, at least not in the accept loop. The loop will exit with TaskCanceledException, which is easier to program against than the undocumented (and possibly ambiguous) exception thrown when TcpListener.Stop() is called asynchronously from another thread. Also the problem is that almost always there is more to clean up than the TcpListener (think the already open connections). – Robert Važan Jan 11 '15 at 21:03
  • 1
    I think you "mentioned" this in the answer but it can still misunderstand people - your answer does NOT work in the sense that once the listener is up and listening, this task CANNOT be cancelled by using the cancellation token. If this is what you already know, I don't understand why you still pose it and confuse people. -1. – KFL May 27 '16 at 01:00
  • Pretty sure this won't work since Task.Run only checks the cancellation token on task creation, while you are responsible for checking the token inside once Task.Run has already started. – Austin Salgat Mar 22 '18 at 02:18
  • 1
    @Salgat Indeed, this will keep blocking until the next connection attempt at which point the cancellation will be observed. One would probably want to register cancellation action and close the listener there. – Robert Važan Mar 23 '18 at 07:19
  • From what I see, the `Register` method is on the `Token` itself and not the `CancellationTokenSource` but your update puts in on the latter. – Peter M May 25 '18 at 13:49
  • @PeterM Thanks. I wish SO was able to compile the sample code and report such errors. – Robert Važan May 26 '18 at 14:46
4

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

Asaf
  • 4,317
  • 28
  • 48
  • This is a funny hack. Doesn't work if the listener does not listen on the loopback interface. – usr Jan 11 '15 at 17:04
  • good enough (and so simple) for me, just you should do a Application.DoEvents() after the Connect() to give the hand to terminate the AcceptTcpClientAsync() in some case – Fabien Oct 14 '20 at 07:40
3

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.

usr
  • 168,620
  • 35
  • 240
  • 369
2

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());
Ronnie Overby
  • 45,287
  • 73
  • 267
  • 346
0

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!

Ray
  • 7,940
  • 7
  • 58
  • 90
0

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
        }
    }
}
jjxtra
  • 20,415
  • 16
  • 100
  • 140
0

https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.beginaccept?view=net-5.0

To cancel a pending call to the BeginAccept method, close the Socket. When the Close method is called while an asynchronous operation is in progress, the callback provided to the BeginAccept method is called. A subsequent call to the EndAccept method will throw an ObjectDisposedException to indicate that the operation has been cancelled.

Here the TcpListner.cs decompiled.

    [HostProtection(SecurityAction.LinkDemand, ExternalThreading = true)]
    public Task<TcpClient> AcceptTcpClientAsync()
    {
        return Task<TcpClient>.Factory.FromAsync(BeginAcceptTcpClient, EndAcceptTcpClient, null);
    }

    /// <summary>Asynchronously accepts an incoming connection attempt and creates a new <see cref="T:System.Net.Sockets.TcpClient" /> to handle remote host communication.</summary>
    /// <returns>A <see cref="T:System.Net.Sockets.TcpClient" />.</returns>
    /// <param name="asyncResult">An <see cref="T:System.IAsyncResult" /> returned by a call to the <see cref="M:System.Net.Sockets.TcpListener.BeginAcceptTcpClient(System.AsyncCallback,System.Object)" /> method.</param>
    /// <PermissionSet>
    ///   <IPermission class="System.Security.Permissions.EnvironmentPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true" />
    ///   <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true" />
    ///   <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="UnmanagedCode, ControlEvidence" />
    ///   <IPermission class="System.Diagnostics.PerformanceCounterPermission, System, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true" />
    /// </PermissionSet>
    public TcpClient EndAcceptTcpClient(IAsyncResult asyncResult)
    {
        if (Logging.On)
        {
            Logging.Enter(Logging.Sockets, this, "EndAcceptTcpClient", null);
        }
        if (asyncResult == null)
        {
            throw new ArgumentNullException("asyncResult");
        }
        LazyAsyncResult lazyResult = asyncResult as LazyAsyncResult;
        Socket asyncSocket = (lazyResult == null) ? null : (lazyResult.AsyncObject as Socket);
        if (asyncSocket == null)
        {
            throw new ArgumentException(SR.GetString("net_io_invalidasyncresult"), "asyncResult");
        }
        Socket socket = asyncSocket.EndAccept(asyncResult);
        if (Logging.On)
        {
            Logging.Exit(Logging.Sockets, this, "EndAcceptTcpClient", socket);
        }
        return new TcpClient(socket);
    }

    /// <summary>Begins an asynchronous operation to accept an incoming connection attempt.</summary>
    /// <returns>An <see cref="T:System.IAsyncResult" /> that references the asynchronous creation of the <see cref="T:System.Net.Sockets.TcpClient" />.</returns>
    /// <param name="callback">An <see cref="T:System.AsyncCallback" /> delegate that references the method to invoke when the operation is complete.</param>
    /// <param name="state">A user-defined object containing information about the accept operation. This object is passed to the <paramref name="callback" /> delegate when the operation is complete.</param>
    /// <exception cref="T:System.Net.Sockets.SocketException">An error occurred while attempting to access the socket. See the Remarks section for more information. </exception>
    /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.Net.Sockets.Socket" /> has been closed. </exception>
    /// <PermissionSet>
    ///   <IPermission class="System.Security.Permissions.EnvironmentPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true" />
    ///   <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true" />
    ///   <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="UnmanagedCode, ControlEvidence" />
    ///   <IPermission class="System.Diagnostics.PerformanceCounterPermission, System, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true" />
    /// </PermissionSet>
    [HostProtection(SecurityAction.LinkDemand, ExternalThreading = true)]
    public IAsyncResult BeginAcceptTcpClient(AsyncCallback callback, object state)
    {
        if (Logging.On)
        {
            Logging.Enter(Logging.Sockets, this, "BeginAcceptTcpClient", null);
        }
        if (!m_Active)
        {
            throw new InvalidOperationException(SR.GetString("net_stopped"));
        }
        IAsyncResult result = m_ServerSocket.BeginAccept(callback, state);
        if (Logging.On)
        {
            Logging.Exit(Logging.Sockets, this, "BeginAcceptTcpClient", null);
        }
        return result;
    }

And Socket.cs decompiled.

    /// <summary>Asynchronously accepts an incoming connection attempt and creates a new <see cref="T:System.Net.Sockets.Socket" /> to handle remote host communication.</summary>
    /// <returns>A <see cref="T:System.Net.Sockets.Socket" /> to handle communication with the remote host.</returns>
    /// <param name="asyncResult">An <see cref="T:System.IAsyncResult" /> that stores state information for this asynchronous operation as well as any user defined data. </param>
    /// <exception cref="T:System.ArgumentNullException">
    ///   <paramref name="asyncResult" /> is null. </exception>
    /// <exception cref="T:System.ArgumentException">
    ///   <paramref name="asyncResult" /> was not created by a call to <see cref="M:System.Net.Sockets.Socket.BeginAccept(System.AsyncCallback,System.Object)" />. </exception>
    /// <exception cref="T:System.Net.Sockets.SocketException">An error occurred when attempting to access the socket. See the Remarks section for more information. </exception>
    /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.Net.Sockets.Socket" /> has been closed. </exception>
    /// <exception cref="T:System.InvalidOperationException">
    ///   <see cref="M:System.Net.Sockets.Socket.EndAccept(System.IAsyncResult)" /> method was previously called. </exception>
    /// <exception cref="T:System.NotSupportedException">Windows NT is required for this method. </exception>
    /// <PermissionSet>
    ///   <IPermission class="System.Security.Permissions.EnvironmentPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true" />
    ///   <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true" />
    ///   <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="UnmanagedCode, ControlEvidence" />
    ///   <IPermission class="System.Diagnostics.PerformanceCounterPermission, System, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true" />
    /// </PermissionSet>
    public Socket EndAccept(IAsyncResult asyncResult)
    {
        if (s_LoggingEnabled)
        {
            Logging.Enter(Logging.Sockets, this, "EndAccept", asyncResult);
        }
        if (CleanedUp)
        {
            throw new ObjectDisposedException(GetType().FullName);
        }
        byte[] buffer;
        int bytesTransferred;
        if (asyncResult != null && asyncResult is AcceptOverlappedAsyncResult)
        {
            return EndAccept(out buffer, out bytesTransferred, asyncResult);
        }
        if (asyncResult == null)
        {
            throw new ArgumentNullException("asyncResult");
        }
        AcceptAsyncResult castedAsyncResult = asyncResult as AcceptAsyncResult;
        if (castedAsyncResult == null || castedAsyncResult.AsyncObject != this)
        {
            throw new ArgumentException(SR.GetString("net_io_invalidasyncresult"), "asyncResult");
        }
        if (castedAsyncResult.EndCalled)
        {
            throw new InvalidOperationException(SR.GetString("net_io_invalidendcall", "EndAccept"));
        }
        object result = castedAsyncResult.InternalWaitForCompletion();
        castedAsyncResult.EndCalled = true;
        Exception exception = result as Exception;
        if (exception != null)
        {
            throw exception;
        }
        if (castedAsyncResult.ErrorCode != 0)
        {
            SocketException socketException = new SocketException(castedAsyncResult.ErrorCode);
            UpdateStatusAfterSocketError(socketException);
            if (s_LoggingEnabled)
            {
                Logging.Exception(Logging.Sockets, this, "EndAccept", socketException);
            }
            throw socketException;
        }
        Socket acceptedSocket = (Socket)result;
        if (s_LoggingEnabled)
        {
            Logging.PrintInfo(Logging.Sockets, acceptedSocket, SR.GetString("net_log_socket_accepted", acceptedSocket.RemoteEndPoint, acceptedSocket.LocalEndPoint));
            Logging.Exit(Logging.Sockets, this, "EndAccept", result);
        }
        return acceptedSocket;
    }

It seems that AcceptTcpClientAsync() uses something like BeginAccept() and EndAccept() internally. In Socket.cs you can see if CleanedUp is true throw ObjectDisposedException, which means listening socket is closed. So closing listening socket causes AcceptTcpClientAsync() throw ObjectDisposedException.

namespace TestTcpListenStop {
    class Program {
        static TcpListener listner;

        static void Main(string[] args) {
            for (int i = 0; i < 100; ++i) {
                StartStopTest();
            }

            Console.ReadKey();
            return;
        }

        static void StartStopTest() {
            // start listner
            listner = new TcpListener(IPAddress.Any, 17000);
            listner.Start();

            // start accept
            Task tk = AcceptAsync();

            // do other things
            Task.Delay(1).Wait();

            // close listen socket
            listner.Stop();
            tk.Wait();
        
            return;
        }

        static async Task AcceptAsync() {
            Console.WriteLine("Accepting client...");

            TcpClient client;
            while (true) {
                try {
                    // Closing listen socket causes
                    // AcceptTcpClientAsync() throw ObjectDisposedException
                    client = await listner.AcceptTcpClientAsync().ConfigureAwait(false);
                    Console.WriteLine("A client has been accepted.");
                }
                catch (ObjectDisposedException) {
                    Console.WriteLine("This exception means listening socket closed.");
                    break;
                }

                // we just close.
                client.Client.Shutdown(SocketShutdown.Both);
                client.Close();
            }

            Console.WriteLine("AcceptAsync() terminated.");
        }
    }
}

https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.wait?view=net-5.0

Canceling the cancellationToken cancellation token has no effect on the running task unless it has also been passed the cancellation token and is prepared to handle cancellation. Passing the cancellationToken object to this method simply allows the wait to be canceled.

And I think using cancellation token doesn't actually stop AcceptTcpClientAsync(). We just cancel waiting, not AcceptTcpClientAsync() because AcceptTcpClientAsync() doesn't receive cancellation token as a parameter. Only closing listening socket can cancel AcceptTcpClientAsync(). Please see the following from msdn.

public class Example {
    public static void Main() {
        CancellationTokenSource ts = new CancellationTokenSource();

        Task t = Task.Run(() => {
            Console.WriteLine("Calling Cancel...");
            ts.Cancel();
            Task.Delay(5000).Wait();
            Console.WriteLine("Task ended delay...");
        });
        try {
            Console.WriteLine("About to wait for the task to complete...");
            t.Wait(ts.Token);
        }
        catch (OperationCanceledException e) {
            Console.WriteLine("{0}: The wait has been canceled. Task status: {1:G}",
                                e.GetType().Name, t.Status);
            Thread.Sleep(6000);
            Console.WriteLine("After sleeping, the task status:  {0:G}", t.Status);
        }
        ts.Dispose();
    }
}
// The example displays output like the following:
//    About to wait for the task to complete...
//    Calling Cancel...
//    OperationCanceledException: The wait has been canceled. Task status: Running
//    Task ended delay...
//    After sleeping, the task status:  RanToCompletion
Joe
  • 11
  • 1