Подтвердить что ты не робот

Длительные задержки при отправке пакетов UDP

У меня есть приложение, которое принимает, обрабатывает и передает UDP-пакеты.

Все работает нормально, если номера портов для приема и передачи различны.

Если номера портов совпадают, а IP-адреса различны, он обычно работает отлично ИСКЛЮЧАЕТ, когда IP-адрес находится в той же подсети, что и компьютер, на котором запущено приложение. В последнем случае для функции send_to требуется несколько секунд, а не несколько миллисекунд, как обычно.

Rx Port  Tx IP          Tx Port    Result

5001     Same           5002       OK  Delay ~ 0.001 secs
         subnet     

5001     Different      5001       OK  Delay ~ 0.001 secs
         subnet

5001     Same           5001       Fails  Delay > 2 secs
         subnet

Вот короткая программа, которая демонстрирует проблему.

#include <ctime>
#include <iostream>
#include <string>
#include <boost/array.hpp>
#include <boost/asio.hpp>

using boost::asio::ip::udp;
using std::cout;
using std::endl;

int test( const std::string& output_IP)
{
    try
    {
        unsigned short prev_seq_no;

        boost::asio::io_service io_service;

        // build the input socket

        /* This is connected to a UDP client that is running continuously
        sending messages that include an incrementing sequence number
        */

        const int input_port = 5001;
        udp::socket input_socket(io_service, udp::endpoint(udp::v4(), input_port ));

        // build the output socket

        const std::string output_Port = "5001";
        udp::resolver resolver(io_service);
        udp::resolver::query query(udp::v4(), output_IP, output_Port );
        udp::endpoint output_endpoint = *resolver.resolve(query);
        udp::socket output_socket( io_service );
        output_socket.open(udp::v4());

       // double output buffer size
       boost::asio::socket_base::send_buffer_size option( 8192 * 2 );
       output_socket.set_option(option);

        cout  << "TX to " << output_endpoint.address() << ":"  << output_endpoint.port() << endl;



        int count = 0;
        for (;;)
        {
            // receive packet
            unsigned short recv_buf[ 20000 ];
            udp::endpoint remote_endpoint;
            boost::system::error_code error;
            int bytes_received = input_socket.receive_from(boost::asio::buffer(recv_buf,20000),
                                 remote_endpoint, 0, error);

            if (error && error != boost::asio::error::message_size)
                throw boost::system::system_error(error);

            // start timer
            __int64 TimeStart;
            QueryPerformanceCounter( (LARGE_INTEGER *)&TimeStart );

            // send onwards
            boost::system::error_code ignored_error;
            output_socket.send_to(
                boost::asio::buffer(recv_buf,bytes_received),
                output_endpoint, 0, ignored_error);

            // stop time and display tx time
            __int64 TimeEnd;
            QueryPerformanceCounter( (LARGE_INTEGER *)&TimeEnd );
            __int64 f;
            QueryPerformanceFrequency( (LARGE_INTEGER *)&f );
            cout << "Send time secs " << (double) ( TimeEnd - TimeStart ) / (double) f << endl;

            // stop after loops
            if( count++ > 10 )
                break;
        }
    }
    catch (std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }

}
int main(  )
{

    test( "193.168.1.200" );

    test( "192.168.1.200" );

    return 0;
}

Выход из этой программы при работе на компьютере с адресом 192.168.1.101

TX to 193.168.1.200:5001
Send time secs 0.0232749
Send time secs 0.00541566
Send time secs 0.00924535
Send time secs 0.00449014
Send time secs 0.00616714
Send time secs 0.0199299
Send time secs 0.00746081
Send time secs 0.000157972
Send time secs 0.000246775
Send time secs 0.00775578
Send time secs 0.00477618
Send time secs 0.0187321
TX to 192.168.1.200:5001
Send time secs 1.39485
Send time secs 3.00026
Send time secs 3.00104
Send time secs 0.00025927
Send time secs 3.00163
Send time secs 2.99895
Send time secs 6.64908e-005
Send time secs 2.99864
Send time secs 2.98798
Send time secs 3.00001
Send time secs 3.00124
Send time secs 9.86207e-005

