Отмена блокировки вызова AcceptTcpClient

Как все уже знают, самый простой способ принять входящие TCP-соединения в C# - это зацикливание на TcpListener.AcceptTcpClient(). Кроме того, этот способ заблокирует выполнение кода до получения соединения. Это чрезвычайно ограничивает графический интерфейс, поэтому я хочу слушать соединения в отдельном потоке или задаче.

мне сказали, что у потоков есть несколько недостатков, однако никто не объяснил мне, что это такое. Поэтому вместо использования потоков я использовал задачи. Это отлично работает, однако, поскольку метод 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();
}

I просто нужно что-то заменить listenTask.Abort () вызов (который я прокомментировал, потому что метод не существует)

5 ответов


следующий код закроет / прервет AcceptTcpClient, когда переменная isRunning станет false

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)
  {

  }

}

Отмена AcceptTcpClient

ваш лучший выбор для отмены блокировки AcceptTcpClient операция заключается в вызове tcplistener с.Остановка который бросит 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 будет нужен некоторый уровень доступа к нему, так что вы либо охватить его вне вашего метода прослушивания или обернуть логика прослушивателя внутри объекта, который управляет TcpListener и предоставляет методы Start и Stop (с остановкой вызова TcpListener.Stop()).

Асинхронный Прекращения

потому что принятый ответ использует Thread.Abort() чтобы завершить поток, здесь может быть полезно отметить, что лучший способ завершить асинхронную операцию-это совместная отмена, а не жесткая отмена.

в кооперативной модели, деятельность цели может контролировать а индикатор отмены, который сопровождается Терминатор. Это позволяет цели обнаружить запрос на отмену, очистить по мере необходимости, а затем в соответствующее время сообщить статус завершения обратно Терминатору. Без такого подхода резкое прекращение операции может оставить ресурсы потока и, возможно, даже процесс размещения или домен приложения в поврежденном состоянии.

начиная с .NET 4.0, лучший способ реализовать этот шаблон-это CancellationToken. При работе с потоками токен может быть передан в качестве параметра методу, выполняющемуся в потоке. С задачами поддержка CancellationTokens встроена в несколько задач конструкторов. Отмена токес обсуждаются более подробно в этом статья MSDN.


для полноты, асинхронный аналог ответ выше:

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();
        }
    }
}

Ну, в старые времена, прежде чем правильно работать асинхронные сокеты (лучший способ сегодня IMO, BitMask говорит об этом), мы использовали простой трюк: установите isRunning значение false (опять же, в идеале, вы хотите использовать CancellationToken вместо public static bool isRunning; и не потокобезопасный способ завершить фоновый рабочий:)) и начать новый TcpClient.Connect к себе-это вернет тебя из Accept вызов, и вы можете закончить изящно.

как уже сказал BitMask,Thread.Abort определенно, это небезопасный подход при увольнении. На самом деле, это вообще не сработало бы, учитывая это Accept обрабатывается собственным кодом, где Thread.Abort не имеет силы. Единственная причина, по которой это работает, заключается в том, что вы фактически не блокируете ввод-вывод, а скорее запускаете бесконечный цикл при проверке Pending (не-блокирующий вызов). Это выглядит как отличный способ иметь 100% использование процессора на одном ядре:)

у вашего кода есть много других проблем, которые не взрываются у вас на лице только потому, что вы делаете очень простые вещи, и из-за того, что .NET довольно приятный. Например, вы всегда делаете GetString на весь буфер, в который Вы читаете - но это неправильно. Фактически, это хрестоматийный пример переполнения буфера, например, в C++ - единственная причина, по которой он работает в C#, заключается в том, что он предварительно обнуляет буфер, поэтому GetString игнорирует данные после" реальной " строки, которую Вы читаете. Вместо этого вам нужно взять возвращаемое значение Read call-это говорит вам, сколько байтов вы прочитали, и как такое, сколько нужно расшифровать.

еще одним очень важным преимуществом этого является то, что вам больше не нужно воссоздавать byte[] после каждого чтения, вы можете просто повторно использовать буфер снова и снова.

не работа с GUI из другого потока, чем поток GUI (да, ваш Task выполняется в отдельном потоке из пула потоков). MessageBox.Show - это грязный хак, который на самом деле работает из других потоков, но это действительно не то, что вы хотите. Вы необходимо вызвать действия GUI в потоке GUI (например, используя форму.Вызов или с помощью задачи, имеющей контекст синхронизации в потоке GUI). Это будет означать, что окно сообщения будет правильным диалогом, который вы ожидаете.

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


вот как я преодолел это. Надеюсь, это поможет. Может быть, не самый чистый, но работает для меня

    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);

    }
    }