问题 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。


10502
2017-10-07 09:16


起源

在SO上找到答案:[stackoverflow.com/questions/14524209/...        [1]: stackoverflow.com/questions/14524209/...    谢谢 - Baptiste
为什么不使用try {}来捕获异常? - Martin Meeser


答案:


虽然有一个公平的 复杂的解决方 基于a 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());

5
2018-04-15 18:47



这会泄漏套接字,异步调用并永远使用端口。 - usr
是的它无处不在,但你必须打电话给Stop。它不是或者。它是停止或停止和CT。永远停下来然后 将 即使CT必须处理,也会导致异常。 - usr
@usr实际上,CancellationToken不会导致任何套接字异常,至少不会在接受循环中。循环将以TaskCanceledException退出,这比从另一个线程异步调用TcpListener.Stop()时抛出的未记录的(并且可能是模糊的)异常更容易编程。此外,问题是几乎总是有比TcpListener更多的清理(想想已经打开的连接)。 - Robert Važan
我认为你在答案中“提到”了这一点,但它仍然可能误解了人们 - 你的答案不起作用,一旦听众启动并聆听,这项任务就不能通过使用取消令牌取消。如果这是你已经知道的,我不明白为什么你仍然摆出它并使人混淆。 -1。 - KFL
@Salgat实际上,这将持续阻塞,直到下一次连接尝试,此时将观察到取消。人们可能想要注册取消操作并关闭那里的听众。 - Robert Važan


定义此扩展方法:

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

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

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

2
2017-07-30 17:08





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

假设你有两个范围 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;
    }
}

2
2017-11-01 06:05





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

// This is so the accept callback knows to not 
_Active = false;

TcpClient dummyClient = new TcpClient();
dummyClient.Connect(m_listener.LocalEndpoint as IPEndPoint);
dummyClient.Close();

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


1
2018-01-11 17:00



这是一个有趣的黑客。如果侦听器不侦听环回接口,则不起作用。 - usr


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


0
2018-01-11 17:03





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

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() 在...上 TcpListener 实例的时候 CancellationToken 被取消了。
  • AcceptSocketAsync 通常会立即抛出一个 ObjectDisposedException 然后。
  • 我抓住了 Exception 以外 OperationCanceledException 虽然要“理智” OperationCanceledException 到外面的来电者。

我很新 async 编程,对不起,如果这个方法有问题 - 我很高兴看到它指出要从中学习!


0
2018-01-27 22:29





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

请参阅以下代码:

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

0
2017-08-03 19:44