首页 > 解决方案 > TcpListener backlog concept misunderstanding

问题描述

I'm trying to understand the backlog parameter of TcpListener class but I struggle on how to achieve maximum number of pending connections at same time so I can test it.

I have a sample async server and client code. MSDN says that the backlog is the maximum length of the pending connections queue. I made the server listen for connections all the time and the client is connecting 30 times. What I expect is after the 20th request to throw a SocketException in the client because the backlog is set to 20. Why doesn't it block it?

My second misunderstanding is do I really need to put my logic of the accepted connection in a new thread assuming there is a slow operation which takes around 10 seconds e.g. sending a file over the TCP? Currently, I put my logic in a new Thread, I know it's not the best solution and instead I should use a ThreadPool but the question is principal. I tested it by changing the client side's loop to 1000 iterations and if my logic is not in a new thread, the connections were getting blocked after the 200th connection probably because Thread.Sleep slows the main thread each time by 10 seconds and the main thread is responsible for all the accept callbacks. So basically, I explain it myself as the following: if I want to use the same concept, I have to put my AcceptCallback logic in a new thread like I did or I have to do something like the accepted answer here: TcpListener is queuing connections faster than I can clear them. Am I right?

Server code:

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

Client code:

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

Edit: Since my second question might be a little bit confusing, I will post my code which includes sent messages and the question is should I leave it like that or put the NetworkStream in a new thread?

Server:

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: https://stackoverflow.com/questions/2745401/tcplistener-is-queuing-connections-faster-than-i-can-clear-them
            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.");
        }
    }
}

Client:

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

标签: c#asynchronousclient-server

解决方案


侦听积压在 RFC 6458 中定义,并告诉操作系统在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在您的示例中,您可以在主线程的循环末尾添加一个睡眠,并提高连接速率。

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

  • 客户端/互联网/操作系统可以填满队列的速率
  • 操作系统/服务器可以处理队列的速率

我不建议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.");            
    }

推荐阅读