Создать сервер сокетов TCP, способный обрабатывать тысячи запросов в секунду

С первой попытки я создал основной TCP-сервер как текущий:

public class Tcp
{
    private  TcpListener listener { get; set; }
    private  bool accept { get; set; } = false;

    public  void StartServer(string ip, int port)
    {
        IPAddress address = IPAddress.Parse(ip);
        listener = new TcpListener(address, port);

        listener.Start();
        accept = true;
        StartListener();
        Console.WriteLine($"Server started. Listening to TCP clients at {ip}:{port}");
    }
    public async void StartListener() //non blocking listener
    {

        listener.Start();
        while (true)
        {
            try
            {
                TcpClient client = await listener.AcceptTcpClientAsync().ConfigureAwait(false);
                HandleClient(client);
            }
            finally { }
        }
    }
    private void HandleClient(TcpClient client)
    {
        try
        {

            NetworkStream networkStream = client.GetStream();
            byte[] bytesFrom = new byte[20];
            networkStream.Read(bytesFrom, 0, 20);
            string dataFromClient = System.Text.Encoding.ASCII.GetString(bytesFrom);
            string serverResponse = "Received!";
            Byte[] sendBytes = Encoding.ASCII.GetBytes(serverResponse);
            networkStream.Write(sendBytes, 0, sendBytes.Length);
            networkStream.Flush();
        }
        catch(Exception ex)
        {
        }
    }
}

Я написал тестовый код клиента, который отправляет и записывает количество запросов в секунду

public class Program
{
    private volatile static Dictionary<int, int> connections = new Dictionary<int, int>();
    private volatile static int fail = 0;
    private static string message = "";
    public static void Main(string[] args)
    {
        ServicePointManager.DefaultConnectionLimit = 1000000;
        ServicePointManager.Expect100Continue = false;
        for (int i = 0; i < 512; i++)
        {
            message += "T";
        }

        int taskCount = 10;
        int requestsCount = 1000;
        var taskList = new List<Task>();
        int seconds = 0;
        Console.WriteLine($"start : {DateTime.Now.ToString("mm:ss")} ");

        for (int i = 0; i < taskCount; i++)
        {

            taskList.Add(Task.Factory.StartNew(() =>
            {
                for (int j = 0; j < requestsCount; j++)
                {
                    Send();
                }
            }));
        }
        Console.WriteLine($"threads stablished : {DateTime.Now.ToString("mm: ss")}");
        while (taskList.Any(t => !t.IsCompleted)) { Thread.Sleep(5000); }
        Console.WriteLine($"Compelete : {DateTime.Now.ToString("mm: ss")}");
        int total = 0;
        foreach (KeyValuePair<int, int> keyValuePair in connections)
        {
            Console.WriteLine($"{keyValuePair.Key}:{keyValuePair.Value}");
            total += keyValuePair.Value;
            seconds++;
        }
        Console.WriteLine($"succeded:{total}\tfail:{fail}\tseconds:{seconds}");
        Console.WriteLine($"End : {DateTime.Now.ToString("mm: ss")}");
        Console.ReadKey();
    }

    private static void Send()
    {
        try
        {
            TcpClient tcpclnt = new TcpClient();
            tcpclnt.ConnectAsync("192.168.1.21", 5678).Wait();
            String str = message;
            Stream stm = tcpclnt.GetStream();

            ASCIIEncoding asen = new ASCIIEncoding();
            byte[] ba = asen.GetBytes(str);

            stm.Write(ba, 0, ba.Length);

            byte[] bb = new byte[100];
            int k = stm.Read(bb, 0, 100);
            tcpclnt.Close();
            lock (connections)
            {
                int key = int.Parse(DateTime.Now.ToString("hhmmss"));
                if (!connections.ContainsKey(key))
                {
                    connections.Add(key, 0);
                }
                connections[key] = connections[key] + 1;
            }
        }
        catch (Exception e)
        {
            lock (connections)
            {
                fail += 1;
            }
        }
    }
}

когда я тестирую его на локальном компьютере, я получаю максимальное количество 4000 запросов в секунду, а когда я загружаю его на локальный Lan, он уменьшается до 200 запросов в секунду.

Вопрос: как я могу улучшить производительность сервера? Какой правильный способ нагрузочного тестирования сокет-серверов?

1 ответ

Решение

У вас может быть "неблокирующий слушатель", но когда подключается какой-либо конкретный клиент, он посвящает себя только этому клиенту, пока тот клиент не отправит сообщение и ответ не будет отправлен обратно. Это не будет хорошо масштабироваться.

Я обычно не фанат async void, но это соответствует вашему текущему коду:

public async void StartListener() //non blocking listener
{

    listener.Start();
    while (true)
    {
        TcpClient client = await listener.AcceptTcpClientAsync().ConfigureAwait(false);
        HandleClient(client);
    }
}
private async void HandleClient(TcpClient client)
{
    NetworkStream networkStream = client.GetStream();
    byte[] bytesFrom = new byte[20];
    int totalRead = 0;
    while(totalRead<20)
    {
        totalRead += await networkStream.ReadAsync(bytesFrom, totalRead, 20-totalRead).ConfigureAwait(false);
    }
    string dataFromClient = System.Text.Encoding.ASCII.GetString(bytesFrom);
    string serverResponse = "Received!";
    Byte[] sendBytes = Encoding.ASCII.GetBytes(serverResponse);
    await networkStream.WriteAsync(sendBytes, 0, sendBytes.Length).ConfigureAwait(false);
    networkStream.Flush(); /* Not sure necessary */
}

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

Если вы не гарантированы, что ваши клиенты всегда будут отправлять 20-байтовое сообщение этому коду, вам нужно сделать что-то еще, чтобы сервер знал, сколько данных нужно прочитать. Обычно это делается путем добавления префикса сообщения к его длине или использования некоторой формы значения часового, чтобы указать конец. Обратите внимание, что даже с префиксом длины вы не гарантированно прочитаете всю длину за один раз, поэтому вам также потребуется использовать цикл чтения, как описано выше, чтобы сначала узнать длину.


Если переключить все на async не дает вам нужный вам масштаб, то вам нужно отказаться от использования NetworkStream и начать работать на Socket уровень, а именно с асинхронными методами, предназначенными для работы с SocketAsyncEventArgs:

Класс SocketAsyncEventArgs является частью набора улучшений класса System.Net.Sockets.Socket, которые предоставляют альтернативный асинхронный шаблон, который может использоваться специализированными высокопроизводительными приложениями сокетов... Приложение может использовать расширенный асинхронный шаблон исключительно или только в целевых горячих областях (например, при получении больших объемов данных).

Главной особенностью этих улучшений является предотвращение повторного выделения и синхронизации объектов во время асинхронного ввода-вывода большого объема...

В новых усовершенствованиях класса System.Net.Sockets.Socket асинхронные операции с сокетами описываются повторно используемыми объектами SocketAsyncEventArgs, выделяемыми и поддерживаемыми приложением...

Другие вопросы по тегам