Каков самый быстрый способ отправки данных между клиентом и сервером?

Я хочу реализовать связь между двумя серверами. На стороне клиента: один поток подготавливает данные и ставит их в очередь. Второй поток извлекает данные из очереди, сериализует их и отправляет на сервер. На стороне сервера: получает данные (байты) и ставит их в очередь. Дальнейшая обработка поставленных в очередь данных не имеет значения.

Только для целей тестирования:

  1. Я запускаю клиент и сервер на одной машине.
  2. Я имитирую очередь, инициируя records со строкой длиной 70 байт (объект, который я хочу отправить, имеет размер около 70 байт). См. код Client->StartClient ниже.
  3. Я отправляю данные итеративно. Данные будут поставлены в очередь очень быстро, поэтому я хочу отправить их, как только они попадут в очередь. Я имитирую его, отправляя его итеративно.

Я использовал WCF как для клиента, так и для сервера для отправки данных, но для отправки 70 МБ (1 миллион записей по 70 байт каждая) потребовалось более 5 минут. Тогда я решил использовать Socket. Я использовал асинхронный подход, и отправка 70 МБ заняла несколько минут. Насколько я понимаю, при наличии клиента и сервера на одном компьютере отправка 70 МБ НЕ должна занимать несколько минут. Это должно занять максимум несколько секунд, верно? Как мне его ускорить?

P.S. Я знаю, что вы можете сказать «накопить, а затем отправить». Но я хочу иметь возможность отправлять данные, как только они попадут в очередь.

Клиент:

public class StateObject
{
    // Client socket.
    public Socket workSocket = null;
    // Size of receive buffer.
    public const int BufferSize = 256;
    // Receive buffer.
    public byte[] buffer = new byte[BufferSize];
    // Received data string.
    public StringBuilder sb = new StringBuilder();
}

public class AsynchronousClient
{
    // The port number for the remote device.
    private const int port = 11000;

    // ManualResetEvent instances signal completion.
    private static ManualResetEvent connectDone =
        new ManualResetEvent(false);
    private static ManualResetEvent sendDone =
        new ManualResetEvent(false);
    private static ManualResetEvent receiveDone =
        new ManualResetEvent(false);

    // The response from the remote device.
    private static String response = String.Empty;

    private static void StartClient()
    {
        // Connect to a remote device.
        try
        {                
            IPHostEntry ipHostInfo = Dns.Resolve("MY_HOST");
            IPAddress ipAddress = ipHostInfo.AddressList[0];
            IPEndPoint remoteEP = new IPEndPoint(ipAddress, port);

            // Create a TCP/IP socket.
            Socket client = new Socket(AddressFamily.InterNetwork,
                SocketType.Stream, ProtocolType.Tcp);

            // Connect to the remote endpoint.
            client.BeginConnect(remoteEP,
                new AsyncCallback(ConnectCallback), client);
            connectDone.WaitOne();

//============================================================
            var records = new List<string>();                
            for (int i = 0; i < 1000000; i++)
            {
                records.Add(new string('a', 70));
            }

            var serializer = new BinaryFormatter();
            var stopWatch = new Stopwatch();
            stopWatch.Start();                
            foreach (var rec in records)
            {
                using (var ms = new MemoryStream())
                {
                    serializer.Serialize(ms, rec);
                    var serData = ms.ToArray();

                    // Send test data to the remote device.
                    Send(client, serData);                                                  
                }
            }
            stopWatch.Stop();    
//================================================================                                      
            // Release the socket.
            client.Shutdown(SocketShutdown.Both);
            client.Close();

        }
        catch (Exception e)
        {
            Console.WriteLine(e.ToString());
        }
    }

    private static void ConnectCallback(IAsyncResult ar)
    {
        try
        {
            // Retrieve the socket from the state object.
            Socket client = (Socket)ar.AsyncState;

            // Complete the connection.
            client.EndConnect(ar);

            Console.WriteLine("Socket connected to {0}",
                client.RemoteEndPoint.ToString());

            // Signal that the connection has been made.
            connectDone.Set();
        }
        catch (Exception e)
        {
            Console.WriteLine(e.ToString());
        }
    }

    private static void Send(Socket client, byte[] data)
    {
        // Convert the string data to byte data using ASCII encoding.
        byte[] byteData = data;// Encoding.ASCII.GetBytes(data);

        // Begin sending the data to the remote device.
        client.BeginSend(byteData, 0, byteData.Length, 0,
            new AsyncCallback(SendCallback), client);
    }

    private static void SendCallback(IAsyncResult ar)
    {
        try
        {
            // Retrieve the socket from the state object.
            Socket client = (Socket)ar.AsyncState;

            // Complete sending the data to the remote device.
            int bytesSent = client.EndSend(ar);
            Console.WriteLine("Sent {0} bytes to server.", bytesSent);                   
        }
        catch (Exception e)
        {
            Console.WriteLine(e.ToString());
        }
    }

    public static int Main(String[] args)
    {
        StartClient();
        return 0;
    }
}

Сервер:

