如何设置TcpListener始终监听并接受多个连接?

35

这是我的服务器应用程序:

public static void Main()
{
    try
    {
        IPAddress ipAddress = IPAddress.Parse("127.0.0.1");

        Console.WriteLine("Starting TCP listener...");

        TcpListener listener = new TcpListener(ipAddress, 500);

        listener.Start();

        while (true)
        {
            Console.WriteLine("Server is listening on " + listener.LocalEndpoint);

            Console.WriteLine("Waiting for a connection...");

            Socket client = listener.AcceptSocket();

            Console.WriteLine("Connection accepted.");

            Console.WriteLine("Reading data...");

            byte[] data = new byte[100];
            int size = client.Receive(data);
            Console.WriteLine("Recieved data: ");
            for (int i = 0; i < size; i++)
                Console.Write(Convert.ToChar(data[i]));

            Console.WriteLine();

            client.Close();
        }

        listener.Stop();
    }
    catch (Exception e)
    {
        Console.WriteLine("Error: " + e.StackTrace);
        Console.ReadLine();
    }
}

正如您所看到的,它在工作时一直在监听,但我想指定我希望应用程序能够同时监听并支持多个连接。

我该如何修改以在不中断监听的情况下接受多个连接?


1
指定127.0.0.1会限制监听套接字仅在本地计算机上工作吗?公共网络上的设备能够访问此监听套接字吗? - Zapnologica
为什么要使用 new byte[100]; 数组?这是从客户端发送的数据大小吗? - barteloma
@barteloma 这件事情发生得太久远了,我已经记不清了,但很可能只是一个魔数。 - keeehlan
3个回答

60
  1. 你要监听传入连接的套接字通常称为监听套接字

  2. 监听套接字确认一个传入连接时,会创建一个套接字,通常称为子套接字,它有效地表示远程端点。

  3. 为了同时处理多个客户端连接,您需要为每个服务器将接收和处理数据的子套接字生成一个新线程。
    这样做将允许监听套接字接受和处理多个连接,因为您正在监听的线程不再被阻塞或等待,而是等待传入数据。

while (true)
{
   Socket client = listener.AcceptSocket();
   Console.WriteLine("Connection accepted.");
    
   var childSocketThread = new Thread(() =>
   {
       byte[] data = new byte[100];
       int size = client.Receive(data);
       Console.WriteLine("Recieved data: ");
       
       for (int i = 0; i < size; i++)
       {
           Console.Write(Convert.ToChar(data[i]));
       }

       Console.WriteLine();
    
       client.Close();
    });

    childSocketThread.Start();
}

1
@MrDysprosium 显然是可扩展性。 - SHM
1
你说得很明显,所以也许我对这个话题的理解非常低... 但为什么异步比线程更可扩展? - MrDysprosium
1
@MrDysprosium 因为线程在操作系统中的创建和管理成本很高,因此为每个客户端启动一个线程是昂贵的。除此之外,您为每个客户端创建的线程大部分时间都在等待来自客户端的某些东西,什么也不做。 - SHM
2
@SHM 不想成为两年后才回复的人,但是为了实现异步进程,必须建立一个线程,因此必须有人启动一个线程。即使从另一台机器通过网络传输,也会打开一个线程等待响应。否则我不知道你怎么能建立异步进程(例如任务仍然是线程)。 - jkrieg
1
@jkrieg 任务不是线程。任务在线程上运行。您可以在没有每个连接一个线程的情况下进行异步通信。为每个连接生成一个线程是非常幼稚的方法,因为大多数线程将处于空闲状态。对从文件或网络套接字读取的任务使用await也不会生成新线程。当前线程只是转移到其他事物,直到设备驱动程序执行所请求的活动(例如通过向CPU发出中断来执行)。 - Orestis P.
显示剩余3条评论

5

今天我也遇到了类似的问题,我是这样解决的:

while (listen) // <--- boolean flag to exit loop
{
   if (listener.Pending())
   {
      Thread tmp_thread = new Thread(new ThreadStart(() =>
      {
         string msg = null;

         TcpClient clt = listener.AcceptTcpClient();

         using (NetworkStream ns = clt.GetStream())
         using (StreamReader sr = new StreamReader(ns))
         {
            msg = sr.ReadToEnd();
         }

         Console.WriteLine("Received new message (" + msg.Length + " bytes):\n" + msg);
      }
      tmp_thread.Start();
   }
   else
   {
       Thread.Sleep(100); //<--- timeout
   }
}

我的循环没有在等待连接时被卡住,它接受了多个连接。


编辑:下面的代码片段是使用任务而不是线程的async等效代码。请注意,该代码包含C#-8构造。

private static TcpListener listener = .....;
private static bool listen = true; // <--- boolean flag to exit loop


private static async Task HandleClient(TcpClient clt)
{
    using NetworkStream ns = clt.GetStream();
    using StreamReader sr = new StreamReader(ns);
    string msg = await sr.ReadToEndAsync();

    Console.WriteLine($"Received new message ({msg.Length} bytes):\n{msg}");
}

public static async void Main()
{
    while (listen)
        if (listener.Pending())
            await HandleClient(await listener.AcceptTcpClientAsync());
        else
            await Task.Delay(100); //<--- timeout
}

通常,当我想从外部管理监听并有可能适当地停止它时,我也会使用这个解决方案。 我面临的唯一问题是,如果将睡眠命令设置得太高,会产生太多的延迟,并且如果设置得更低,则会消耗CPU。有没有什么好的解决办法? - Cpt Balu
我相信最好的解决方案是使用 async/await 结合 "Task.Delay(...)"。当我可以访问我的电脑时,我会尝试进一步阐述。 - unknown6656
好的,让我们看看它是如何工作的。 我的另一个想法是继续使用AcceptTcpClient而无需嵌套到睡眠等待循环中,并在另一个单独的线程中启动它,然后在想要完成监听时强制终止它。这会抛出异常,必须加以处理。虽然不太优美,但似乎是速度最快的解决方案,我只是想知道是否有类似但更好的解决方案。 - Cpt Balu
我更新了我的答案,加入了使用“Task.Delay(…)”的异步代码。使用任务进行异步编程被认为是线程基础构造的“继承者”。 - unknown6656
最好的方法就是在while循环中等待listener.AcceptTcpClientAsync。如果你想要能够从外部关闭监听套接字,那么只需使用取消令牌来关闭套接字,并捕获AcceptTcpClientAsync()抛出的套接字异常即可。 - Rowan Smith

2
基本思路是,有一个侦听器套接字始终在给定的 IP 地址和端口号上监听。当有连接请求时,侦听器接受连接,并使用 tcpclient 对象获取远程端点,直到连接关闭或丢失。

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