もちろん、xml、json、bson、yaml、protobuf、Thrift、ASN.1などの形式については知っています。それ自体がJSON、XML、YAMLなどのキラーであるエキゾチックなツリーも見つけました。
では、なぜそれらすべてが適合しなかったのでしょうか?なぜ私は別のシリアライザーを書くことを余儀なくされたのですか?
コメントで記事を公開した後、彼らは私が見逃したCBOR、UBJSON、MessagePack形式へのいくつかのリンクを提供しました。そして、彼らはおそらく自転車を書かずに私の問題を解決します。
以前にこれらの仕様を見つけることができなかったのは残念なので、読者のために、そしてコードを急いで書かないように私自身のリマインダーのためにこの段落を追加します;-)。
Habréのフォーマットのレビュー:CBOR、UBJSON
初期要件
異なるタイプの数百のデバイス(異なる機能を実行する10を超えるタイプのデバイス)で構成される分散システムを変更する必要があると想像してください。これらは、ModbusRTUプロトコルを使用してシリアル通信回線を介して相互にデータを交換するグループに結合されます。
また、これらのデバイスの一部は、システム全体でデータ送信を提供する共通のCAN通信回線に接続されています。 Modbus通信回線のデータ転送速度は最大115200ボーであり、CANバスの速度は、その長さと深刻な産業干渉の存在により、最大50kボーの速度に制限されています。
圧倒的多数のデバイスは、STM32F1xおよびSTM32F2xシリーズのマイクロコンピューターで開発されています。それらのいくつかはSTM32F4xでも動作しますが。そしてもちろん、トップレベルのコントローラーとしてx86マイクロプロセッサーを搭載したWindows / Linuxベースのシステム。
デバイス間で処理および送信されるデータ、または設定/操作パラメーターとして保存されるデータの量を見積もるには、次のようにします。等参考までに、標準のCANフレームのデータサイズは最大8バイトで、Modbusフレームのデータサイズは最大252バイトのペイロードです。
ウサギの穴の深さをまだ浸透していない場合は、この入力データに追加します。さまざまなタイプのデバイスのプロトコルバージョンとファームウェアバージョンを追跡する必要性、および現在存在するデータ形式との互換性を維持するための要件だけでなく、結合を確保するための要件機能が開発され、実装にわき柱が見つかると、静止せず、絶えず進化し、作り直されている、将来の世代のデバイスの作業。さらに、外部システムとの相互作用、要件の拡大など。
当初、リソースが限られていて通信回線の速度が遅いため、データ交換にはバイナリ形式が使用されていましたが、これはModbusレジスタにのみ関連付けられていました。しかし、そのような実装は、最初の互換性と拡張性のテストに合格しませんでした。
したがって、アーキテクチャを再設計するときは、標準のModbusレジスタの使用を中止する必要がありました。また、このプロトコルに加えて他の通信回線が使用されているためではなく、16ビットレジスタに基づくデータ構造の編成が過度に制限されているためです。
実際、将来的には、システムの必然的な進化に伴い、テキスト文字列または配列を転送することが必要になる可能性があります(実際にはすでに必要でした)。理論的には、Modbusレジスタマップに表示することもできますが、これはオイルであることが判明しました。抽象化よりも抽象化があります。
もちろん、プロトコルバージョンとブロックタイプを参照して、データをバイナリblobとして転送できます。また、一見、このアイデアは正しいように思えるかもしれません。アーキテクチャの特定の要件を修正することで、データ形式を一度に定義できるため、XMLやJSONなどの形式を使用する場合に避けられないオーバーヘッドコストを大幅に節約できます。
オプションの比較を簡単にするために、次の表を自分で作成しました。
:
:
:
:
:
:
:
- . , .
:
- , .
- . , .
- . , , . , .
- , .
:
:
- .
:
- . , .
- , , .
そして、各メッセージがプロトコルバージョンやデバイスタイプにバインドされていても、数百のデバイスが互いにバイナリデータを交換し始める方法を想像してみてください。そうすれば、名前付きフィールドを持つシリアライザーを使用する必要性がすぐに明らかになります。結局のところ、そのようなソリューション全体をサポートすることの複雑さの単純な補間でさえ、非常に短い時間の後ではありますが、頭をつかむことを余儀なくされます。
そして、これは、機能を向上させたいという顧客の期待される要望を考慮しなくても、実装における必須のわき柱の存在と、一見したところ「マイナーな」改善であり、そのような動物園のよく調整された作業で繰り返し発生するわき柱の検索の特別な気まぐれを確実にもたらします...
オプションは何ですか?
そのような推論の後、あなたは、低速通信回線を介してパケットを交換する場合を含め、最初からバイナリデータの普遍的な識別を行う必要があるという結論に達します。
そして、シリアライザーなしでは実現できないという結論に達したとき、私は最初に、すでに最良の側面から証明され、多くのプロジェクトですでに使用されている既存のソリューションを調べました。
非常に便利でシンプルな形式構文を備えた基本形式のxml、json、yaml、およびその他のテキストバリアントは、ドキュメントの処理に適していると同時に、人間による読み取りと編集にも便利ですが、すぐに削除する必要がありました。また、利便性とシンプルさのために、処理が必要なバイナリデータを格納する際のオーバーヘッドが非常に大きくなります。
そのため、限られたリソースと低速の通信回線を考慮して、バイナリデータ表示形式を使用することにしました。ただし、Protocol Buffers、FlatBuffers、ASN.1、Apache Thriftなど、データをバイナリ表現に変換できる形式の場合でも、データのシリアル化のオーバーヘッドと一般的な使いやすさは、これらのライブラリの即時実装には寄与しませんでした。
オーバーヘッドが最小限のBSON形式が、一連のパラメーターに最適でした。そして、私はそれを使うことを真剣に考えました。しかし、結果として、私はそれを放棄することにしました。他のすべての条件が同じであるため、BSONでさえ許容できないオーバーヘッドコストが発生するからです。
余分な数十バイトを心配しなければならないのは奇妙に思えるかもしれませんが、残念ながら、メッセージが送信されるたびにこの数十バイトを送信する必要があります。また、低速の通信回線で作業する場合は、各メッセージに10バイトを追加することも重要です。
つまり、10バイトで操作すると、それぞれをカウントし始めます。ただし、データとともに、デバイスアドレス、パケットチェックサム、および各通信回線とプロトコルに固有のその他の情報もネットワークに送信されます。
どうした
検討といくつかの実験の結果、次の機能と特性を備えたシリアライザーが得られました。
- 固定サイズのデータのオーバーヘッドは1バイトです(データフィールド名の長さはカウントされません)。
- , , — 2 ( ). , CAN Modbus, .
- — 16 .
- , , .. . , 16 .
- (, ) — 252 (.. ).
- — .
- . .
- « », , . , , - ( 0xFF).
- . , . .
- , . .
- 8 64 .
- .
- ( ).
- — . , , . ;-)
- . , .
別途注意したい
実装は、SFINAE(置換の失敗はエラーではありません)テンプレートメカニズムを使用して、単一のヘッダーファイルでC ++ x11で実行されます。
バッファ(変数)内のデータの正しい読み取りによってサポートされます。b格納されているデータタイプよりも約ng大きいサイズ。たとえば、8ビットの整数を8〜64ビットの変数に読み込むことができます。サイズが8ビットを超える整数のパッキングを追加して、より少ない数で送信できるようにする価値があるのではないかと考えています。
シリアル化された配列は、指定されたメモリ領域にコピーするか、コピーを回避したい場合は元のバッファ内のデータへの通常の参照を取得して、不要な場合に読み取ることができます。ただし、この機能は注意して使用する必要があります。整数の配列はネットワークバイトオーダーで保存され、マシン間で異なる場合があります。
構造やより複雑なオブジェクトのシリアル化も計画されていませんでした。フィールドが整列する可能性があるため、構造をバイナリ形式で転送することは一般に危険です。しかし、それでもこの問題が比較的簡単な方法で解決された場合でも、整数を含むオブジェクトのすべてのフィールドをネットワークバイトオーダーに変換したり、その逆に変換したりするという問題が発生します。
さらに、緊急の場合、構造は常にバイトの配列として保存および復元できます。当然、この場合、整数の変換は手動で行う必要があります。
実装
実装は次のとおりです。https://github.com/rsashka/microprop
使用方法は、さまざまな詳細度の例で記述されています。
迅速な使用
#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); //
ゆっくりと思慮深い使用
#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);
enumをデータ識別子として使用する実装例
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...);
}
};
コードはMITライセンスの下で公開されているため、健康のために使用してください。
コメントや提案など、フィードバックをいただければ幸いです。
更新:私は記事の写真を選ぶのに間違いはありませんでした;-)