public class StateObject
{
    // Client  socket.
    public Socket workSocket = null;
    // Size of receive buffer.
    public const int BufferSize = 1024;
    // Receive buffer.
    public byte[] buffer = new byte[BufferSize];
    // Received data string.
    public StringBuilder sb = new StringBuilder();
}

public class AsynchronousSocketListener
{
    // Thread signal.
    public static ManualResetEvent allDone = new ManualResetEvent(false);

    public AsynchronousSocketListener()
    {
    }

    public static void StartListening()
    {
        // Data buffer for incoming data.
        byte[] bytes = new Byte[1024];

        IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName());
        IPAddress ipAddress = ipHostInfo.AddressList[0];
        IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 11000);

        // Create a TCP/IP socket.
        Socket listener = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream, ProtocolType.Tcp);

        // Bind the socket to the local endpoint and listen for incoming connections.
        try
        {
            listener.Bind(localEndPoint);
            listener.Listen(100);

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

                // Start an asynchronous socket to listen for connections.
                Console.WriteLine("Waiting for a connection...");
                listener.BeginAccept(
                    new AsyncCallback(AcceptCallback),
                    listener);

                // Wait until a connection is made before continuing.
                allDone.WaitOne();
            }

        }
        catch (Exception e)
        {
            Console.WriteLine(e.ToString());
        }

        Console.WriteLine("\nPress ENTER to continue...");
        Console.Read();

    }

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

        // Get the socket that handles the client request.
        Socket listener = (Socket)ar.AsyncState;
        Socket handler = listener.EndAccept(ar);

        // Create the state object.
        StateObject state = new StateObject();
        state.workSocket = handler;
        handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
            new AsyncCallback(ReadCallback), state);
    }

    public static void ReadCallback(IAsyncResult ar)
    {
        String content = String.Empty;

        // Retrieve the state object and the handler socket
        // from the asynchronous state object.
        StateObject state = (StateObject)ar.AsyncState;
        Socket handler = state.workSocket;

        // Read data from the client socket. 
        int bytesRead = handler.EndReceive(ar);
        Console.WriteLine("Read {0} bytes from socket", bytesRead);

        handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
               new AsyncCallback(ReadCallback), state);
    }                

    public static int Main(String[] args)
    {
        StartListening();
        return 0;
    }
}

sdf


person theateist    schedule 27.08.2015    source источник
comment
Вы пытались использовать именованный канал?   -  person Yosef O    schedule 27.08.2015
comment
Вы ищете скорость с точки зрения наименьшего времени между постановкой сообщения в очередь и его получением или самой высокой пропускной способностью в течение фиксированного промежутка времени? Пробовали ли вы отключить алгоритм Nagle (socket.NoDelay = true;) или изменить размер буфера приема и отправки?   -  person sisve    schedule 27.08.2015
comment
Если ваша метрика связана с использованием ЦП, я бы предложил работать с как можно меньшим количеством протоколов, ИЛИ вы можете использовать Механизм разгрузки TCP, чтобы уменьшить нагрузку на ЦП при выполнении сетевого вызова.   -  person Aron    schedule 27.08.2015
comment
@Yosef, именованные каналы используются, когда процессы выполняются на одном компьютере. В конце концов, мой клиент и сервер будут на разных машинах. Тем временем я запускаю их обоих на отдельных машинах только в целях тестирования.   -  person theateist    schedule 27.08.2015
comment
@SimonSvensson, в целом, согласны ли вы с тем, что отправка миллионов объектов по 70 байт каждый НЕ должна занимать несколько минут при запуске клиента и сервера на одной машине?   -  person theateist    schedule 27.08.2015
comment
1 000 000 объектов/пакетов (70 000 000 байт + служебные данные tcp) за одну минуту — это ... 16 667 пакетов в секунду. Можете ли вы обрабатывать входящие запросы так быстро? (И кстати, какова скорость без вывода на консоль? Запись замедлит ваш цикл.)   -  person sisve    schedule 27.08.2015
comment
Кроме того, если вы используете клиентскую ОС Windows, у вас могут возникнуть проблемы с IIS, ограничивающим количество одновременных подключений.   -  person Aron    schedule 28.08.2015


Ответы (3)


Вы фактически свели на нет преимущества асинхронного шаблона, заблокировав дальнейший прогресс с помощью

sendDone.WaitOne();

Другими словами: вам удалось реализовать блокировку ввода-вывода сокета с помощью асинхронных вызовов: вы используете асинхронную отправку данных, но вы также ждете, пока это будет сделано, не делая ничего другого, поэтому в Эффект, вы могли бы также использовать блокировку вызовов.

Вместо того, чтобы использовать событие sendDone для управления потоком, вы должны определить в обратном вызове EndSend(), нужно ли отправить что-то еще, и начать новый BeginSend().

Невозможно сказать, отвечает ли блокировка событием sendDone за полное время, затраченное на операцию, но это точно не поможет.

