TcpListener积压概念误解

TcpListener backlog concept misunderstanding

我正在尝试了解 TcpListener class 的积压参数,但我在如何同时实现最大挂起连接数方面苦苦挣扎,因此我可以对其进行测试。

我有一个示例异步服务器和客户端代码。 MSDN 表示积压是挂起连接队列的最大长度。我让服务器一直监听连接,客户端连接了 30 次。我期望的是在第20个请求之后在客户端抛出一个SocketException因为积压设置为20。为什么不阻止它?

我的第二个误解是我是否真的需要将接受连接的逻辑放在一个新线程中,假设有一个缓慢的操作需要大约 10 秒,例如通过 TCP 发送文件?目前,我将我的逻辑放在 new Thread 中,我知道这不是最佳解决方案,我应该使用 ThreadPool,但问题是主要的。我通过将客户端的循环更改为 1000 次迭代来测试它,如果我的逻辑不在新线程中,则连接在第 200 次连接后被阻塞可能是因为 Thread.Sleep 每次都会将主线程减慢 10 秒,并且主线程负责所有的接受回调。所以基本上,我自己解释如下:如果我想使用相同的概念,我必须像我一样将我的 AcceptCallback 逻辑放在一个新线程中,或者我必须像这里接受的答案一样做一些事情:TcpListener is queuing connections faster than I can clear them.我说得对吗?

服务器代码:

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Server
{
    class Program
    {
        private static readonly ManualResetEvent _mre = new ManualResetEvent(false);

        static void Main(string[] args)
        {
            TcpListener listener = new TcpListener(IPAddress.Any, 80);

            try
            {
                listener.Start(20); 

                while (true)
                {
                    _mre.Reset();

                    Console.WriteLine("Waiting for a connection...");
                    listener.BeginAcceptTcpClient(new AsyncCallback(AcceptCallback), listener);

                    _mre.WaitOne();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        private static void AcceptCallback(IAsyncResult ar)
        {
            _mre.Set();

            TcpListener listener = (TcpListener)ar.AsyncState;
            TcpClient client = listener.EndAcceptTcpClient(ar);

            IPAddress ip = ((IPEndPoint)client.Client.RemoteEndPoint).Address;
            Console.WriteLine($"{ip} has connected!");

            // Actually I changed it to ThreadPool
            //new Thread(() =>
            //{
            //  Console.WriteLine("Sleeping 10 seconds...");
            //  Thread.Sleep(10000);
            //  Console.WriteLine("Done");
            //}).Start();

            ThreadPool.QueueUserWorkItem(new WaitCallback((obj) =>
            {
                Console.WriteLine("Sleeping 10 seconds...");
                Thread.Sleep(10000);
                Console.WriteLine("Done");
            }));

            // Close connection
            client.Close();
        }
    }
}

客户代码:

using System;
using System.Net.Sockets;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 30; i++)
            {
                Console.WriteLine($"Connecting {i}");

                using (TcpClient client = new TcpClient()) // because once we are done, we have to close the connection with close.Close() and in this way it will be executed automatically by the using statement
                {
                    try
                    {
                        client.Connect("localhost", 80);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }
            }

            Console.ReadKey();
        }
    }
}

编辑:由于我的第二个问题可能有点令人困惑,我将 post 我的代码,其中包含已发送的消息,问题是我应该这样保留它还是将 NetworkStream 在新线程中?

服务器:

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace Server
{
    class Program
    {
        private static readonly ManualResetEvent _mre = new ManualResetEvent(false);

        static void Main(string[] args)
        {
            // MSDN example: https://docs.microsoft.com/en-us/dotnet/framework/network-programming/asynchronous-server-socket-example
            // A better solution is posted here: 
            TcpListener listener = new TcpListener(IPAddress.Any, 80);

            try
            {
                // Backlog limit is 200 for Windows 10 consumer edition
                listener.Start(5);

                while (true)
                {
                    // Set event to nonsignaled state
                    _mre.Reset();

                    Console.WriteLine("Waiting for a connection...");
                    listener.BeginAcceptTcpClient(new AsyncCallback(AcceptCallback), listener);

                    // Wait before a connection is made before continuing
                    _mre.WaitOne();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        private static void AcceptCallback(IAsyncResult ar)
        {
            // Signal the main thread to continue
            _mre.Set();

            TcpListener listener = (TcpListener)ar.AsyncState;
            TcpClient client = listener.EndAcceptTcpClient(ar);

            IPAddress ip = ((IPEndPoint)client.Client.RemoteEndPoint).Address;
            Console.WriteLine($"{ip} has connected!");

            using (NetworkStream ns = client.GetStream())
            {
                byte[] bytes = Encoding.Unicode.GetBytes("test");
                ns.Write(bytes, 0, bytes.Length);
            }

            // Use this only with backlog 20 in order to test
            Thread.Sleep(5000);

            // Close connection
            client.Close();
            Console.WriteLine("Connection closed.");
        }
    }
}

客户:

using System;
using System.Net.Sockets;
using System.Text;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 33; i++)
            {
                Console.WriteLine($"Connecting {i}");

                using (TcpClient client = new TcpClient()) // once we are done, the using statement will do client.Close()
                {
                    try
                    {
                        client.Connect("localhost", 80);

                        using (NetworkStream ns = client.GetStream())
                        {
                            byte[] bytes = new byte[100];
                            int readBytes = ns.Read(bytes, 0, bytes.Length);
                            string result = Encoding.Unicode.GetString(bytes, 0, readBytes);
                            Console.WriteLine(result);
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }
            }

            Console.ReadKey();
        }
    }
}

listen backlog is defined in RFC 6458 并告诉 OS accept queue.

中允许的最大套接字数

传入连接由 TCP/IP 堆栈放置在此队列中,并在服务器调用 Accept 处理新连接时删除。

在您的问题中,两个版本的服务器代码都在主线程的循环中调用 Accept,并等待 AcceptCallback 开始,然后再进行另一个接受调用。这导致相当快排空队列。

为了演示侦听队列溢出,最简单的方法是减慢 服务器的接受率 - 例如减慢到零:

    var serverEp = new IPEndPoint(IPAddress.Loopback, 34567);
    var serverSocket = new TcpListener(serverEp);        
    serverSocket.Start(3);
    for (int i = 1; i <= 10; i++)
    {
        var clientSocket = new TcpClient();
        clientSocket.Connect(serverEp);
        Console.WriteLine($"Connected socket {i}");
    }   

在您的示例中,您可以在主线程的 Accept 循环结束时添加一个睡眠,并提高连接率。

在现实世界中,最佳积压取决于:

  • 客户端/互联网/OS 可以填充队列的速率
  • OS/服务器可以处理队列的速率

我不建议直接使用 Thread,这里是服务器使用 TaskSocket Task Extensions 的样子:

    static async Task Main(string[] args)
    {
        var server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        server.Bind(new IPEndPoint(IPAddress.Any, 80));
        server.Listen(5);            
        while (true)
        {
            var client = await server.AcceptAsync();
            var backTask = ProcessClient(client); 
        }  
    }

    private static async Task ProcessClient(Socket socket)
    {
        using (socket)
        {
            var ip = ((IPEndPoint)(socket.RemoteEndPoint)).Address;
            Console.WriteLine($"{ip} has connected!");

            var buffer = Encoding.Unicode.GetBytes("test");
            await socket.SendAsync(buffer, SocketFlags.None);
        }
        Console.WriteLine("Connection closed.");            
    }