Qt QTcpSocket Наложение данных при чтении вызывает недопустимое поведение TCP во время чтения и записи с высокой пропускной способностью - PullRequest
0 голосов
/ 03 августа 2020

Сводка: Некоторая часть памяти в сокете TCP будет перезаписана другими входящими данными.

Приложение: Система клиент / сервер, использующая TCP в Qt (QTcpSocket и QTcpServer) . Клиент запрашивает у сервера фрейм (простое строковое сообщение) и ответ (Сервер -> Клиент), который состоит из этого фрейма (614400 байт для целей тестирования). Размеры кадра устанавливаются заранее и фиксированы.

Детали реализации: Из гарантий протокола TCP (Сервер -> Клиент) я знаю, что у меня должна быть возможность читать 614400 байтов из сокета и что они в порядке. Если какой-либо из этих двух моментов не удается, соединение должно быть неудачным.

Важный код: Предполагается, что сокет подключен.

Этот код запрашивает кадр с сервера. . Известная как функция GetFrame().

// Prompt the server to send a frame over
if(socket->isWritable() && !is_receiving) { // Validate that socket is ready
    is_receiving = true; // Forces only one request to go out at a time
    qDebug() << "Getting frame from socket..." << image_no;
    int written = SafeWrite((char*)"ReadyFrame"); // Writes then flushes the write buffer
    if (written == -1) {
        qDebug() << "Failed to write...";
        return temp_frame.data();
    }
    this->SocketRead();
    is_receiving = false;
}
qDebug() << image_no << "- Image Received";
image_no ++;
return temp_frame.data();

Этот код ожидает только что запрошенного для чтения кадра. Это функция SocketRead()

size_t byte_pos = 0;
qint64 bytes_read = 0;
do {
    if (!socket->waitForReadyRead(500)) { // If it timed out return existing frame
        if (!(socket->bytesAvailable() > 0)) {
            qDebug() << "Timed Out" << byte_pos;
            break;
        }
    }
    bytes_read = socket->read((char*)temp_frame.data() + byte_pos, frame_byte_size - byte_pos);
    if (bytes_read < 0) {
        qDebug() << "Reading Failed" << bytes_read << errno;
        break;
    }
    byte_pos += bytes_read;

} while (byte_pos < frame_byte_size && is_connected); // While we still have more pixels
qDebug() << "Finished Receiving Frame: " << byte_pos;

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

Проблема, с которой я столкнулся, заключается в том, что операция чтения QTcpSocket - это пропуск байтов способами, которые не соответствуют гарантиям протокола TCP. Так как я пропускаю байты, я не дохожу до конца while l oop и просто "Time Out". Почему это происходит?

Что я сделал до сих пор: Данные, которые отправляет сервер, напрямую конвертируются в целые числа uint16_t (короткие), которые используются в других части клиента. Я изменил сервер, чтобы просто выводить данные, которые просто подсчитывают добавление единицы для каждого отправленного числа. Так как тип данных - uint16_t, а количество байтов превышает максимальное число для этого целочисленного типа, int-16 будет l oop каждые 65535.

Это программное обеспечение для визуализации данных, поэтому эта конфигурация отладки (на на стороне клиента) приводит к примерно следующему: Debugging visual output

I have determined (and as you can see a little at the bottom of the graphic) that some bytes are being skipped. In the memory of temp_frame it is possible to see the exact point at which the memory skipped:

Memory trace for skipping

Under correct circumstances, this should count up sequentially.

From Wireshark and following this specific TCP connection I have determined that all of the bytes are in fact arriving (all 6114400), and that all the numbers are in order (I used a python script to ensure counting was sequential).

This is work on an open source project so this - это вся база кода для клиента.

В целом, я не понимаю, как я мог бы это сделать что-то не так в этом решении, все, что я делаю, это читаю из сокета стандартным способом.

1 Ответ

0 голосов
/ 04 августа 2020

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

С (например) GigE скорость передачи данных составляет ~ 100 МБ / с. При [общем] объеме буферного пространства ядра 614400 оно будет пополняться ~ 175 раз в секунду. ИМО, это все еще маловато. Когда я использовал SO_RCVBUF [для коммерческого продукта], я использовал минимум 8 МБ. Это дает широкий (более) запас для задержек переключения задач.

Попробуйте установить что-нибудь огромное например 100 МБ, чтобы исключить это как фактор [во время тестирования / запуска].

Во-первых, важно убедиться, что ядро ​​и драйвер NI C могут обрабатывать пропускную способность / задержку.

Возможно, вы получаете слишком много прерываний в секунду, и накладные расходы пролога / эпилога ISR могут быть слишком высокая. Драйвер карты NI C может реализовать драйвер опроса и прерывания с NAPI для карт ethe rnet.

См .: https://serverfault.com/questions/241421/napi-vs-adaptive-interrupts

См .: https://01.org/linux-interrupt-moderation

Возможно, у вашего процесса / потока недостаточно высокого приоритета для быстрого планирования.

Вы можете использовать планировщик R / T с sched_setscheduler, SCHED_RR и приоритетом (например) 8. Примечание: если значение выше 11 убивает систему, потому что при 12 и выше у вас более высокий приоритет, чем у большинства внутренних потоков ядра - не очень хорошо.

Возможно, вам потребуется отключить балансировку IRQ и установить привязку IRQ к одному ядру ЦП.

Затем вы можете установить свой входной процесс / поток, заблокированный для этого ядра [с помощью sched_setaffinity и / или pthread_setaffinity].

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

Вы можете mmap буферы сокета ядра с помощью PACKET_MMAP. См .: https://sites.google.com/site/packetmmap/

Я был бы осторожен с накладными расходами на ваш вывод qDebug. Похоже на реализацию типа iostream. Накладные расходы могут быть значительными. Это может значительно замедлить работу.

То есть вы не измеряете производительность своей системы. Вы измеряете производительность своей системы плюс код отладки.

Когда мне приходилось отлаживать / отслеживать такие вещи, я использовал [собственный] журнал «событий» реализована с помощью кольцевой очереди в памяти с фиксированным числом элементов.

Вызовы отладки, такие как:

eventadd(EVENT_TYPE_RECEIVE_START,some_event_specific_data);

Здесь eventadd заполняет «событие» фиксированного размера struct с тип события, данные события и метка времени найма (например, struct timespec из clock_gettime(CLOCK_MONOTONIC,...).

Накладные расходы каждого такого вызова довольно низкие. События просто сохраняются в кольце событий. Только последние Запоминаются N.

В какой-то момент ваша программа запускает дамп этой очереди в файл и завершается.

Этот механизм похож на [и смоделирован] H / W logi c анализатор. Он также похож на dtrace

Вот пример элемента события:

struct event {
    long long evt_tstamp;               // timestamp
    int evt_type;                       // event type
    int evt_data;                       // type specific data
};
...