web-dev-qa-db-ja.com

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()を呼び出すと、オブジェクトが削除されます。

だから私の質問は、TcpListenerを正しく閉じるためにAcceptTcpClientAsync()をキャンセルするにはどうすればよいですか?.

16
Bastiflew

Stephen Toubによるブログ投稿 に基づくかなり 複雑なソリューション がありますが、組み込みの.NETAPIを使用したはるかに簡単なソリューションがあります。

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:Task.Runは、タスクの開始時にキャンセルトークンのみをチェックします。受け入れループの終了を高速化するには、キャンセルアクションを登録することをお勧めします。

cancellation.Token.Register(() => listener.Stop());
5
Robert Važan

ここには適切な実例がないため、次の例を示します。

スコープ内にcancellationTokentcpListenerの両方があるとすると、次のことができます。

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;
    }
}
3
porges

私のために働いた:リスナーに接続するためのローカルダミークライアントを作成し、接続が受け入れられた後、別の非同期受け入れを行わないでください(アクティブフラグを使用してください)。

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

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

これはハックかもしれませんが、ここの他のオプションよりもきれいに見えます:)

2
Asaf

この拡張メソッドを定義します。

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());
2
Ronnie Overby

StopListening(ソケットを破棄する)の呼び出しは正しいです。その特定のエラーを飲み込むだけです。とにかく保留中の通話を停止する必要があるため、これを回避することはできません。そうでない場合は、ソケットと保留中の非同期IOをリークし、ポートは使用されたままになります。

0
usr

新しい接続クライアントを継続的にリッスンするときに、次のソリューションを使用しました。

_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(); }
}
_
  • TcpListenerがキャンセルされたときに、CancellationTokenインスタンスでStop()を呼び出すコールバックを登録します。
  • AcceptSocketAsyncは通常、すぐにObjectDisposedExceptionをスローします。
  • Exception以外のOperationCanceledExceptionをキャッチしましたが、外部の呼び出し元に「正気の」OperationCanceledExceptionをスローしました。

私はasyncプログラミングにかなり慣れていないので、このアプローチに問題がある場合はすみません。それから学ぶことが指摘されているのを見てうれしいです。

0
Ray

キャンセルトークンには、サーバーを停止するために使用できるデリゲートがあります。サーバーが停止すると、リスニング接続呼び出しはソケット例外をスローします。

次のコードを参照してください。

public class TcpListenerWrapper
{
    // helper class would not be necessary if base.Active was public, c'mon Microsoft...
    private class TcpListenerActive : TcpListener, IDisposable
    {
        public TcpListenerActive(IPEndPoint localEP) : base(localEP) {}
        public TcpListenerActive(IPAddress localaddr, int port) : base(localaddr, port) {}
        public void Dispose() { Stop(); }
        public new bool Active => base.Active;
    }

    private TcpListenerActive server

    public async Task StartAsync(int port, CancellationToken token)
    {
        if (server != null)
        {
            server.Stop();
        }

        server = new TcpListenerActive(IPAddress.Any, port);
        server.Start(maxConnectionCount);
        token.Register(() => server.Stop());
        while (server.Active)
        {
            try
            {
                await ProcessConnection();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }

    private async Task ProcessConnection()
    {
        using (TcpClient client = await server.AcceptTcpClientAsync())
        {
            // handle connection
        }
    }
}
0
jjxtra