取消阻止AcceptTcpClient调用

众所周知,在C#中接受传入TCP连接的最简单方法是循环使用TcpListener.AcceptTcpClient()。 此外,这种方式将阻止代码执行,直到获得连接。 这对GUI非常有限,所以我想在单独的线程或任务中监听连接。

我被告知,线程有几个缺点,但没有人解释我这是什么。 所以我没有使用线程,而是使用了任务。 这很好用,但是由于AcceptTcpClient方法阻塞了执行,我找不到任何处理任务取消的方法。

目前代码看起来像这样,但是当我希望程序停止侦听连接时,我不知道如何取消任务。

首先关闭任务中执行的function:

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

二,启动和停止监听服务的function:

 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()调用(我注释掉了,因为该方法不存在)

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

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

无论什么想要调用TcpListener上的Stop都需要一定程度的访问权限,因此您可以将其范围限制在Listen方法之外,或者将侦听器逻辑包装在管理TcpListener的对象内并公开Start和Stop方法(使用Stop调用TcpListener.Stop() )。

异步终止

因为接受的答案使用Thread.Abort()来终止线程,所以在这里注意,终止异步操作的最佳方法是通过合作取消而不是硬中止。

在协作模型中,目标操作可以监视由终结器发信号通知的取消指示符。 这允许目标检测取消请求,根据需要进行清理,然后在适当的时间将终止的状态传送回终结器。 如果没有这样的方法,突然终止操作可能会使线程的资源,甚至托管进程或应用程序域处于损坏状态。

从.NET 4.0开始,实现此模式的最佳方法是使用CancellationToken 。 使用线程时,令牌可以作为参数传递给在线程上执行的方法。 使用Tasks,可以在几个Task构造函数中内置对CancellationTokens的支持。 在这篇MSDN文章中更详细地讨论了取消标记。

为完整起见,上述答案的异步对应:

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

更新:正如@Mitch在评论中建议的那样(并且正如本讨论确认的那样),等待AcceptTcpClientAsync可能会在Stop (我们正在调用)之后抛出ObjectDisposedException ,因此捕获ObjectDisposedException也是有意义的:

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

好吧,在正常工作异步套接字之前的过去(今天最好的方式是IMO,BitMask谈论这个),我们使用了一个简单的技巧:将isRunning设置为false(理想情况下,你想要使用CancellationTokenpublic static bool isRunning; 不是终止后台工作者的线程安全方式:))并为自己启动一个新的TcpClient.Connect – 这将使您从Accept调用返回,并且您可以正常终止。

正如BitMask已经说过的那样, Thread.Abort绝对不是终止时的安全方法。 事实上,它根本不起作用,因为Accept由本机代码处理,其中Thread.Abort没有权力。 它工作的唯一原因是因为你实际上并没有在I / O中阻塞,而是在检查Pending (非阻塞调用)时运行无限循环。 这看起来像是在一个核心上拥有100%CPU使用率的好方法:)

你的代码也有很多其他问题,只是因为你做的很简单,并且因为.NET相当不错,所以你不会在你面前爆炸。 例如,你总是在你正在阅读的整个缓冲区上做GetString – 但这是错误的。 实际上,这是一个例如C ++中缓冲区溢出的教科书示例 – 它似乎在C#中工作的唯一原因是因为它预先将缓冲区归零,因此GetString在您读取的“真实”字符串之后忽略数据。 相反,您需要获取Read调用的返回值 – 它告诉您已读取了多少字节,因此需要解码多少字节。

另一个非常重要的好处是它意味着您不必再在每次读取后重新创建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); } } 
Interesting Posts