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.

2

8 Answers 8

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; } } 
Sign up to request clarification or add additional context in comments.

2 Comments

I get System.ObjectDisposedException at tcpListener.AcceptTcpClientAsync() because it is in a while loop. So it's not a good enough solution.
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.
5

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

11 Comments

This leaks the socket, the async call and leaves the port in use forever.
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 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).
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.
@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.
|
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 :)

2 Comments

This is a funny hack. Doesn't work if the listener does not listen on the loopback interface.
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
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.

Comments

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

Comments

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!

Comments

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

Comments

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 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.