Почему это происходит? Есть ли способ уменьшить задержку?

Примечания:

  • Построено с использованием code:: blocks, работающего под различными вариантами Windows

  • Пакет длиной 10000 байтов

  • Проблема исчезает, если я подключу компьютер, запускающий приложение, ко второй сети. Например, WWLAN (сотовая сеть "ракетная палочка" )

Насколько я могу судить, это та ситуация, которую мы имеем:

Это работает (разные порты, одна и та же ЛВС):

введите описание изображения здесь

Это также работает (одни и те же порты, разные LANS):

введите описание изображения здесь

Это НЕ работает (те же порты, одна и та же ЛВС):

введите описание изображения здесь

Кажется, что это работает (те же порты, один и тот же LAN, двухъядерный хост Module2)

введите описание изображения здесь

4b9b3361

Ответ 1

Учитывая, что это наблюдается в Windows для больших датаграмм с адресом назначения несуществующего однорангового узла в пределах той же подсети, что и отправитель, проблема, скорее всего, является результатом блокировки send(), ожидающей Ответ протокола разрешения адресов (ARP), чтобы можно было заполнить кадр ether2 ethernet:

  • При отправке данных кадр ether2 layer2 будет заполнен адресом управления доступом к среде передачи (MAC) следующего перехода на маршруте. Если отправитель не знает MAC-адрес для следующего перехода, он будет транслировать запрос ARP и ответы в кеше. Используя маску подсети отправителя и адрес назначения, отправитель может определить, находится ли следующий прыжок в той же подсети, что и отправитель, или данные должны проходить через шлюз по умолчанию. Исходя из результатов в вопросе при отправке больших дейтаграмм:

    • датаграммы, предназначенные для другой подсети, не имеют задержки, поскольку MAC-адрес шлюза по умолчанию находится в кэше ARP отправителя
    • датаграммы, предназначенные для несуществующего однорангового узла в подсети отправителя, несут задержку, ожидающую разрешения ARP.
  • Сокет размер отправляемого буфера (SO_SNDBUF) устанавливается в 16384 байты, но размер датаграмм отправлены 10000. Неопределенно поведение поведения send(), когда буфер насыщен, но некоторые системы будут наблюдать блокировку send(). В этом случае насыщение будет происходить довольно быстро, если какие-либо датаграммы несут задержку, например, ожидая ответа ARP.

    // Datagrams being sent are 10000 bytes, but the socket buffer is 16384.
    boost::asio::socket_base::send_buffer_size option(8192 * 2);
    output_socket.set_option(option);
    

    Подумайте о том, чтобы позволить ядру управлять размером буфера сокета или увеличивать его на основе ожидаемой пропускной способности.

  • При отправке дейтаграммы с размером, превышающим параметр реестра Windows FastSendDatagramThreshold‌, вызов send() может блокироваться до тех пор, пока датаграмма не будет отправлена. Для получения дополнительной информации см. Сведения о реализации TCP/IP в Microsoft:

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

Если вы наблюдаете задержки для каждого send() для существующего однорангового узла в подсети отправителя, затем профиль и анализ сети:

  • Используйте iperf для измерения пропускной способности сети.
  • Используйте wireshark, чтобы получить более глубокое представление о том, что происходит на данном node. Найдите запрос и ответы ARP.
  • На машине отправителя выполните ping peer, а затем проверьте APR-кеш. Проверьте, есть ли запись кэша для сверстника и что это правильно.
  • Попробуйте использовать другой порт и/или TCP. Это может помочь определить, регулирует ли политика сетей или формирует трафик для определенного порта или протокола.

Также обратите внимание, что отправка дейтаграмм ниже значения FastSendDatagramThreshold в быстрой последовательности при ожидании ARP для устранения может привести к отбрасыванию датаграмм:

