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.

Ответ 1

Хотя существует довольно сложное решение на основе блога Стивена Тууба, существует гораздо более простое решение с использованием встроенных .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

Работал для меня: Создайте локальный фиктивный клиент для подключения к слушателю, и после того, как соединение будет принято, просто не выполняйте другой асинхронный прием (используйте активный флаг).

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

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

Это может быть взлом, но он кажется более красивым, чем другие варианты:)

Ответ 3

Поскольку здесь нет надлежащего рабочего примера, вот один из них:

Предполагая, что у вас есть в области 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;
    }
}

Ответ 4

Вызов StopListening (который размещает сокет) правильный. Просто проглотите эту конкретную ошибку. Вы не можете этого избежать, так как вам все равно нужно остановить ожидающий вызов. Если вы не протекаете сокет и ожидающий async IO, и порт остается в использовании.

Ответ 5

Определите этот метод расширения:

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

Ответ 6

Я использовал следующее решение при постоянном прослушивании новых подключающихся клиентов:

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, поэтому извините меня, если возникнет проблема с этим подходом - я был бы рад видеть, что он указал, чтобы учиться на нем!