可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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());
回答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());
回答3:
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;
}
}
回答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 :)
回答5:
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.
回答6:
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!
回答7:
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
}
}
}