Подтвердить что ты не робот

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

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

4b9b3361

Ответ 1

Следующий код будет закрывать/прерывать 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)
  {

  }

}

Ответ 2

Отмена 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. При работе с потоками токен может передаваться как параметр для метода, выполняющегося в потоке. С помощью Задачи поддержка CancellationTokens встроена в несколько конструкторов задач. Аннотации отмены описаны более подробно в этой статье

Ответ 3

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

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

Ответ 4

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

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

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

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

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

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

Ответ 5

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

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

    }
    }