14

誰もが知っているように、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() 呼び出しを置き換える何かが必要なだけです(メソッドが存在しないため、コメントアウトしました)

4

5 に答える 5

43

AcceptTcpClient のキャンセル

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

       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 のサポートがいくつかのTask コンストラクターに組み込まれています。キャンセル トークンについては、このMSDN 記事で詳しく説明しています。

于 2013-05-30T21:41:03.827 に答える
15

完全を期すために、上記の回答の非同期対応、@Mitch の提案を使用します (ここで確認)。

同期関数 awaitingは後AcceptTcpClientAsyncをスローするように見えるのとは対照的に(これはとにかく呼び出しています)、キャッチすることも理にかなっています。ObjectDisposedExceptionStopObjectDisposedException

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(ct);
        }
        catch (ObjectDisposedException) when (ct.IsCancellationRequested)
        {
            throw new OperationCanceledException(ct);
        }
    }
}

2021 年からの更新: .NET 5 は をスローSocketExceptionしますが、.NET Framework (バージョン 4.5-4.8 でテスト済み) と .NET Core 2.x-3.x はObjectDisposedException. したがって、今日の時点で、正しいコードは次のようになります

#if NET5_0 //_OR_GREATER?
catch (SocketException ex) when (ct.IsCancellationRequested &&
                                 ex.SocketErrorCode == SocketError.OperationAborted)
#elif (NETFRAMEWORK && NET40_OR_GREATER) || NETCOREAPP2_0_OR_GREATER
catch (ObjectDisposedException ex) when (ct.IsCancellationRequested)
#else
#error Untested target framework
#endif
{
    throw new OperationCanceledException(ct);
}

対応する同期 ( ) は と一貫してlistener.AcceptTcpClient()スローするため、以下は .NET 5.0 までのすべてのフレームワークで実行されます。SocketExceptionSocketErrorCode == Interrupted

try
{
    return serverSocket.AcceptTcpClient();
}
catch (SocketException e) when (e.SocketErrorCode == SocketError.Interrupted)
{
    throw new OperationCanceledException(ct);
}
于 2015-06-15T22:43:19.350 に答える
1

さて、非同期ソケットが適切に機能する前の昔 (今日の IMO では、BitMask がこれについて説明している最良の方法)、単純なトリックisRunningを使用していましCancellationTokenた。バックグラウンドワーカーを終了するスレッドセーフな方法:))そして自分自身に新しいものを開始します-これにより、呼び出しから戻り、正常に終了できます。public static bool isRunning;TcpClient.ConnectAccept

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 スレッドで同期コンテキストを持つタスクを使用して)。これは、メッセージ ボックスが期待どおりの適切なダイアログになることを意味します。

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

于 2014-05-26T07:33:04.460 に答える
-3

次のコードは、isRunning 変数が false になったときに AcceptTcpClient を閉じる/中止します

public static bool isRunning;

delegate void mThread(ref book isRunning);
delegate void AccptTcpClnt(ref TcpClient client, TcpListener listener);

public static main()
{
   isRunning = true;
   mThread t = new mThread(StartListening);
   Thread masterThread = new Thread(() => t(this, ref isRunning));
   masterThread.IsBackground = true; //better to run it as a background thread
   masterThread.Start();
}

public static void AccptClnt(ref TcpClient client, TcpListener listener)
{
  if(client == null)
    client = listener.AcceptTcpClient(); 
}

public static void StartListening(ref bool isRunning)
{
  TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any, portNum));

  try
  {
     listener.Start();

     TcpClient handler = null;
     while (isRunning)
     {
        AccptTcpClnt t = new AccptTcpClnt(AccptClnt);

        Thread tt = new Thread(() => t(ref handler, listener));
        tt.IsBackground = true;
        // the AcceptTcpClient() is a blocking method, so we are invoking it
        // in a separate dedicated thread 
        tt.Start(); 
        while (isRunning && tt.IsAlive && handler == null) 
        Thread.Sleep(500); //change the time as you prefer


        if (handler != null)
        {
           //handle the accepted connection here
        }        
        // as was suggested in comments, aborting the thread this way
        // is not a good practice. so we can omit the else if block
        // else if (!isRunning && tt.IsAlive)
        // {
        //   tt.Abort();
        //}                   
     }
     // when isRunning is set to false, the code exits the while(isRunning)
     // and listner.Stop() is called which throws SocketException 
     listener.Stop();           
  }
  // catching the SocketException as was suggested by the most
  // voted answer
  catch (SocketException e)
  {

  }

}
于 2012-09-19T05:57:57.417 に答える