TcpListener:如何在等待AcceptTcpClientAsync()时停止侦听?

我不知道如何在异步方法等待传入连接时正确关闭TcpListener。 我在SO上找到了这个代码,这里是代码:

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) { // ... } } 

主要:

  static void Main(string[] args) { var server = new Server(); server.StartListening(); Thread.Sleep(5000); server.StopListening(); Console.Read(); } 

这条线上有一个例外

  await AcceptConnections(); 

当我调用Server.StopListening()时,该对象被删除。

所以我的问题是,如何取消AcceptTcpClientAsync()以正确关闭TcpListener。

虽然基于Stephen Toub的博客文章有一个相当复杂的解决方案 ,但使用内置.NET API的解决方案更为简单:

 var cancellation = new CancellationTokenSource(); await Task.Run(() => listener.AcceptTcpClientAsync(), cancellation.Token); // somewhere in another thread cancellation.Cancel(); 

此解决方案不会终止待处理的接受呼叫。 但其他解决方案也没有这样做,这个解决方案至少更短。

更新:一个更完整的示例,显示取消后应发生的情况:

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

更新2: Task.Run仅在任务启动时检查取消令牌。 要加速终止接受循环,您可能希望注册取消操作:

 cancellation.Token.Register(() => listener.Stop()); 

定义此扩展方法:

 public static class Extensions { public static async Task 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); } } } 

在使用扩展方法接受客户端连接之前,请执行以下操作:

 token.Register(() => listener.Stop()); 

由于这里没有正确的工作示例,这里有一个:

假设您在范围内同时包含cancellationTokentcpListener ,那么您可以执行以下操作:

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

为我工作:创建一个本地虚拟客户端连接到监听器,并在接受连接后,不要再做另一个异步接受(使用活动标志)。

 // This is so the accept callback knows to not _Active = false; TcpClient dummyClient = new TcpClient(); dummyClient.Connect(m_listener.LocalEndpoint as IPEndPoint); dummyClient.Close(); 

这可能是一个黑客,但它似乎比其他选项更漂亮:)

调用StopListening (配置套接字)是正确的。 只是吞下那个特别的错误。 你无法避免这种情况,因为无论如何你都需要停止待处理的呼叫。 如果没有,则泄漏套接字并且挂起的异步IO和端口保持使用状态。

在不断收听新的连接客户端时,我使用了以下解决方案:

 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(); } } 
  • CancellationToken被取消时,我注册了一个回调来调用TcpListener实例上的Stop()
  • AcceptSocketAsync通常会立即抛出ObjectDisposedException
  • 我捕获除OperationCanceledException之外的任何Exception但是向外部调用者抛出“理智的” OperationCanceledException

我对async编程很新,所以请原谅我这个方法有问题 – 我很高兴看到它指出要从中学习!

取消令牌有一个委托,您可以使用该委托来停止服务器。 当服务器停止时,任何侦听连接调用都将引发套接字exception。

请参阅以下代码:

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