Правильный подход для асинхронного TcpListener с использованием async/wait

Я думал о том, как правильно настроить TCP-сервер, используя асинхронное программирование.

Обычно я создавал поток для каждого входящего запроса, но я хотел бы использовать большую часть ThreadPool, поэтому, когда соединения простаивают, потоки заблокированы.

Сначала я бы создал слушателя и начал принимать клиентов в этом случае в приложении консоли:

static void Main(string[] args)
{
    CancellationTokenSource cancellation = new CancellationTokenSource();
    var endpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8001);
    TcpListener server = new TcpListener(endpoint); 

    server.Start();
    var task = AcceptTcpClients(server, cancellation.Token);

    Console.ReadKey(true);
    cancellation.Cancel();
    await task;
    Console.ReadKey(true);
}

В этом методе я получал бы цикл, принимающий входящие запросы, и создавал новую задачу для обработки соединения, поэтому цикл может вернуться, чтобы принять больше клиентов:

static async Task AcceptTcpClients(TcpListener server, CancellationToken token)
{
    while (!token.IsCancellationRequested)
    {
        var ws = await server.AcceptTcpClientAsync();

        Task.Factory.StartNew(async () =>
        {
            while (ws.IsConnected && !token.IsCancellationRequested)
            {
                String msg = await ws.ReadAsync();
                if (msg != null)
                    await ws.WriteAsync(ProcessResponse(msg));
            }
        }, token);
    }
 }

Создание новой задачи необязательно означает новый поток, но правильно ли это? Я пользуюсь ThreadPool или есть что-нибудь еще, что я могу сделать?

Есть ли потенциальная ошибка в этом подходе?

Ответ 1

await task; в вашем Main не будет компилироваться; вам нужно будет использовать task.Wait(); если вы хотите заблокировать его.

Кроме того, вы должны использовать Task.Run вместо Task.Factory.StartNew в асинхронном программировании.

Создание новой задачи необязательно означает новый поток, но правильно ли это?

Вы, безусловно, можете запускать отдельные задачи (используя Task.Run). Хотя вам и не нужно. Вы также можете просто вызвать отдельный метод async для обработки отдельных соединений сокетов.

Однако есть несколько проблем с вашей фактической обработкой сокетов. Свойство Connected практически бесполезно. Вы всегда должны постоянно читать из подключенного сокета, даже когда вы пишете на него. Кроме того, вы должны писать сообщения "keepalive" или иметь тайм-аут в своих чтениях, чтобы вы могли обнаруживать ситуации с половинным открытием. Я поддерживаю TCP/IP.NET FAQ, который объясняет эти общие проблемы.

Я действительно настоятельно рекомендую, чтобы люди не писали TCP/IP-серверы или клиенты. Есть тонны подводных камней. Было бы намного лучше, если это возможно, самостоятельно принимать WebAPI и/или SignalR.

Ответ 2

Чтобы прекратить прием приемной циклы грамотно, я регистрирую обратный вызов, который перестает слушать, когда отменяет cancellationToken.Register(listener.Stop);).

Это вызовет ObjectDisposedException в await listener.AcceptTcpClientAsync(); что легко захватывать.

Нет необходимости в Task.Run(HandleClient()), потому что вызов метода async возвращает задачу, которая выполняется параллельно.

    public async Task Run(CancellationToken cancellationToken)
    {
        TcpListener listener = new TcpListener(address, port);
        listener.Start();
        cancellationToken.Register(listener.Stop);
        while (!cancellationToken.IsCancellationRequested)
        {
            try
            {
                TcpClient client = await listener.AcceptTcpClientAsync();
                var clientTask = protocol.HandleClient(client, cancellationToken)
                    .ContinueWith((antecedent) => client.Dispose())
                    .ContinueWith((antecedent)=> logger.LogInformation("Client disposed."));
            }
            catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested)
            {
                logger.LogInformation("TcpListener stopped listening because cancellation was requested.");
            }
            catch (Exception ex)
            {
                logger.LogError(new EventId(), ex, $"Error handling client: {ex.Message}");
            }
        }
    }