У першій части серії статей , автор розглянув проблему налаштування та об'єднання повідомлень та зменшення їх навантаження у рамках телеметричних датчиків
Ця частина присвячена корисному навантаженню від повідомлень та її оптимізації.
Існує безліч методів серіалізації об'єкта за допомогою Qt. У першій частині серії статей ми використовували JSON. Для цього вся інформація про датчик зберігається в QJsonObject , а QJsonDocument піклується про потік значень QByteArray.
QJsonObject jobject; jobject[ "SensorID" ] = m_id; jobject[ "AmbientTemperature" ] = m_ambientTemperature; jobject[ "ObjectTemperature" ] = m_objectTemperature; jobject[ "AccelerometerX" ] = m_accelerometerX; jobject[ "AccelerometerY" ] = m_accelerometerY; jobject[ "AccelerometerZ" ] = m_accelerometerZ; jobject[ "Altitude" ] = m_altitude; jobject[ "Light" ] = m_light; jobject[ "Humidity" ] = m_humidity; QJsonDocument doc( jobject ); return doc.toJson();
JSON має кілька переваг:
- Текстовий JSON є описовим, що робить його читаним для людей;
- інформація структурована;
- Обмін основної інформації простий і зрозумілий;
- JSON дозволяє розширювати повідомлення додатковими значеннями;
- Існує безліч рішень для сприйняття та аналізу JSON у хмарних рішеннях.
Однак цей підхід має певні обмеження. По-перше, створення повідомлення JSON може бути тяжкою операцією, що займає багато циклів. Бенчмарк другої частини тестування показує, що серіалізація та десеріалізація 10.000 повідомлень займає близько 263 мс. Це може бути не схожим на значну кількість повідомлень, але в цьому контексті час відповідає енергії. Це може суттєво впливати на датчик, який призначений для його використання без зарядки тривалий час.
Інший аспект полягає в тому, що корисне навантаження для MQTT повідомлення на оновлення датчика становить 346 байт. Враховуючи, що датчик відправляє лише вісім дублів і один обмежений рядок, це може бути потенційно більшими за витрати обчислювальних ресурсів.
всередині коментарів попереднього посту рекомендовано використовувати QJsonDocument : Compact (компакт), який зменшує розмір корисного навантаження, в середньому до 290 байт
Як це можна покращити?
Як багато хто з вас знає, існує також двійковий JSON, який може зменшити читаність, але всі інші аспекти, як і раніше, актуальні. Головне у проведенні бенчмарків те, що простий перемикач doc.toJson на doc.toBinaryData подвоїть швидкість тесту, скоротивши ітерацію бенчмарку до 125 мілісекунд.
Перевіривши навантаження, розмір повідомлень становить 338 байт, різниця майже непомітна. Однак це може змінитися в різних сценаріях, наприклад, якщо додати більше рядків у повідомленні.
Залежно від вимог можна додавати сторонні проекти та інші можливості.
Якщо проект знаходиться !в рамках світу Qt", весь потік даних визначений і не змінюється QDataStream , є можливим варіантом .
Додаємо підтримку для цього класу SensorInformation, що вимагає додавання двох операторів.
QDataStream &operator<<(QDataStream &, const SensorInformation &); QDataStream &operator>>(QDataStream &, SensorInformation &);
Реалізація досить проста, нижче показаний приклад серіалізації:
QDataStream &operator<<(QDataStream &out, const SensorInformation &item) { QDataStream::FloatingPointPrecision prev = out.floatingPointPrecision(); out.setFloatingPointPrecision(QDataStream::DoublePrecision); out << item.m_id << item.m_ambientTemperature << item.m_objectTemperature << item.m_accelerometerX << item.m_accelerometerY << item.m_accelerometerZ << item.m_altitude << item.m_light << item.m_humidity; out.setFloatingPointPrecision(prev); return out; }
При використанні QDataStream час бенчмарку склало 26 мілісекунд, що майже в 10 разів швидше ніж текстовий JSON. Крім того, середній розмір повідомлення становить всього 84 байти порівняно з 290. Отже, якщо вищезазначені обмеження прийнятні, QDataStream є хорошим варіантом вирішення поставленої задачі.
Якщо проект дозволяє додавати додаткові сторонні компоненти, одним із найвідоміших рішень серіалізації є протокол буферів Google (protobuf).
Щоб додати protobuf до нашого рішення, потрібно зробити пару змін. По-перше, protobuf використовує IDL для опису структур даних або повідомлень. Тоді, розробка сенсора інформації:
syntax = "proto2" ; package serialtest; message Sensor { required string id = 1; required double ambientTemperature = 2; required double objectTemperature = 3; required double accelerometerX = 4; required double accelerometerY = 5; required double accelerometerZ = 6; required double altitude = 7; required double light = 8; required double humidity = 9; }
Щоб додати генератор коду protobuf (протокол) у проект qmake, необхідно додати додатковий крок компілятора, подібний до цього:
PROTO_FILE = sensor.proto protoc.output = $${OUT_PWD}/${QMAKE_FILE_IN_BASE}.pb.cc protoc.commands = $${PROTO_PATH}/bin/protoc -I=$$relative_path($${PWD}, $${OUT_PWD}) --cpp_out=. ${QMAKE_FILE_NAME} protoc.variable_out = GENERATED_SOURCES protoc.input = PROTO_FILE QMAKE_EXTRA_COMPILERS += protoc
Потім, щоб мати зіставний бенчмарк з точки зору розміру об'єкта, згенерована структура, яка використовується як член для класу SensorInformationProto, який успадковує QObject, як і для прикладу QDataStream і JSON
class SensorInformationProto : public QObject { Q_OBJECT Q_PROPERTY( double ambientTemperature READ ambientTemperature WRITE setAmbientTemperature NOTIFY ambientTemperatureChanged) [...] public : SensorInformationProto( const std::string &pr); [...] std::string serialize() const ; [...] private : serialtest::Sensor m_protoInfo; };
Функція серіалізації proto Info генерується протоколом, тому крок створення корисного навантаження, що передається, виглядає наступним чином:
std::string SensorInformationProto::serialize() const { std::string res; m_protoInfo.SerializeToString(&res); return res; }
Зверніть увагу, що в порівнянні з попередніми рішеннями protobuf використовує рядок STD. Це означає, що ви втрачаєте можливості QString, якщо рядок не зберігається у вигляді масиву байтів (потрібне ручне перетворення). Знову ж таки, це сповільнить весь процес через розбір.
З точки зору ефективності результати контрольних показників виглядають перспективними. Бенчмарк з 10.000 елементів займає всього 5 мс із середнім розміром повідомлення 82 байти.
Як висновок, наступна таблиця показує різні підходи до серіалізації:
| | Розмір корисного навантаження Час (мс)
| JSON (текстовий) 346 | 263 |
| JSON (двійковий) | 338 | 125 |
| QDataStream | 84 | 26 |
| Протобуф | 82 | 5 |
Однією з перспективних альтернатив є CBOR, що в даний час реалізується Thiago Macieira Qt 5.12. Однак, оскільки процес розробки продовжується, ще зарано його включати його до цієї статті. З обговорень результати виглядають багатообіцяючими, зі значною продуктивністю порівняно з JSON, та з усіма його перевагами.
У статті були показані різні підходи до серіалізації даних до корисних даних повідомлень MQTT. Це може бути зроблено виключно всередині Qt або за допомогою зовнішніх рішень (наприклад, protobuf). Інтеграція зовнішніх рішень у Qt проста.
Варто відзначити, що всі варіанти серіалізації, які були проведені за допомогою бенчмарку, проводилися з малою кількістю даних у повідомленні, і якщо кількість даних буде більшою за розміром, результати можуть відрізнятися, і різні підходи можуть призвести до кращих результатів.