Tentu saja, saya tahu tentang format seperti xml, json, bson, yaml, protobuf, Thrift, ASN.1. Saya bahkan menemukan Pohon eksotis, yang dengan sendirinya merupakan pembunuh JSON, XML, YAML, dan lainnya seperti mereka .
Jadi mengapa semuanya tidak cocok? Mengapa saya dipaksa untuk menulis serializer lain?
Setelah publikasi artikel di komentar, mereka memberikan beberapa tautan ke format CBOR , UBJSON dan MessagePack yang saya lewatkan . Dan mereka cenderung menyelesaikan masalah saya tanpa menulis sepeda.
Sayang sekali saya tidak dapat menemukan spesifikasi ini sebelumnya, jadi saya akan menambahkan paragraf ini untuk para pembaca dan untuk pengingat saya sendiri agar tidak terburu-buru menulis kode ;-).
Review format di Habrรฉ: CBOR , UBJSON
Persyaratan awal
Bayangkan Anda perlu memodifikasi sistem terdistribusi yang terdiri dari beberapa ratus perangkat dengan jenis berbeda (lebih dari sepuluh jenis perangkat yang menjalankan fungsi berbeda). Mereka digabungkan menjadi grup yang bertukar data satu sama lain melalui jalur komunikasi serial menggunakan protokol Modbus RTU.
Selain itu, beberapa perangkat ini tersambung ke jalur komunikasi CAN umum, yang menyediakan transfer data dalam seluruh sistem secara keseluruhan. Kecepatan transfer data pada jalur komunikasi Modbus hingga 115200 Baud, dan kecepatan pada bus CAN dibatasi hingga 50kBaud karena panjangnya dan adanya gangguan industri yang serius.
Sebagian besar perangkat dikembangkan pada mikrokontroler seri STM32F1x dan STM32F2x. Meskipun beberapa dari mereka bekerja di STM32F4x juga. Dan tentu saja, sistem berbasis Windows / Linux dengan mikroprosesor x86 sebagai pengontrol tingkat atas.
Untuk memperkirakan jumlah data yang diproses dan dikirim antar perangkat atau disimpan sebagai pengaturan / parameter operasi: Dalam satu kasus - 2 angka 1 byte dan 6 angka 4 byte, di kasus lain - 11 angka 1 byte dan 1 angka 4 byte dan dll. Untuk referensi, ukuran data dalam bingkai CAN standar hingga 8 byte, dan dalam bingkai Modbus, hingga 252 byte payload.
Jika Anda belum menembus kedalaman lubang kelinci, tambahkan ke data input ini: kebutuhan untuk melacak versi protokol dan versi firmware untuk berbagai jenis perangkat, serta persyaratan untuk menjaga kompatibilitas tidak hanya dengan format data yang ada saat ini, tetapi juga untuk memastikan sambungan pekerjaan perangkat dengan generasi mendatang, yang juga tidak berhenti dan terus berkembang dan dikerjakan ulang seiring berkembangnya fungsionalitas dan tiang tembok ditemukan dalam implementasi. Plus, interaksi dengan sistem eksternal, perluasan persyaratan, dll.
Awalnya, karena sumber daya yang terbatas dan kecepatan jalur komunikasi yang rendah, format biner digunakan untuk pertukaran data, yang hanya terikat pada register Modbus. Tetapi implementasi seperti itu tidak lulus tes pertama untuk kompatibilitas dan ekstensibilitas.
Oleh karena itu, ketika mendesain ulang arsitektur, penggunaan register Modbus standar harus ditinggalkan. Dan bahkan bukan karena jalur komunikasi lain digunakan selain protokol ini, melainkan karena organisasi struktur data yang terlalu terbatas berdasarkan register 16-bit.
Memang, di masa depan, dengan evolusi sistem yang tak terelakkan, mungkin diperlukan, (dan sebenarnya, itu sudah diperlukan), untuk mentransfer string teks atau array. Secara teori, mereka juga bisa ditampilkan di peta register Modbus, tapi ini ternyata oli, karena datang abstraksi atas abstraksi.
Tentu saja, Anda dapat mentransfer data sebagai blob biner dengan mengacu pada versi protokol dan tipe blok. Dan meskipun sekilas, ide ini mungkin tampak masuk akal, karena dengan memperbaiki persyaratan tertentu untuk arsitektur, Anda dapat menentukan format data untuk selamanya, sehingga secara signifikan menghemat biaya overhead yang tidak dapat dihindari saat menggunakan format seperti XML atau JSON.
Untuk mempermudah membandingkan opsi, saya membuat tabel berikut untuk diri saya sendiri:
:
:
:
:
:
:
:
- . , .
:
- , .
- . , .
- . , , . , .
- , .
:
:
- .
:
- . , .
- , , .
Dan bayangkan bagaimana beberapa ratus perangkat mulai bertukar data biner satu sama lain, bahkan dengan mengikat setiap pesan ke versi protokol dan / atau jenis perangkat, maka kebutuhan untuk menggunakan serializer dengan bidang bernama segera menjadi jelas. Lagipula, bahkan interpolasi sederhana dari kerumitan dalam mendukung solusi semacam itu secara keseluruhan, meskipun setelah waktu yang sangat singkat, memaksa Anda untuk memegang kepala.
Dan ini, bahkan tanpa memperhitungkan keinginan pelanggan yang diharapkan untuk meningkatkan fungsionalitas, kehadiran tiang wajib dalam pelaksanaan dan "kecil", pada pandangan pertama, perbaikan, yang tentunya akan membawa kesedian khusus dari pencarian tiang berulang dalam pekerjaan yang terkoordinasi dengan baik dari kebun binatang seperti itu ...
Apa saja opsinya?
Setelah alasan seperti itu, Anda tanpa sadar sampai pada kesimpulan bahwa dari awal diperlukan untuk meletakkan identifikasi universal dari data biner, termasuk saat bertukar paket melalui jalur komunikasi berkecepatan rendah.
Dan ketika saya sampai pada kesimpulan bahwa seseorang tidak dapat melakukannya tanpa serializer, saya pertama kali melihat solusi yang ada yang telah membuktikan diri dari sisi terbaik, dan yang sudah digunakan di banyak proyek.
Format dasar xml, json, yaml dan varian teks lainnya dengan sintaks formal yang sangat nyaman dan sederhana, yang cocok untuk memproses dokumen dan pada saat yang sama nyaman untuk dibaca dan diedit oleh manusia, harus segera dihapus. Dan hanya karena kenyamanan dan kesederhanaannya, mereka memiliki overhead yang sangat besar saat menyimpan data biner, yang hanya perlu diproses.
Oleh karena itu, mengingat sumber daya yang terbatas dan jalur komunikasi berkecepatan rendah, diputuskan untuk menggunakan format presentasi data biner. Tetapi bahkan dalam kasus format yang dapat mengubah data menjadi representasi biner, seperti Protocol Buffer, FlatBuffers, ASN.1 atau Apache Thrift, overhead serialisasi data, serta kemudahan penggunaan secara umum, tidak berkontribusi pada implementasi langsung salah satu pustaka ini.
Format BSON, yang memiliki overhead minimal, paling sesuai untuk kumpulan parameter. Dan saya benar-benar mempertimbangkan untuk menggunakannya. Tetapi sebagai hasilnya, saya tetap memutuskan untuk meninggalkannya, karena semua hal lain dianggap sama, bahkan BSON akan memiliki biaya overhead yang tidak dapat diterima.
Mungkin tampak aneh bagi beberapa orang bahwa Anda harus mengkhawatirkan selusin byte tambahan, tetapi sayangnya, lusinan byte ini harus dikirim setiap kali pesan dikirim. Dan dalam kasus bekerja pada jalur komunikasi kecepatan rendah, bahkan tambahan sepuluh byte di setiap paket adalah penting.
Dengan kata lain, saat Anda beroperasi dengan sepuluh byte, Anda mulai menghitung masing-masing. Tetapi bersama dengan data, alamat perangkat, checksum paket dan informasi lain yang spesifik untuk setiap jalur komunikasi dan protokol juga dikirim ke jaringan.
Apa yang terjadi
Sebagai hasil dari musyawarah dan beberapa percobaan, serializer diperoleh dengan fitur dan karakteristik berikut:
- Overhead untuk data ukuran tetap adalah 1 byte (tidak termasuk panjang nama field data).
- , , โ 2 ( ). , CAN Modbus, .
- โ 16 .
- , , .. . , 16 .
- (, ) โ 252 (.. ).
- โ .
- . .
- ยซ ยป, , . , , - ( 0xFF).
- . , . .
- , . .
- 8 64 .
- .
- ( ).
- โ . , , . ;-)
- . , .
Saya ingin mencatat secara terpisah
Implementasinya dilakukan di C ++ x11 dalam satu file header menggunakan mekanisme template SFINAE (Kegagalan substitusi bukan kesalahan).
Didukung dengan pembacaan data yang benar dalam buffer (variabel) b. Tentang ukuran ng lebih besar dari tipe data yang disimpan. Misalnya, bilangan bulat 8 bit dapat dibaca menjadi variabel dari 8 hingga 64 bit. Saya berpikir mungkin ada baiknya menambahkan pengepakan bilangan bulat yang lebih besar dari 8 bit, sehingga dapat ditransmisikan dalam jumlah yang lebih kecil.
Array berseri dapat dibaca baik dengan menyalin ke area memori yang ditentukan, atau dengan mendapatkan referensi normal ke data dalam buffer asli, jika Anda ingin menghindari penyalinan, dalam kasus di mana itu tidak diperlukan. Tetapi fitur ini harus digunakan dengan hati-hati, karena array bilangan bulat disimpan dalam urutan byte jaringan, yang dapat berbeda antar mesin.
Serialisasi struktur atau objek yang lebih kompleks bahkan tidak direncanakan. Biasanya berbahaya untuk mentransfer struktur dalam bentuk biner karena kemungkinan kesejajaran bidangnya. Tetapi jika masalah ini diselesaikan dengan cara yang relatif sederhana, maka masih akan ada masalah untuk mengubah semua bidang objek yang berisi bilangan bulat ke urutan byte jaringan dan sebaliknya.
Selain itu, dalam keadaan darurat, struktur selalu dapat disimpan dan dipulihkan sebagai array byte. Secara alami, dalam hal ini, konversi bilangan bulat perlu dilakukan secara manual.
Penerapan
Implementasinya ada di sini: https://github.com/rsashka/microprop
Cara menggunakannya ditulis dalam contoh dengan berbagai tingkat detail:
Penggunaan cepat
#include "microprop.h"
Microprop prop(buffer, sizeof (buffer));//
prop.FieldExist(string || integer); // ID
prop.FieldType(string || integer); //
prop.Append(string || integer, value); //
prop.Read(string || integer, value); //
Penggunaan yang lambat dan bijaksana
#include "microprop.h"
Microprop prop(buffer, sizeof (buffer)); //
prop.AssignBuffer(buffer, sizeof (buffer)); //
prop.AssignBuffer((const)buffer, sizeof (buffer)); // read only
prop.AssignBuffer(buffer, sizeof (buffer), true); // read only
prop.FieldNext(ptr); //
prop.FieldName(string || integer, size_t *length = nullptr); // ID
prop.FieldDataSize(string || integer); //
//
prop.Append(string || blob || integer, value || array);
prop.Read(string || blob || integer, value || array);
prop.Append(string || blob || integer, uint8_t *, size_t);
prop.Read(string || blob || integer, uint8_t *, size_t);
prop.AppendAsString(string || blob || integer, string);
const char * ReadAsString(string || blob || integer);
Contoh implementasi menggunakan enum sebagai pengenal data
class Property : public Microprop {
public:
enum ID {
ID1, ID2, ID3
};
template <typename ... Types>
inline const uint8_t * FieldExist(ID id, Types ... arg) {
return Microprop::FieldExist((uint8_t) id, arg...);
}
template <typename ... Types>
inline size_t Append(ID id, Types ... arg) {
return Microprop::Append((uint8_t) id, arg...);
}
template <typename T>
inline size_t Read(ID id, T & val) {
return Microprop::Read((uint8_t) id, val);
}
inline size_t Read(ID id, uint8_t *data, size_t size) {
return Microprop::Read((uint8_t) id, data, size);
}
template <typename ... Types>
inline size_t AppendAsString(ID id, Types ... arg) {
return Microprop::AppendAsString((uint8_t) id, arg...);
}
template <typename ... Types>
inline const char * ReadAsString(ID id, Types... arg) {
return Microprop::ReadAsString((uint8_t) id, arg...);
}
};
Kode diterbitkan di bawah lisensi MIT, jadi gunakanlah untuk kesehatan.
Saya akan dengan senang hati menerima umpan balik, termasuk komentar dan / atau saran.
Pembaruan: Saya tidak salah memilih gambar untuk artikel ;-)