【问题标题】:Proper way to stop TcpListener停止 TcpListener 的正确方法
【发布时间】:2010-09-26 19:03:15
【问题描述】:

我目前正在使用 TcpListener 来处理传入连接,每个连接都有一个线程来处理通信,然后关闭该单个连接。代码如下:

TcpListener listener = new TcpListener(IPAddress.Any, Port);
System.Console.WriteLine("Server Initialized, listening for incoming connections");
listener.Start();
while (listen)
{
     // Step 0: Client connection
     TcpClient client = listener.AcceptTcpClient();
     Thread clientThread = new Thread(new ParameterizedThreadStart(HandleConnection));
     clientThread.Start(client.GetStream());
     client.Close();
}

listen 变量是一个布尔值,它是类中的一个字段。现在,当程序关闭时,我希望它停止监听客户端。设置监听false 将阻止它接受更多连接,但由于AcceptTcpClient 是一个阻塞调用,它至少会占用下一个客户端然后退出。有什么办法可以迫使它在当时和那里简单地爆发并停止?在另一个阻塞调用运行时调用 listener.Stop() 有什么影响?

【问题讨论】:

    标签: c# sockets networking tcplistener


    【解决方案1】:

    鉴于代码和我认为是您的设计,这是您可以使用的两个快速修复:

    1。线程.Abort()

    如果您从另一个线程启动了这个 TcpListener 线程,您可以简单地在该线程上调用 Abort(),这将在阻塞调用中产生一个 ThreadAbortException 并沿堆栈向上移动。

    2。 TcpListener.Pending()

    第二个低成本解决方法是使用listener.Pending() 方法来实现轮询模型。然后,您使用Thread.Sleep() 等待,然后再查看是否有新连接挂起。一旦你有一个挂起的连接,你调用AcceptTcpClient() 并释放挂起的连接。代码看起来像这样:

    while (listen) {
         // Step 0: Client connection
         if (!listener.Pending()) {
              Thread.Sleep(500); // choose a number (in milliseconds) that makes sense
              continue; // skip to next iteration of loop
         }
         TcpClient client = listener.AcceptTcpClient();
         Thread clientThread = new Thread(new ParameterizedThreadStart(HandleConnection));
         clientThread.Start(client.GetStream());
         client.Close();
    }
    

    异步重写

    但是,您确实应该为您的应用程序转向非阻塞方法。在幕后,框架将使用重叠的 I/O 和 I/O 完成端口来实现异步调用的非阻塞 I/O。这也不是很困难,只是需要对你的代码进行一些不同的思考。

    基本上,您将使用BeginAcceptTcpClient() 方法开始您的代码,并跟踪您返回的IAsyncResult。您将其指向一个负责获取TcpClient 并将其传递给新线程但传递给ThreadPool.QueueUserWorkerItem 的线程的方法,因此您不会启动和关闭每个客户端请求都有一个新线程(注意:如果您有特别长的请求,您可能需要使用自己的线程池,因为线程池是共享的,如果您垄断了所有线程,系统实现的应用程序的其他部分可能饿死)。一旦侦听器方法将您的新TcpClient 启动到它自己的ThreadPool 请求,它就会再次调用BeginAcceptTcpClient() 并将代理指向它自己。

    实际上,您只是将当前方法分解为 3 个不同的方法,然后由各个部分调用:

    1. 引导一切;
    2. 成为调用EndAcceptTcpClient()的目标,将TcpClient启动到自己的线程,然后再次调用自己;
    3. 处理客户端请求并在完成后关闭它。

    (注意:您应该将TcpClient 调用包含在using(){} 块中,以确保即使在发生异常时也调用TcpClient.Dispose()TcpClient.Close() 方法。或者您可以将它放在try {} finally {} 块的finally 块中。)

    【讨论】:

    • 我刚刚阅读了您的答案,但对我来说,不清楚如何正确实施您所描述的内容。你能提供一些代码来澄清它。谢谢
    • 在我的测试中,当线程被阻塞等待连接时,Thread.Abort 似乎无法按预期工作,请在此处查看我的问题:stackoverflow.com/questions/5778267/…
    • 您确定可以在 clientThread 使用 client.GetStream() 时关闭客户端吗?如果我离开 client.Close() 调用,我无法在 HandleConnection 方法中的 NetworkStream 上创建 StreamReader。
    • 为什么这被标记为解决方案?由于某种原因,前 3 点似乎是错误的,最后一个可能使用一些代码。
    • TcpClient 没有实现IDisposable。这个答案有几个不好的地方,不应该被接受。
    【解决方案2】:

    来自另一个线程的listener.Server.Close() 中断了阻塞调用。

    A blocking operation was interrupted by a call to WSACancelBlockingCall
    

    【讨论】:

    • 虽然我喜欢 Peter Oehlert 对完整性和最佳实践的回答,但我有一个简单的任务并不能证明完全异步重写的复杂性,而且我发现 Thread.Abort() 不起作用为了我。然而,这对我来说非常快速有效。
    • 我更喜欢这个答案,好像你要重写它,你可能应该使用 WCF,它解决了我在 .net 中使用 TCP/IP 套接字时遇到的许多问题
    • 你甚至可以添加一些行。在您的 SocketException 捕获处理程序中。 if ((e.SocketErrorCode == SocketError.Interrupted)) Console.WriteLine("A blocking listen has been cancelled"); 所以你确定你在等待客户端连接时终止了。
    • 为什么不做 listener.Stop();
    • 我同意@John;为什么叫listener.Server.Close() 而不是listener.Stop()?如果您查看Reference Source for TcpListener.Stop(),会为您提供相当于listener.Server.Close() 的功能(以及清除旧连接请求等)。
    【解决方案3】:

    不要使用循环。相反,在没有循环的情况下调用 BeginAcceptTcpClient()。在回调中,如果您的监听标志仍然设置,只需再次调用 BeginAcceptTcpClient()。

    要停止侦听器,由于您没有被阻止,您的代码只需调用 Close() 即可。

    【讨论】:

    • 你能举个例子吗?
    • @omJohn8372 我写的东西没有什么要补充的了。您的回调方法检查的某处有一个标志。您在调用 BeginAcceptTcpClient 时提供该回调。有关回调的示例,请参阅Microsoft docs。因此,当您想停止侦听器时,请在其上设置标志和 Close()。在回调中,你首先检查标志是否被设置,如果是,你知道它已经被关闭并退出。
    【解决方案4】:

    套接字提供强大的异步功能。看看Using an Asynchronous Server Socket

    这里有几个关于代码的注释。

    在这种情况下使用手动创建的线程可能会产生开销。

    下面的代码受竞争条件的影响 - TcpClient.Close() 关闭您通过 TcpClient.GetStream() 获得的网络流。考虑在您可以肯定地说不再需要它的地方关闭客户端。

     clientThread.Start(client.GetStream());
     client.Close();
    

    TcpClient.Stop() 关闭底层套接字。 TcpCliet.AcceptTcpClient() 在底层套接字上使用 Socket.Accept() 方法,一旦关闭就会抛出 SocketException。您可以从不同的线程调用它。

    无论如何我推荐异步套接字。

    【讨论】:

    • 文档特别指出关闭 TcpClient 不会关闭底层流。
    • 是的,文档是这么说的,但是实现会关闭流...检查它 TcpClient tcpClient = new TcpClient(); tcpClient.Connect("www.google.com", 80); NetworkStream networkStream = tcpClient.GetStream(); tcpClient.Close();字节[]字节=新字节[1024]; networkStream.Read(bytes, 0, 1024);
    • 没有TcpClient.Stop()方法。
    【解决方案5】:

    在这里查看我的答案https://stackoverflow.com/a/17816763/2548170 TcpListener.Pending() 不是很好的解决方案

    【讨论】:

    • 将问题标记为重复或提供完整答案。
    【解决方案6】:

    只是为了增加使用异步方法的更多理由,我很确定 Thread.Abort 不会工作,因为调用在操作系统级别的 TCP 堆栈中被阻止。

    另外...如果您在回调中调用 BeginAcceptTCPClient 以侦听除第一个连接之外的每个连接,请注意确保执行初始 BeginAccept 的线程不会终止,否则侦听器将自动被释放框架。我想这是一个功能,但实际上它很烦人。在桌面应用程序中这通常不是问题,但在 Web 上,您可能希望使用线程池,因为这些线程不会真正终止。

    【讨论】:

      【解决方案7】:

      上面已经提到了,改用 BeginAcceptTcpClient,异步管理起来会容易很多。

      这里是一些示例代码:

              ServerSocket = new TcpListener(endpoint);
              try
              {
                  ServerSocket.Start();
                  ServerSocket.BeginAcceptTcpClient(OnClientConnect, null);
                  ServerStarted = true;
      
                  Console.WriteLine("Server has successfully started.");
              }
              catch (Exception ex)
              {
                  Console.WriteLine($"Server was unable to start : {ex.Message}");
                  return false;
              }
      

      【讨论】:

        【解决方案8】:

        可能最好使用异步BeginAcceptTcpClient 函数。然后你可以在监听器上调用 Stop() ,因为它不会阻塞。

        【讨论】:

          【解决方案9】:

          一些改变使 Peter Oehlert 的回答更加完美。因为在 500 毫秒之前,侦听器再次阻塞。要更正此问题:

              while (listen)     
              {
                 // Step 0: Client connection     
                 if (!listener.Pending())     
                 {
                     Thread.Sleep(500); // choose a number (in milliseconds) that makes sense
                     continue; // skip to next iteration of loop
                 }
                 else // Enter here only if have pending clients
                 {
                    TcpClient client = listener.AcceptTcpClient();
                    Thread clientThread = new Thread(new ParameterizedThreadStart(HandleConnection));
                    clientThread.Start(client.GetStream());
                    client.Close();
                 }
             }
          

          【讨论】:

          • Elimar,我在这段代码和我自己的代码之间看到的唯一变化是 else 块。因为 continue 基本上会跳到 while 循环的顶部,所以 else 并不是绝对必要的。可以根据清洁度或编码风格的优点进行争论,但不是必需的。我还缺少其他东西吗?
          猜你喜欢
          • 1970-01-01
          • 2023-03-28
          • 1970-01-01
          • 2013-11-24
          • 2014-03-14
          • 2011-05-26
          • 1970-01-01
          • 2016-12-18
          • 1970-01-01
          相关资源
          最近更新 更多