web-dev-qa-db-ja.com

async / awaitを使用した非同期TcpListenerの正しいアプローチ

非同期プログラミングを使用して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を利用していますか、それとも他に何かできることはありますか?

このアプローチには潜在的な落とし穴はありますか?

20
vtortola

Mainの_await task;_はコンパイルされません。ブロックする場合は、task.Wait();を使用する必要があります。

また、非同期プログラミングでは_Task.Run_ではなく_Task.Factory.StartNew_を使用する必要があります。

新しいタスクの作成は必ずしも新しいスレッドを意味するわけではありませんが、これは正しい方法ですか?

あなたは確かにcan別のタスクを開始します(_Task.Run_を使用)。 持っているしませんが。個別のasyncメソッドを呼び出すだけで、個々のソケット接続を処理できます。

ただし、実際のソケット処理にはいくつかの問題があります。 Connectedプロパティは実質的に役に立たない。接続しているソケットに書き込んでいる間でも、常に接続しているソケットから継続的に読み取る必要があります。また、ハーフオープンの状況を検出できるように、「キープアライブ」メッセージを書き込むか、読み取りにタイムアウトを設定する必要があります。これらの一般的な問題を説明する TCP/IP .NET FAQ を維持しています。

私は本当に、TCP/IPサーバーまたはクライアントを作成しないように強く推奨します。 tonsの落とし穴があります。可能であれば、セルフホストのWebAPIやSignalRを使用する方がはるかに優れています。

17
Stephen Cleary

サーバー受け入れループを正常に停止するために、cancelationTokenがキャンセルされたときにリッスンを停止するコールバックを登録します(cancellationToken.Register(listener.Stop);)。

これにより、キャプチャが簡単なawait listener.AcceptTcpClientAsync();でObjectDisposedExceptionがスローされます。

Asyncメソッドを呼び出すと、並行して実行されているタスクが返されるため、Task.Run(HandleClient())は必要ありません。

    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}");
            }
        }
    }
4
aschoenebeck