2

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://learn.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();
        }
    }
}
nop
  • 4,711
  • 6
  • 32
  • 93
  • Your client is closing the connection as soon as it connects, and the connects are serial. In order to test the backlog you´d need to fire several Connects at the same time and keep the connectons open for some time. – C. Gonzalez May 15 '19 at 20:38
  • Once I put a Thread.Sleep in the main thread, this is happening. ``Connecting 202 No connection could be made because the target machine actively refused it 127.0.0.1:80``. It won't handle more than 200 connections. – nop May 15 '19 at 20:42
  • The 200 connections are probably an (cousumer) OS limit. Server OS should allow much more. – C. Gonzalez May 15 '19 at 20:46
  • 1
    Oh, you seem to be right. I changed the backlog to 20 and it blocked it earlier (on the 20th) but it is interesting that if I put the backlog to 1000, it keeps blocking it at 200. So the maximum backlog value on Win 10 Pro 1809 seems to be 200. A solution would be the answer from https://stackoverflow.com/questions/2745401/tcplistener-is-queuing-connections-faster-than-i-can-clear-them. What about the second question? – nop May 15 '19 at 20:59
  • AcceptCallback is already running in a threadpool thread (different from the listener), as far as I know. You could print out the thread id to confirm. For high performance, high volume servers you may want to look into [SocketAsyncEventArgs](https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socketasynceventargs?redirectedfrom=MSDN&view=netframework-4.7.2) though. – C. Gonzalez May 16 '19 at 18:15

1 Answers1

1

The listen backlog is defined in RFC 6458 and tells the OS the maximum number of sockets allowed in the accept queue.

Incoming connections are placed in this queue by the TCP/IP stack and removed when the server calls Accept to handle the new connection.

In your question, both versions of the server code call Accept in a loop from the main thread, and wait for AcceptCallback to start before making another accept call. This results in quite fast draining of the queue.

To demonstrate listen queue overflow, its easiest to slow down your server's rate of accepting - E.g. slow it down to zero:

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

In your examples you could just add a sleep at the end of the Accept loop in the main thread, and increase the connection rate.

In the real world, the optimal backlog depends on:

  • The rate that the clients / internet / OS can fill the queue
  • The rate that the OS / server can process the queue

I don't recommend using Thread directly, here's how the server looks using Task and Socket 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.");            
    }
Community
  • 1
  • 1
Peter Wishart
  • 11,600
  • 1
  • 26
  • 45