person Willem van Rumpt    schedule 27.08.2015
comment
sendDone.WaitOne(); там быть не должно. Я забыл удалить его. Я проводил тесты без него. Я отредактирую свой пост. В целом, согласны ли вы с тем, что НЕ не должно занимать несколько минут, чтобы итеративно отправить миллион записей по 70 байт каждая, когда клиент и сервер находятся на одном компьютере? - person theateist; 27.08.2015
comment
@theateist: Имеет ли значение, согласен я или нет? Я не, кстати, и не должен. Вы можете начать с удаления оператора Console.Writeline() во время тестирования производительности. Только из-за этого время передачи сократилось с [слишком долго, не дождался окончания] до 18 секунд. - person Willem van Rumpt; 27.08.2015
comment
@theateist: И кстати: это не 70 миллионов байт, а 94 миллиона байт. - person Willem van Rumpt; 27.08.2015
comment
@theateist: И хотя вам это может не понравиться: удвоение размера буфера (то есть: размер строки 140 символов и 500000 элементов) сокращает эти 18 секунд до 9 секунд, повторное удвоение уменьшает его до 5 секунд (после того, как я изменил ваш образец программа для правильной реализации асинхронного шаблона для сокетов, за что, кстати, похвала. Редко публикуется что-то, что почти сразу же можно использовать). - person Willem van Rumpt; 27.08.2015
comment
@theateist: И последнее, перед обедом: я работаю в режиме отладки и забыл удалить Console.WriteLine() из серверной части. Удаление этого с размером буфера 3000 и 30000 итераций (~ 90 МБ) делает это примерно за 1,8 секунды. - person Willem van Rumpt; 27.08.2015

Несколько вопросов:

  • Вы пробовали без функций Begin/End? Я не понимаю использование блокировки в цикле отправки клиента. Кроме того, при использовании методов Begin/End возникают небольшие накладные расходы по сравнению с методами Async (создание объекта IAsyncResult при каждом вызове вместо его повторного использования), но я сомневаюсь, что это реальная проблема.
  • Вы рассчитали время сериализации сообщения на стороне клиента? Я уверен, что сериализация один раз выполняется быстро, но, возможно, зацикливание 70 МБ сериализации оказывает давление на сборщик мусора. Попробуйте сериализовать один раз и отправить один и тот же блок (что, по сути, вы и делаете).

Стратегия:

  • Вам нужно найти узкое место, почему бы не использовать профайлер VS? это делает достойную работу.
  • Если вы подозреваете, что проблема связана со скоростью сокета, попробуйте использовать именованный канал или общую память.
  • Если вы подозреваете, что проблема связана с блокировкой, попробуйте без блокировки.
  • Если вы подозреваете, что проблема связана с асинхронными методами, попробуйте методы синхронизации.

Удачи!

person Yosef O    schedule 27.08.2015
comment
Я пытался. Сериализация миллионов объектов итеративно без отправки занимает 15 секунд. Итак, я почти уверен, что проблема в отправке/получении. Я также пробовал без начала/конца, и это тоже медленно. В целом, согласны ли вы с тем, что отправка миллионов записей итеративно, когда клиент и сервер находятся на одном компьютере, НЕ должна занимать несколько минут? - person theateist; 27.08.2015
comment
Настоящий вопрос в том, почему это занимает так много времени... Как я и предлагал, приложите усилия, чтобы найти элементы, где машина хрустит или работает на холостом ходу. Я понимаю ваше разочарование и согласен с тем, что это кажется очень медленным. Пробовали ли вы профилировать свой код и отслеживать время выполнения? Его довольно просто выполнить, и он дает много информации, непосредственно связанной с вашими проблемами. - person Yosef O; 29.08.2015

WCF по умолчанию будет использовать так называемый «буферизованный режим». Это означает, что каждый байт должен быть отправлен до того, как вы начнете обработку. Это имеет два основных последствия.

  1. Требуется как минимум время на передачу последнего байта + время на обработку данных для выполнения запроса.
  2. .net необходимо динамически выделить 70 МБ буфера, но он не знает размер буфера в начале. В результате он фактически выделяет около 150 МБ памяти и много раз копирует данные туда и обратно.

Самый простой способ исправить это.

В вашей привязке установите привязку для использования режима Stream. Затем в контракте WCF замените все параметры метода одним параметром Stream.

person Aron    schedule 27.08.2015
comment
в целом, согласны ли вы с тем, что отправка миллионов объектов итеративно по 70 байт каждый НЕ должна занимать несколько минут при запуске клиента и сервера на одном компьютере? - person theateist; 27.08.2015
comment
@theateist Зависит от машины. Я предполагаю, что это клиентская ОС Windows, так как это ваша отладка на ней. В этом случае IIS позволит вам открыть только 3 одновременных HTTP-соединения. Учитывая это, вы пытаетесь открыть миллион соединений с накладными расходами. Предположим, что для открытия и закрытия соединения требуется примерно 100 мс (плюс накладные расходы из-за того, что вы переполнили очередь HTTP-соединений), да... несколько минут вполне подходят. Попробуйте ваше решение на Windows Server, оно будет быстрее, но недостаточно быстро, так как 1 миллион одновременных подключений — это трудный зверь. - person Aron; 28.08.2015