ARP ставит в очередь только одну исходящую IP-дейтаграмму для указанного адреса назначения, пока этот IP-адрес разрешен на адрес управления доступом к среде. Если приложение с протоколом User Datagram Protocol (UDP) отправляет несколько дейтаграмм IP на один адрес назначения без каких-либо пауз между ними, некоторые из датаграмм могут быть удалены, если нет записи ARP-кэша, уже присутствующей. Приложение может компенсировать это, вызывая процедуру iphlpapi.dll SendArp(), чтобы установить запись кэша ARP, перед отправкой потока пакетов.

Ответ 2

Хорошо, соберите код (см. ниже). Понятно, что отправка занимает не более одной милисекунды большую часть времени. Это доказывает, что проблема связана с повышением.

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdexcept>
#include <poll.h>
#include <string>
#include <memory.h>
#include <chrono>
#include <stdio.h>

void test( const std::string& remote, const std::string& hello_string, bool first)
{
    try
    {
        const short unsigned input_port = htons(5001);
        int sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock == -1) {
            perror("Socket creation error: ");
            throw std::runtime_error("Could not create socket!");
        }

        sockaddr_in local_addr;
        local_addr.sin_port = input_port;
        local_addr.sin_addr.s_addr = INADDR_ANY;
        if (bind(sock, (const sockaddr*)&local_addr, sizeof(local_addr))) {
            perror("Error: ");
            throw std::runtime_error("Can't bind to port!");
        }

        sockaddr_in remote_addr;
        remote_addr.sin_port = input_port;
        if (!inet_aton(remote.c_str(), &remote_addr.sin_addr))
            throw std::runtime_error("Can't parse remote IP address!");

        std::cout  << "TX to " << remote << "\n";

        unsigned char recv_buf[40000];

        if (first) {
            std::cout << "First launched, waiting for hello.\n";
            int bytes = recv(sock, &recv_buf, sizeof(recv_buf), 0);
            std::cout << "Seen hello from my friend here: " << recv_buf << ".\n";
        }

        int count = 0;
        for (;;)
        {

            std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
            if (sendto(sock, hello_string.c_str(), hello_string.size() + 1, 0, (const sockaddr*)&remote_addr, sizeof(remote_addr)) != hello_string.size() + 1) {
                perror("Sendto error: ");
                throw std::runtime_error("Error sending data");
            }
            std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();

            std::cout << "Send time nanosecs " << std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() << "\n";

            int bytes = recv(sock, &recv_buf, sizeof(recv_buf), 0);
            std::cout << "Seen hello from my friend here: " << recv_buf << ".\n";

            // stop after loops
            if (count++ > 10)
                break;
        }
    }
    catch (std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }

}
int main(int argc, char* argv[])
{
    test(argv[1], argv[2], *argv[3] == 'f');

    return 0;
}

Как и ожидалось, нет задержки. Здесь выводится одна из пар (я запускаю код попарно на двух машинах в одной сети):

./socktest x.x.x.x 'ThingTwo' f
TX to x.x.x.x
First launched, waiting for hello.
Seen hello from my friend here: ThingOne.
Send time nanosecs 17726
Seen hello from my friend here: ThingOne.
Send time nanosecs 6479
Seen hello from my friend here: ThingOne.
Send time nanosecs 6362
Seen hello from my friend here: ThingOne.
Send time nanosecs 6048
Seen hello from my friend here: ThingOne.
Send time nanosecs 6246
Seen hello from my friend here: ThingOne.
Send time nanosecs 5691
Seen hello from my friend here: ThingOne.
Send time nanosecs 5665
Seen hello from my friend here: ThingOne.
Send time nanosecs 5930
Seen hello from my friend here: ThingOne.
Send time nanosecs 6082
Seen hello from my friend here: ThingOne.
Send time nanosecs 5493
Seen hello from my friend here: ThingOne.
Send time nanosecs 5893
Seen hello from my friend here: ThingOne.
Send time nanosecs 5597

Ответ 3

Хорошей практикой является разделение портов Tx и Rx. Я получаю свой собственный класс сокетов из CAsynchSocket, поскольку у него есть насос сообщений, который отправляет системное сообщение, когда данные получены в вашем сокете, и yanks функция OnReceive (либо ваша, если u переопределяет базовую виртуальную функцию или значение по умолчанию, если вы не