Недопонимание концепции отставания TcpListener - PullRequest
1 голос
/ 15 мая 2019

Я пытаюсь понять параметр backlog класса TcpListener, но я изо всех сил пытаюсь добиться максимального количества одновременно ожидающих соединений, чтобы я мог его протестировать.

У меня есть пример асинхронного сервера и клиентский код. MSDN говорит, что задержка - это максимальная длина очереди ожидающих соединений.Я заставил сервер постоянно прослушивать соединения, а клиент подключался 30 раз.Я ожидаю, что после 20-го запроса выбрасывается SocketException в клиенте, поскольку отставание установлено на 20. Почему оно не блокирует его?

Мое второе недоразумение: действительно ли мне нужно поместить свою логику принятого соединения в новый поток, предполагая, что есть медленная операция, которая занимает около 10 секунд, например, отправка файла через TCP?В настоящее время я помещаю свою логику в new Thread, я знаю, что это не лучшее решение, и вместо этого я должен использовать ThreadPool, но вопрос принципиален.Я проверил это, изменив цикл на стороне клиента на 1000 итераций, и если моя логика не в новом потоке, соединения были заблокированы после 200-го соединения, вероятно, потому что Thread.Sleep замедляет основной поток каждый раз на 10 секунд и основной потокотвечает за все обратные вызовы.В общем, я объясняю это сам следующим образом: если я хочу использовать ту же концепцию, я должен поместить свою логику AcceptCallback в новый поток, как я, или я должен сделать что-то вроде принятого ответа здесь: TcpListenerставит в очередь соединения быстрее, чем я могу их очистить .Я прав?

Код сервера:

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

Редактировать: так как мой второй вопрос может быть немного запутанным,Я опубликую свой код, который включает отправленные сообщения, и вопрос в том, должен ли я оставить его таким или поставить 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: /1815428/tcplistener-stavit-v-ochered-soedineniya-bystree-chem-ya-mogu-ih-ochistit
            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();
        }
    }
}

1 Ответ

1 голос
/ 17 мая 2019

Журнал ожидания прослушивания определен в 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 напрямую, вот как выглядит сервер, используя Task и 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.");            
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...