Reputation: 1166
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
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
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
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
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(); }
}
Stop()
on the TcpListener
instance when the CancellationToken
gets canceled.AcceptSocketAsync
typically immediately throws an ObjectDisposedException
then.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
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
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
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
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