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());
由于这里没有正确的工作示例,这里有一个:
假设您在范围内同时包含cancellationToken
和tcpListener
,那么您可以执行以下操作:
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 } } }