正确的停止TcpListener的方法

73

我目前正在使用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变量是该类的一个字段,它的值为布尔类型。当程序关闭时,我希望它停止监听客户端。将listen设置为false可以防止它继续接受新的连接,但由于AcceptTcpClient是一个阻塞调用,它至少会接受下一个客户端并然后退出。有没有办法强制它立即中断并停止?在其他阻塞调用运行时调用listener.Stop()会产生什么影响?

9个回答

68

以下是两个快速解决方案,针对你的代码和我所预设的设计:

1. Thread.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()并将委托指回自身。

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

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

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


3
我刚刚阅读了你的回答,但对于我来说,如何正确实现你所描述的内容并不清楚。你能提供一些代码来澄清吗?谢谢。 - Jehof
5
在我的测试中,当线程在等待连接时被阻塞时,Thread.Abort似乎不能按预期工作,请参见我在此处的问题:https://dev59.com/v1bTa4cB1Zd3GeqP_opr。 - Ralph Shillington
1
你确定在clientThread正在使用client.GetStream()的时候关闭客户端是可行的吗?如果我保留client.Close()调用,我将无法在HandleConnection方法中创建一个StreamReader来读取NetworkStream。 - comecme
5
为什么这被标记为解决方案?前三个点似乎因为某些原因是错误的,而最后一个需要一些代码。 - IvanP
1
TcpClient没有实现IDisposable接口。这个答案有几个错误点,不应该被接受。 - xxbbcc
显示剩余5条评论

50

listener.Server.Close()在另一个线程中调用会打破阻塞调用。

A blocking operation was interrupted by a call to WSACancelBlockingCall

虽然我喜欢Peter Oehlert的答案,因为它很完整并且是最佳实践,但我的任务很简单,不需要进行完整的异步重写,而且我发现Thread.Abort()对我没有用。然而,这个方法对我非常快速和有效。 - Matt Connolly
2
我更喜欢这个答案,因为如果你要重写它,你应该使用WCF,它解决了我在.NET中使用TCP/IP套接字时遇到的许多问题。 - NibblyPig
你甚至可以在 SocketException 的 catch 处理程序中添加一些代码。if ((e.SocketErrorCode == SocketError.Interrupted)) Console.WriteLine("A blocking listen has been cancelled"); 这样,你就可以确保在等待客户端连接时终止了程序。 - WagoL
1
为什么不执行 listener.Stop(); 呢? - John
1
我同意@John的观点; 为什么要调用listener.Server.Close()而不是listener.Stop()?如果您查看TcpListener.Stop()的参考源代码,是否为您提供了listener.Server.Close()的等效操作(以及清除旧连接请求等其他操作)。 - jrh

3
不要使用循环,而是直接调用BeginAcceptTcpClient(),在回调函数中,如果仍然设置了监听标志,则只需发出另一个BeginAcceptTcpClient()调用。
要停止监听器,由于您没有阻塞,所以您的代码只需调用它的Close()方法即可。

你能提供一个例子吗? - omJohn8372
@omJohn8372,关于我写的内容,没有什么需要补充的了。你在某个地方设置了一个标志,你的回调方法会检查这个标志。当你调用BeginAcceptTcpClient时提供该回调。有关回调的示例,请参见Microsoft文档。因此,当您想要停止侦听器时,请设置标志并对其进行Close()操作。在回调中,首先检查标志是否已设置,如果是,则知道它已关闭并退出。 - Mike Scott

3

3
将问题标记为重复或提供完整答案。 - Denise Skidmore

3
套接字提供了强大的异步功能。请看使用异步服务器套接字
以下是关于代码的一些注释。
在这种情况下,使用手动创建的线程可能会增加负担。
下面的代码存在竞争条件 - TcpClient.Close()关闭通过TcpClient.GetStream()获取的网络流。请考虑在您确定不再需要客户端时关闭它。
 clientThread.Start(client.GetStream());
 client.Close();

TcpClient.Stop() 方法会关闭底层的套接字。TcpClient.AcceptTcpClient() 方法使用底层套接字上的 Socket.Accept() 方法,一旦套接字被关闭就会抛出 SocketException 异常。您可以在不同的线程中调用它。
总之,我建议使用异步套接字。

文档明确指出,关闭TcpClient不会关闭底层流。 - Christian P.
是的,文档上是这么说的,但实现会关闭流...检查一下吧。 TcpClient tcpClient = new TcpClient(); tcpClient.Connect("www.google.com", 80); NetworkStream networkStream = tcpClient.GetStream(); tcpClient.Close(); byte[] bytes = new byte[1024]; networkStream.Read(bytes, 0, 1024); - Dzmitry Huba
没有 TcpClient.Stop() 方法。 - Qwertie

1

只是为了更多地理由使用异步方法,我相当确定Thread.Abort不起作用,因为调用被阻塞在操作系统级别的TCP堆栈中。

另外...如果你正在回调中调用BeginAcceptTCPClient来监听除第一个连接以外的每个连接,请小心确保执行初始BeginAccept的线程不会终止,否则监听器将自动被框架处理掉。我想这是一种特性,但实际上非常烦人。在桌面应用程序中通常不是问题,但在Web上,您可能希望使用线程池,因为这些线程永远不会真正终止。


0

如上所述,使用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;
        }

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

-2

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

5
我发现这段代码与我的代码唯一的区别在于else块。因为continue语句基本上会跳到while循环的顶部,所以else块并不是必需的。可以根据清晰度或编码风格的优点进行论证,但并非必需。我还有什么遗漏吗? - Peter Oehlert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接