web-dev-qa-db-ja.com

AcceptTcpClient呼び出しのブロックをキャンセルします

誰もがすでに知っているかもしれませんが、C#で着信TCP接続を受け入れる最も簡単な方法は、TcpListener.AcceptTcpClient()をループすることです。さらに、この方法は、接続が取得されるまでコードの実行をブロックします。 GUIに非常に限定されているため、別のスレッドまたはタスクで接続をリッスンしたいと思います。

スレッドにはいくつかの欠点があると言われましたが、誰も私にこれらが何であるかを説明しませんでした。そのため、スレッドを使用する代わりに、タスクを使用しました。これはうまく機能しますが、AcceptTcpClientメソッドが実行をブロックしているため、タスクのキャンセルを処理する方法が見つかりません。

現在、コードは次のようになっていますが、プログラムで接続のリッスンを停止したいときに、タスクをキャンセルする方法がわかりません。

まず、タスクで実行される関数を実行します。

static void Listen () {
// Create listener object
TcpListener serverSocket = new TcpListener ( serverAddr, serverPort );

// Begin listening for connections
while ( true ) {
    try {
        serverSocket.Start ();
    } catch ( SocketException ) {
        MessageBox.Show ( "Another server is currently listening at port " + serverPort );
    }

    // Block and wait for incoming connection
    if ( serverSocket.Pending() ) {
        TcpClient serverClient = serverSocket.AcceptTcpClient ();
        // Retrieve data from network stream
        NetworkStream serverStream = serverClient.GetStream ();
        serverStream.Read ( data, 0, data.Length );
        string serverMsg = ascii.GetString ( data );
        MessageBox.Show ( "Message recieved: " + serverMsg );

        // Close stream and TcpClient connection
        serverClient.Close ();
        serverStream.Close ();

        // Empty buffer
        data = new Byte[256];
        serverMsg = null;
    }
}

次に、リスニングサービスを開始および停止する機能:

private void btnListen_Click (object sender, EventArgs e) {
    btnListen.Enabled = false;
    btnStop.Enabled = true;
    Task listenTask = new Task ( Listen );
    listenTask.Start();
}

private void btnStop_Click ( object sender, EventArgs e ) {
    btnListen.Enabled = true;
    btnStop.Enabled = false;
    //listenTask.Abort();
}

ListenTask.Abort()呼び出しを置き換えるものが必要です(メソッドが存在しないためコメントアウトしました)

14
user1641096

AcceptTcpClientのキャンセル

ブロッキングAcceptTcpClient操作をキャンセルするための最善の策は、 TcpListener.Stop を呼び出すことです。これにより、 SocketException がスローされ、明示的に確認したい場合にキャッチできます。操作はキャンセルされました。

_       TcpListener serverSocket = new TcpListener ( serverAddr, serverPort );

       ...

       try
       {
           TcpClient serverClient = serverSocket.AcceptTcpClient ();
           // do something
       }
       catch (SocketException e)
       {
           if ((e.SocketErrorCode == SocketError.Interrupted))
           // a blocking listen has been cancelled
       }

       ...

       // somewhere else your code will stop the blocking listen:
       serverSocket.Stop();
_

TcpListenerでStopを呼び出したい場合は、ある程度のアクセスが必要になるため、Listenメソッドの外部でスコープを設定するか、TcpListenerを管理し、StartメソッドとStopメソッドを公開するオブジェクト内でリスナーロジックをラップします(Stopを使用) TcpListener.Stop())を呼び出します。

非同期終了

受け入れられた回答はThread.Abort()を使用してスレッドを終了するため、非同期操作を終了する最善の方法は、ハードアボートではなく協調キャンセルによるものであることに注意してください。

協調モデルでは、ターゲット操作は、ターミネータによって通知されるキャンセルインジケータを監視できます。これにより、ターゲットはキャンセルリクエストを検出し、必要に応じてクリーンアップし、適切なタイミングで終了のステータスをターミネータに通知できます。このようなアプローチがないと、操作が突然終了すると、スレッドのリソースが残り、場合によってはホスティングプロセスやアプリドメインも破損した状態になる可能性があります。

.NET 4.0以降、このパターンを実装する最良の方法は、 CancellationToken を使用することです。スレッドを操作する場合、トークンは、スレッドで実行されているメソッドにパラメーターとして渡すことができます。 Tasksでは、CancellationTokensのサポートがいくつかの タスクコンストラクター に組み込まれています。キャンセルトークについては、こちらで詳しく説明しています MSDNの記事

34
BitMask777

完全を期すために、 上記の回答 の非同期対応物:

async Task<TcpClient> AcceptAsync(TcpListener listener, CancellationToken ct)
{
    using (ct.Register(listener.Stop))
    {
        try
        {
            return await listener.AcceptTcpClientAsync();
        }
        catch (SocketException e)
        {
            if (e.SocketErrorCode == SocketError.Interrupted)
                throw new OperationCanceledException();
            throw;
        }
    }
}

更新:@Mitchがコメントで示唆しているように(そして この議論 確認しているように)、AcceptTcpClientAsyncを待つとObjectDisposedExceptionの後にStopがスローされる可能性があります(これを呼び出しています)とにかく)、だからObjectDisposedExceptionもキャッチするのは理にかなっています:

