14

大家可能已经知道,在 C# 中接受传入 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() 调用(我将其注释掉,因为该方法不存在)

4

5 回答 5

43

取消 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,对 CancellationTokens 的支持内置于多个Task 构造函数中。此MSDN 文章中更详细地讨论了取消令牌。

于 2013-05-30T21:41:03.827 回答
15

为了完整起见,上述答案的异步对应物,使用@Mitch 的建议(在此处确认确认)。

与 awaiting 的同步函数相比,它AcceptTcpClientAsync似乎会抛出(无论如何我们都ObjectDisposedExceptionStop调用它),所以 catch 也是有意义的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(ct);
        }
        catch (ObjectDisposedException) when (ct.IsCancellationRequested)
        {
            throw new OperationCanceledException(ct);
        }
    }
}

2021 年更新:.NET 5 抛出SocketException.NET Framework(使用 4.5-4.8 版本测试)和 .NET Core 2.x-3.x 抛出ObjectDisposedException. 所以截至今天,正确的代码是

#if NET5_0 //_OR_GREATER?
catch (SocketException ex) when (ct.IsCancellationRequested &&
                                 ex.SocketErrorCode == SocketError.OperationAborted)
#elif (NETFRAMEWORK && NET40_OR_GREATER) || NETCOREAPP2_0_OR_GREATER
catch (ObjectDisposedException ex) when (ct.IsCancellationRequested)
#else
#error Untested target framework
#endif
{
    throw new OperationCanceledException(ct);
}

同步对应项 ( )与listener.AcceptTcpClient()一致地抛出,因此在 .NET 5.0 之前的所有框架中都可以执行以下操作:SocketExceptionSocketErrorCode == Interrupted

try
{
    return serverSocket.AcceptTcpClient();
}
catch (SocketException e) when (e.SocketErrorCode == SocketError.Interrupted)
{
    throw new OperationCanceledException(ct);
}
于 2015-06-15T22:43:19.350 回答
1

isRunning好吧,在正确工作异步套接字之前的过去(今天最好的方法,BitMask 谈到了这一点),我们使用了一个简单的技巧CancellationToken:将线程安全的方式来终止后台工作者:)) 并为自己开始一个新的 - 这将使您从调用中返回,您可以优雅地终止。public static bool isRunning;TcpClient.ConnectAccept

正如 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 线程上具有同步上下文的任务)。这意味着消息框将是您期望的正确对话框。

您发布的代码段还有更多问题,但鉴于这不是代码审查,而且它是一个旧线程,我不会再做这个了:)

于 2014-05-26T07:33:04.460 回答
1

这就是我克服这个问题的方法。希望这有帮助。可能不是最干净的,但对我有用

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

    }
    }
于 2017-08-09T14:58:46.200 回答
-3

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

  }

}
于 2012-09-19T05:57:57.417 回答