C# TcpListener. Как определить момент отключения клиента?

Есть BackgroundService, который запускает прослушивание tcp-порта. Код сервиса:

public sealed class TcpListenerWrapper : ITcpListenerWrapper
{
    private readonly TcpListener _listener;
    private readonly IMessageManager _messageManager;

    public TcpListenerWrapper(ITcpListenerWrapperConfiguration config, IMessageManager messageManager)
    {
        _listener = new TcpListener(IPAddress.Any, config.PortNumber);
        _messageManager = messageManager;
    }

    public async Task StartListeningAsync(CancellationToken stoppingToken)
    {
        _listener.Start();

        while (!stoppingToken.IsCancellationRequested)
        {
            var client = await _listener.AcceptTcpClientAsync();
            Console.WriteLine("A new client connected!");

            _ = Task.Run(() => ProcessClientAsync(client, stoppingToken), stoppingToken);
        }
    }

    private async Task ProcessClientAsync(TcpClient client, CancellationToken stoppingToken)
    {
        var stream = client.GetStream();

        while (client.Connected && !stoppingToken.IsCancellationRequested)
        {
            var packet = await ReceiveFixedPacketAsync(stream, stoppingToken);

            var messageAsText = Encoding.ASCII.GetString(packet);
            Console.WriteLine($"New message received: {messageAsText}");

            var message = _messageManager.CreateMessage(packet);
            await message.SaveAsync();
            Console.WriteLine("Saving ends");
        }

        var info = !client.Connected
            ? "Remote client disconnected. Client message processing ends"
            : "Service shutting down. Disconnecting...";

        Console.WriteLine(info);
        client.Close();
    }

    private static async Task<byte[]> ReceiveFixedPacketAsync(Stream stream, CancellationToken stoppingToken)
    {
        var buffer = new byte[SuntechProtocolInfo.MaxFullPacketLength];
        int bytesRead;
        do
        {
            bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, stoppingToken);
        } while (bytesRead <= 0);

        return buffer;
    }

    public void Dispose()
    {
        _listener.Stop();
    }
}

Ожидается, что в методе ProcessClientAsync после отключения клиента выполнится выход из цикла по условию client.Connected, но этого не происходит.

В качестве теста сделал себе еще 2 BackgroundService, которые отправляют данные слушателю. Код клиентов ниже:

BackgroundTcpSenderBase.cs

public abstract class BackgroundTcpSenderBase : BackgroundService
{
    private readonly ILogger _logger;
    private readonly TcpClient _client;
    private int _messageCounter = 0;
    protected abstract string DeviceName { get; }
    protected virtual double SendPeriodInMinutes { get; set; } = 1;

    protected BackgroundTcpSenderBase(ILogger logger)
    {
        _client = new TcpClient();
        _logger = logger;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        const int portNumber = 9000;
        var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), portNumber);

        _client.Connect(endPoint);

        var timer = new Timer(e =>
        {
            SendPackage();
        }, null, TimeSpan.Zero, TimeSpan.FromMinutes(SendPeriodInMinutes));

        return Task.CompletedTask;
    }

    public override void Dispose()
    {
        _client.GetStream().Close();
        _client.Close();
        base.Dispose();
    }

    private void SendPackage()
    {
        var stream = _client.GetStream();
        var message = $"Device {DeviceName} sends message #{++_messageCounter}";
        var data = Encoding.ASCII.GetBytes(message);

        stream.Write(data, 0, data.Length);
    }
}

BackgroundTcpSender1.cs

public class BackgroundTcpSender1 : BackgroundTcpSenderBase
{
    protected override string DeviceName { get; } = "Sender_1";

    public BackgroundTcpSender1(ILogger<BackgroundTcpSender1> logger) : base(logger)
    {
    }
}

BackgroundTcpSender2.cs

public class BackgroundTcpSender2 : BackgroundTcpSenderBase
{
    protected override string DeviceName { get; } = "Sender_2";
    protected override double SendPeriodInMinutes { get; set; } = 0.5;

    public BackgroundTcpSender2(ILogger<BackgroundTcpSender2> logger) : base(logger)
    {
    }
}

В дебаге выяснил, что при отключении клиента поток зависает в цикле метода ReceiveFixedPacketAsync:

    do
    {
        bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, stoppingToken);
    } while (bytesRead <= 0);

При этом Socket у переменной stream все еще доступен. Не могу понять, почему так.

Вопрос: как отловить момент отключения TCP-клиента?

UPD. Нашел ответ на англоязычном StackOverflow ссылка на оригинальный вопрос. Однако меня смущает параметр microseconds в функции Socket.Poll. К примеру, я выставлю 1000 микросекунд ожидание ответа. На localhost это прекрасно работает. Но что, если подключится удаленный клиент? И соединение с ним медленное. Не посчитает ли функция SocketConnected, что мой сокет отключился?


Ответы (1 шт):

Автор решения: Snaffu

Решал данную задачу через Poll

в цикле, где слушаете сообщения:

            if (tcpClient.Client.Poll(1000000, SelectMode.SelectRead)) // произошло изменение на сокете
            {
                if (tcpClient.GetStream().DataAvailable) // Есть какие-то данные
                {
                    // обработка запроса
                }
                else // Если нет, то произошел обрыв связи
                {
                    // обработка отключения
                }
             }
→ Ссылка