Bastiflew
Bastiflew

Reputation: 1166

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

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.

Upvotes: 23

Views: 30887

Answers (8)

Zoe
Zoe

Reputation: 11

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

Upvotes: 0

Robert Važan
Robert Važan

Reputation: 3437

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

Upvotes: 5

jjxtra
jjxtra

Reputation: 21100

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

Upvotes: 0

Ray
Ray

Reputation: 8834

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!

Upvotes: 0

porges
porges

Reputation: 30580

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

Upvotes: 11

Ronnie Overby
Ronnie Overby

Reputation: 46460

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

Upvotes: 2

usr
usr

Reputation: 171168

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.

Upvotes: 3

Asaf
Asaf

Reputation: 4407

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

Upvotes: 4

Related Questions