Как создать TcpListener, который будет ожидать входящие запросы?

Здравствуйте! <br/> <br/> В рамках разработки моего <code>asp.net web api</code>, мне необходимо интегрировать <code>Tcp</code> сервер, который будет функционировать в фоновом режиме как <code>BackgroundService</code>. Проблем с самим сервисом нет, однако меня интересует, как правильно создать <code>Tcp</code> слушатель, который будет находиться в режиме ожидания, подобно тем эндпоинтам контроллеров, что используются в <code>web api</code>. <br/> <br/> Я не удовлетворен следующими подходами: <br/> <pre><code class="cs">while (!stoppingToken.IsCancellationRequested)
        {
            Console.WriteLine("Waiting for a connection...");
            TcpClient client = await _listener.AcceptTcpClientAsync();

            if (client != null)
            {
                Console.WriteLine("Connected!");
                _ = HandleClientAsync(client, stoppingToken);
            }
        }</code></pre> <br/> <br/> и <br/> <pre><code class="cs">private async void AcceptClientAsync()
    {
        try
        {
            TcpClient client = await _listener.AcceptTcpClientAsync();
            Console.WriteLine("Connected!");

            _ = HandleClientAsync(client);

            AcceptClientAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Exception: {ex.Message}");
        }
    }</code></pre> <br/> <br/> Можно ли предложить более эффективные альтернативы для реализации <code>TcpListener</code> в режиме ожидания?
  • 27 августа 2024 г. 20:14
Ответы на вопрос 2
Для интеграции `TcpListener` в фоновый сервис `BackgroundService` в ASP.NET Web API вы можете использовать асинхронные методы, которые позволяют лучше управлять потоками и более эффективно обрабатывать входящие подключения. Ваши примеры кода в целом правильны, но их можно оптимизировать и сделать более читаемыми.

Вот улучшенный пример, как можно реализовать `TcpListener` с использованием `BackgroundService`. Обратите внимание, что мы будем использовать цикл обработки клиентов, при этом будем следить за флагом отмены (`cancellationToken`):

```csharp
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public class TcpServerService : BackgroundService
{
    private readonly ILogger<TcpServerService> _logger;
    private TcpListener _listener;

    public TcpServerService(ILogger<TcpServerService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _listener = new TcpListener(IPAddress.Any, 4000); // Используйте необходимый порт
        _listener.Start();
        _logger.LogInformation("TCP Server started, waiting for connections...");

        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var client = await _listener.AcceptTcpClientAsync(stoppingToken);
                _logger.LogInformation("Client connected.");

                _ = HandleClientAsync(client, stoppingToken);
            }
        }
        catch (SocketException socketEx)
        {
            _logger.LogError("SocketException: {Message}", socketEx.Message);
        }
        catch (Exception ex)
        {
            _logger.LogError("General Exception: {Message}", ex.Message);
        }
        finally
        {
            _listener.Stop();
            _logger.LogInformation("TCP Server stopped.");
        }
    }

    private async Task HandleClientAsync(TcpClient client, CancellationToken stoppingToken)
    {
        using (client)
        {
            var buffer = new byte[1024];
            var stream = client.GetStream();
            int bytesRead;

            try
            {
                while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, stoppingToken)) != 0)
                {
                    var message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                    _logger.LogInformation($"Received message: {message}");

                    // Здесь можно добавить логику обработки сообщения, например, отправить ответ
                    var response = $"Echo: {message}";
                    var data = Encoding.UTF8.GetBytes(response);
                    await stream.WriteAsync(data, 0, data.Length, stoppingToken);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError("Exception while handling client: {Message}", ex.Message);
            }
        }
    }
}
```

### Объяснение кода:

1. **TcpListener**: Мы создаем `TcpListener`, который будет слушать заданный порт.
  
2. **Непрерывное ожидание подключений**: Внутри метода `ExecuteAsync` мы запускаем бесконечный цикл, который ожидает подключения. Он использует `AcceptTcpClientAsync` с передачей токена отмены для правильного выхода из цикла в случае отмены сервиса.

3. **Обработка клиентов**: Для каждого подключенного клиента мы вызываем метод `HandleClientAsync`, который получает данные и может обрабатывать их. Это позволяет освободить основной поток для ожидания новых подключений.

4. **Логирование**: Включено логирование для отслеживания событий и возможных ошибок, что очень полезно при отладке.

### Заключение

Данный подход делает ваш TCP сервер более устойчивым и читаемым и предотвращает проблемы, связанные с завершением работы при получении исключений. Этот сервис будет работать в фоновом режиме и обрабатывать клиентские соединения асинхронно, не блокируя основное приложение.
Зависит от того, чего вы хотите. 
Если вас устраивает, что задача, выполняющая HandleClientAsync для подключившегося клиента будет брошена на произвол судьбы, можно использовать оба варианта (но первый мне нравится больше). Если вы хотите узнать, чем она закончилась - надо делать по-друому.
PS См. также комментарии к вопросу.
Похожие вопросы