Cancelar o bloqueio da chamada AcceptTcpClient

Como todos já devem saber, a maneira mais simples de aceitar conexões TCP de input em C # é fazendo um loop através de TcpListener.AcceptTcpClient (). Além disso, esse modo bloqueará a execução do código até que uma conexão seja obtida. Isso é extremamente limitante para uma GUI, então eu quero ouvir conexões em um thread ou tarefa separada.

Foi-me dito que os tópicos têm várias desvantagens, no entanto, ninguém me explicou o que são. Então, ao invés de usar threads, usei tarefas. Isso funciona muito bem, no entanto, como o método AcceptTcpClient está bloqueando a execução, não consigo encontrar nenhuma maneira de manipular um cancelamento de tarefa.

Atualmente, o código se parece com isso, mas não tenho ideia de como gostaria de cancelar a tarefa quando quiser que o programa pare de ouvir as conexões.

Primeiro, a function executada na tarefa:

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

Em segundo lugar, as funções que iniciam e param o serviço de escuta:

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

Eu só preciso de algo para replace a chamada listenTask.Abort () (que eu comentei porque o método não existe)

O código a seguir fechará / abortará AcceptTcpClient quando a variável isRunning se tornar falsa

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

Cancelando AcceptTcpClient

Sua melhor aposta para cancelar a operação de bloqueio de AcceptTcpClient é chamar TcpListener.Stop, que lançará um SocketException que você pode capturar se você quiser verificar explicitamente que a operação foi cancelada.

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

O que quer que você queira chamar Stop em seu TcpListener precisará de algum nível de access a ele, então você o escopeará fora do seu método Listen ou envolverá a lógica do ouvinte dentro de um object que gerencia o TcpListener e expõe os methods Start e Stop (com Stop chamando TcpListener.Stop() ).

Terminação Assíncrona

Como a resposta aceita usa Thread.Abort() para encerrar o thread, pode ser útil observar aqui que a melhor maneira de encerrar uma operação assíncrona é pelo cancelamento cooperativo, em vez de uma interrupção grave.

Em um modelo cooperativo, a operação de destino pode monitorar um indicador de cancelamento que é sinalizado pelo terminador. Isso permite que o alvo detecte uma solicitação de cancelamento, limpe conforme necessário e, em um momento apropriado, comunique o status da terminação de volta ao terminador. Sem uma abordagem como essa, o encerramento abrupto da operação pode deixar os resources do encadeamento e, possivelmente, até mesmo o processo de hospedagem ou o domínio do aplicativo em um estado corrompido.

A partir do .NET 4.0, a melhor maneira de implementar esse padrão é com um CancellationToken . Ao trabalhar com encadeamentos, o token pode ser passado como um parâmetro para o método em execução no encadeamento. Com o Tasks, o suporte para o CancellationTokens é incorporado em vários dos construtores Task . Tokes de cancelamento são discutidos em mais detalhes neste artigo do MSDN.

Para completar, contrapartida assíncrona da resposta acima :

 async Task 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; } } } 

Update: Como o @Mitch sugere em comentários (e como esta discussão confirma), aguardar AcceptTcpClientAsync pode lançar ObjectDisposedException após o Stop (que estamos chamando de qualquer maneira), então faz sentido capturar ObjectDisposedException também:

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

Bem, antigamente, antes de trabalhar apropriadamente sockets asynchronouss (a melhor maneira hoje em dia, a BitMask fala sobre isso), usamos um truque simples: definir o isRunning como false (novamente, de preferência, você deseja usar CancellationToken vez disso, public static bool isRunning; não é uma maneira segura de encerrar um worker em segundo plano :)) e iniciar um novo TcpClient.Connect para si mesmo – isso o retornará da chamada Accept e você poderá encerrar normalmente.

Como BitMask já disse, Thread.Abort definitivamente não é uma abordagem segura na rescisão. Na verdade, não funcionaria de forma alguma, já que Accept é manipulado por código nativo, onde Thread.Abort não tem poder. A única razão pela qual isso funciona é porque você não está realmente bloqueando a E / S, mas sim executando um loop infinito durante a verificação de Pending (chamada sem bloqueio). Isso parece uma ótima maneira de ter 100% de uso da CPU em um só núcleo 🙂

Seu código tem muitos outros problemas também, que não explodem na sua cara apenas porque você está fazendo coisas muito simples, e por causa do .NET ser bastante legal. Por exemplo, você está sempre fazendo GetString em todo o buffer que está lendo – mas isso está errado. Na verdade, esse é um exemplo de livro-texto de um estouro de buffer em, por exemplo, C ++ – a única razão pela qual parece funcionar em C # é porque ele pre-zera o buffer, portanto GetString ignora os dados depois da cadeia “real” que você lê. Em vez disso, você precisa obter o valor de retorno da chamada de Read – que informa quantos bytes você leu e, como tal, quantos você precisa decodificar.

Outro benefício muito importante disso é que você não precisa mais recriar o byte[] após cada leitura – você pode simplesmente reutilizar o buffer uma e outra vez.

Não trabalhe com a GUI de outro thread que não o thread da GUI (sim, sua Task está sendo executada em um thread separado do pool de threads). MessageBox.Show é um hack sujo que na verdade funciona a partir de outros tópicos, mas que realmente não é o que você quer. Você precisa chamar as ações da GUI no encadeamento da GUI (por exemplo, usando Form.Invoke ou usando uma tarefa que tenha um contexto de synchronization no encadeamento da GUI). Isso significa que a checkbox de mensagens será o diálogo adequado que você esperaria.

Há muitos mais problemas com o snippet que você postou, mas como isso não é uma revisão de código, e que é um tópico antigo, não vou mais fazer isso 🙂

Aqui está como eu superei isso. Espero que esta ajuda. Pode não ser o mais limpo, mas funciona para mim

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