بالطبع ، أعرف عن تنسيقات مثل xml و json و bson و yaml و protobuf و Thrift و ASN.1. حتى أنني وجدت شجرة غريبة ، والتي هي نفسها قاتلة لـ JSON و XML و YAML وغيرها من أمثالهم .
فلماذا لم يتناسبوا جميعًا؟ لماذا اضطررت لكتابة مسلسل آخر؟
بعد نشر المقال في التعليقات، وقدموا عدة وصلات إلى CBOR ، UBJSON و MessagePack صيغ فاتني . ومن المحتمل أن يحلوا مشكلتي دون كتابة دراجة.
إنه لأمر مخز أنني لم أجد هذه المواصفات في وقت سابق ، لذلك سأضيف هذه الفقرة للقراء وللتذكير الخاص بعدم التسرع في كتابة الكود ؛-)
مراجعات التنسيقات على Habré : CBOR ، UBJSON
المتطلبات الأولية
تخيل أنك بحاجة إلى تعديل نظام موزع يتكون من عدة مئات من الأجهزة من أنواع مختلفة (أكثر من عشرة أنواع من الأجهزة تؤدي وظائف مختلفة). يتم دمجها في مجموعات تتبادل البيانات مع بعضها البعض عبر خطوط الاتصال التسلسلي باستخدام بروتوكول Modbus RTU.
أيضًا ، يتم توصيل بعض هذه الأجهزة بخط اتصال CAN مشترك ، والذي يوفر نقل البيانات داخل النظام بأكمله ككل. يصل معدل نقل البيانات على خط اتصالات Modbus إلى 115200 Baud ، وتقتصر السرعة على ناقل CAN على سرعة تصل إلى 50kBaud نظرًا لطولها ووجود تداخل صناعي خطير.
تم تطوير الغالبية العظمى من الأجهزة باستخدام وحدات تحكم دقيقة من سلسلة STM32F1x و STM32F2x. على الرغم من أن بعضها يعمل على STM32F4x أيضًا. وبالطبع ، الأنظمة المستندة إلى Windows / Linux مع معالجات x86 الدقيقة كوحدات تحكم عالية المستوى.
لتقدير كمية البيانات التي تتم معالجتها ونقلها بين الأجهزة أو تخزينها كإعدادات / معلمات تشغيل: في حالة واحدة - رقمان من 1 بايت و 6 أرقام من 4 بايت ، في الحالة الأخرى - 11 رقمًا من 1 بايت ورقم 1 من 4 بايت و إلخ كمرجع ، يصل حجم البيانات في إطار CAN القياسي إلى 8 بايت ، وفي إطار Modbus ، يصل إلى 252 بايت من الحمولة.
إذا لم تكن قد اخترقت عمق حفرة الأرانب بعد ، فأضف إلى بيانات الإدخال هذه: الحاجة إلى تتبع إصدارات البروتوكول وإصدارات البرامج الثابتة لأنواع مختلفة من الأجهزة ، بالإضافة إلى مطلب الحفاظ على التوافق ليس فقط مع تنسيقات البيانات الموجودة حاليًا ، ولكن أيضًا لضمان المشترك عمل الأجهزة مع الأجيال القادمة ، والتي أيضًا لا تقف مكتوفة الأيدي وتتطور باستمرار مع تطور الوظائف وتوجد العضادات في عمليات التنفيذ. بالإضافة إلى التفاعل مع الأنظمة الخارجية وتوسيع المتطلبات وما إلى ذلك.
في البداية ، بسبب الموارد المحدودة والسرعات المنخفضة لخطوط الاتصال ، تم استخدام تنسيق ثنائي لتبادل البيانات ، والذي كان مرتبطًا فقط بسجلات Modbus. لكن مثل هذا التطبيق لم يجتاز الاختبار الأول للتوافق وقابلية التوسع.
لذلك ، عند إعادة تصميم البنية ، كان من الضروري التخلي عن استخدام سجلات Modbus القياسية. ولا حتى بسبب استخدام خطوط اتصال أخرى بالإضافة إلى هذا البروتوكول ، ولكن بسبب التنظيم المحدود للغاية لهياكل البيانات القائمة على سجلات 16 بت.
في الواقع ، في المستقبل ، مع التطور الحتمي للنظام ، قد يكون من الضروري (وفي الواقع ، كان مطلوبًا بالفعل) ، نقل سلاسل النص أو المصفوفات. من الناحية النظرية ، يمكن أيضًا عرضها على خريطة سجل Modbus ، ولكن تبين أن هذا النفط ، لأن يأتي التجريد على التجريد.
بالطبع ، يمكنك نقل البيانات على هيئة blob ثنائي بالإشارة إلى إصدار البروتوكول ونوع الكتلة. وعلى الرغم من أن هذه الفكرة قد تبدو للوهلة الأولى سليمة ، لأنه من خلال إصلاح متطلبات معينة للهندسة المعمارية ، يمكنك تحديد تنسيقات البيانات مرة واحدة وإلى الأبد ، وبالتالي توفير التكاليف العامة التي ستكون حتمية عند استخدام تنسيقات مثل XML أو JSON.
لتسهيل مقارنة الخيارات ، أعددت الجدول التالي بنفسي:
:
:
:
:
:
:
:
- . , .
:
- , .
- . , .
- . , , . , .
- , .
:
:
- .
:
- . , .
- , , .
وتخيل فقط كيف تبدأ عدة مئات من الأجهزة في تبادل البيانات الثنائية مع بعضها البعض ، حتى مع ربط كل رسالة بإصدار البروتوكول و / أو نوع الجهاز ، ثم تصبح الحاجة إلى استخدام مُسلسل مع الحقول المسماة واضحة على الفور. بعد كل شيء ، حتى الاستيفاء البسيط لتعقيد دعم مثل هذا الحل ككل ، وإن كان ذلك بعد وقت قصير جدًا ، يجبرك على الاستيلاء على رأسك.
وهذا ، حتى بدون مراعاة رغبات العميل المتوقعة لزيادة الوظائف ، ووجود عضادات إلزامية في التنفيذ و "ثانوية" ، للوهلة الأولى ، تحسينات ، والتي ستجلب معهم بالتأكيد ذوقًا خاصًا للبحث عن عضادات متكررة في العمل المنسق جيدًا لحديقة الحيوانات ...
ما هي الخيارات؟
بعد هذا التفكير ، توصلت إلى استنتاج مفاده أنه مطلوب منذ البداية وضع تعريف عالمي للبيانات الثنائية ، بما في ذلك عند تبادل الحزم عبر خطوط اتصال منخفضة السرعة.
وعندما توصلت إلى استنتاج مفاده أنه لا يمكن الاستغناء عن المسلسل ، نظرت أولاً إلى الحلول الحالية التي أثبتت نفسها بالفعل من الجانب الأفضل ، والتي يتم استخدامها بالفعل في العديد من المشاريع.
يجب إسقاط التنسيقات الأساسية xml و json و yaml ومتغيرات النص الأخرى مع بناء جملة رسمي بسيط ومريح للغاية ، وهو مناسب تمامًا لمعالجة المستندات وفي نفس الوقت مناسب للقراءة والتحرير من قبل البشر. وفقط بسبب بساطتها وبساطتها ، فإن لديهم نفقات كبيرة جدًا عند تخزين البيانات الثنائية ، والتي تحتاج فقط إلى المعالجة.
لذلك ، نظرًا لمحدودية الموارد وخطوط الاتصال منخفضة السرعة ، فقد تقرر استخدام تنسيق عرض بيانات ثنائي. ولكن حتى في حالة التنسيقات التي يمكنها تحويل البيانات إلى تمثيل ثنائي ، مثل Protocol Buffers أو FlatBuffers أو ASN.1 أو Apache Thrift ، فإن النفقات العامة لتسلسل البيانات ، فضلاً عن الراحة العامة لاستخدامها ، لم تساهم في التنفيذ الفوري لأي من هذه المكتبات.
كان تنسيق BSON ، الذي يحتوي على حد أدنى من النفقات العامة ، هو الأنسب لمجموعة المعلمات. وفكرت بجدية في استخدامه. ولكن نتيجة لذلك ، قرر مع ذلك التخلي عنها ، نظرًا لأن جميع الأشياء الأخرى متساوية ، حتى BSON سيكون لها تكاليف عامة غير مقبولة.
قد يبدو غريباً للبعض أن تقلق بشأن عشرات البايتات الإضافية ، ولكن لسوء الحظ ، يجب إرسال عشرات البايتات في كل مرة يتم فيها إرسال رسالة. وفي حالة العمل على خطوط اتصال منخفضة السرعة ، من المهم حتى وجود عشرة بايتات إضافية في كل رسالة .
بمعنى آخر ، عندما تعمل بعشرة بايت ، تبدأ في حساب كل منها. ولكن إلى جانب البيانات ، يتم أيضًا إرسال عناوين الأجهزة والمجموعات الاختبارية للحزم والمعلومات الأخرى الخاصة بكل خط اتصال وبروتوكول إلى الشبكة.
ماذا حدث
نتيجة المداولات والتجارب العديدة ، تم الحصول على مُسلسل بالميزات والخصائص التالية:
- الحمل الزائد للبيانات ذات الحجم الثابت هو 1 بايت (بدون حساب طول اسم حقل البيانات).
- , , — 2 ( ). , CAN Modbus, .
- — 16 .
- , , .. . , 16 .
- (, ) — 252 (.. ).
- — .
- . .
- « », , . , , - ( 0xFF).
- . , . .
- , . .
- 8 64 .
- .
- ( ).
- — . , , . ;-)
- . , .
أود أن أشير بشكل منفصل
يتم التنفيذ في C ++ x11 في ملف رأس واحد باستخدام آلية قوالب SFINAE (فشل الاستبدال ليس خطأ).
مدعوم بالقراءة الصحيحة للبيانات في المخزن المؤقت (متغير) ب حوالي 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);
مثال على التنفيذ باستخدام التعداد كمعرف بيانات
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 ، لذا استخدمه للصحة.
سأكون سعيدًا بأي ملاحظات ، بما في ذلك التعليقات و / أو الاقتراحات.
تحديث: لم أكن مخطئا في اختيار صورة للمقال ؛-)