async Task<TcpClient> AcceptAsync(TcpListener listener, CancellationToken ct)
{
    using (ct.Register(listener.Stop))
    {
        try
        {
            return await listener.AcceptTcpClientAsync();
        }
        catch (SocketException e) when (e.SocketErrorCode == SocketError.Interrupted)
        {
            throw new OperationCanceledException();
        }
        catch (ObjectDisposedException) when (ct.IsCancellationRequested)
        {
            throw new OperationCanceledException();
        }
    }
}
9
Vlad

これが私がこれを克服した方法です。この助けを願っています。最もきれいではないかもしれませんが、私のために働きます

    public class consoleService {
    private CancellationTokenSource cts;
    private TcpListener listener;
    private frmMain main;
    public bool started = false;
    public bool stopped = false;

   public void start() {
        try {
            if (started) {
                stop();
            }
            cts = new CancellationTokenSource();
            listener = new TcpListener(IPAddress.Any, CFDPInstanceData.Settings.RemoteConsolePort);
            listener.Start();
            Task.Run(() => {
                AcceptClientsTask(listener, cts.Token);
            });

            started = true;
            stopped = false;
            functions.Logger.log("Started Remote Console on port " + CFDPInstanceData.Settings.RemoteConsolePort, "RemoteConsole", "General", LOGLEVEL.INFO);

        } catch (Exception E) {
            functions.Logger.log("Error starting remote console socket: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
        }
    }

    public void stop() {
        try {
            if (!started) { return; }
            stopped = false;
            cts.Cancel();
            listener.Stop();
            int attempt = 0;
            while (!stopped && attempt < GlobalSettings.ConsoleStopAttempts) {
                attempt++;
                Thread.Sleep(GlobalSettings.ConsoleStopAttemptsDelayMS);
            }

        } catch (Exception E) {
            functions.Logger.log("Error stopping remote console socket: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
        } finally {
            started = false;
        }
    }

     void AcceptClientsTask(TcpListener listener, CancellationToken ct) {

        try {
            while (!ct.IsCancellationRequested) {
                try {
                    TcpClient client = listener.AcceptTcpClient();
                    if (!ct.IsCancellationRequested) {
                        functions.Logger.log("Client connected from " + client.Client.RemoteEndPoint.ToString(), "RemoteConsole", "General", LOGLEVEL.DEBUG);
                        ParseAndReply(client, ct);
                    }

                } catch (SocketException e) {
                    if (e.SocketErrorCode == SocketError.Interrupted) {
                        break;
                    } else {
                        throw e;
                    }
                 } catch (Exception E) {
                    functions.Logger.log("Error in Remote Console Loop: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
                }

            }
            functions.Logger.log("Stopping Remote Console Loop", "RemoteConsole", "General", LOGLEVEL.DEBUG); 

        } catch (Exception E) {
            functions.Logger.log("Error in Remote Console: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
        } finally {
            stopped = true;

        }
        functions.Logger.log("Stopping Remote Console", "RemoteConsole", "General", LOGLEVEL.INFO);

    }
    }
1
bmigette

さて、非同期ソケットが適切に機能する前の昔(IMO、BitMaskがこれについて話している今日の最良の方法)、私たちは簡単なトリックを使用しました:isRunningをfalseに設定します(ここでも、理想的には、代わりにCancellationTokenを使用してください。代わりに、public static bool isRunning;notバックグラウンドワーカーを終了するスレッドセーフな方法です:))そして新しいTcpClient.Connectを自分で開始します-これはAccept呼び出しから戻ると、正常に終了できます。

BitMaskがすでに述べたように、Thread.Abortは間違いなく終了時の安全なアプローチではありません。実際、Acceptがネイティブコードによって処理され、Thread.Abortには権限がないことを考えると、まったく機能しません。それが機能する唯一の理由は、I/Oで実際にブロックしているのではなく、Pending(非ブロック呼び出し)をチェックしながら無限ループを実行しているためです。これは、1つのコアで100%のCPU使用率を持つための優れた方法のように見えます:)

あなたのコードには他にもたくさんの問題がありますが、それはあなたが非常に単純なことをしているという理由だけで、そして.NETがかなり素晴らしいという理由だけであなたの顔に爆発することはありません。たとえば、読み込んでいるバッファ全体に対して常にGetStringを実行していますが、それは間違っています。実際、これは、たとえば、バッファオーバーフローの教科書の例です。 C++-C#で機能するように見える唯一の理由は、バッファーが事前にゼロ化されるためです。したがって、GetStringは、読み取った「実際の」文字列の後のデータを無視します。代わりに、Read呼び出しの戻り値を取得する必要があります。これは、読み取ったバイト数、つまりデコードする必要のあるバイト数を示します。

これのもう1つの非常に重要な利点は、読み取るたびにbyte[]を再作成する必要がなくなることです。バッファを何度も再利用するだけです。

しないでください GUIスレッド以外のスレッドからGUIを操作します(はい、Taskは別のスレッドプールスレッドで実行されています)。 MessageBox.Showは、実際には他のスレッドから機能するダーティハックですが、それは本当にあなたが望むものではありません。 GUIスレッドでGUIアクションを呼び出す必要があります(たとえば、Form.Invokeを使用するか、GUIスレッドで同期コンテキストを持つタスクを使用します)。これは、メッセージボックスが期待どおりの適切なダイアログになることを意味します。

投稿したスニペットにはさらに多くの問題がありますが、これはコードレビューではなく、古いスレッドであるため、これ以上作成するつもりはありません:)

1
Luaan