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

Как все, возможно, уже знают, самый простой способ принимать входящие TCP-соединения в C# - это циклическое выполнение по 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() (который я закомментировал, потому что метод не существует)

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

Что бы вы ни хотели вызвать Stop на вашем TcpListener, потребуется некоторый уровень доступа к нему, поэтому вы должны либо ограничить его за пределами вашего метода Listen, либо обернуть логику слушателя внутри объекта, который управляет TcpListener и предоставляет методы Start и Stop (с помощью Stop призвание TcpListener.Stop()).

Асинхронное завершение

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

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

Начиная с.NET 4.0 и далее лучший способ реализовать этот шаблон - использовать CancellationToken. При работе с потоками токен может быть передан в качестве параметра методу, выполняемому в потоке. В Tasks поддержка CancellationTokens встроена в несколько конструкторов Task. Токены отмены обсуждаются более подробно в этой статье 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 (например, с помощью Form.Invoke или с помощью задачи, которая имеет контекст синхронизации в потоке GUI). Это будет означать, что окно сообщения будет правильным диалогом, который вы ожидаете.

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

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

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

    }
    }
Другие вопросы по тегам