FlowingDev

شرح Protobuf: الصيغة الثنائية (binary) فائقة الكفاءة اللي بتلعب معاك على التقيل

افهم Protocol Buffers، صيغة جوجل الثنائية (binary serialization) السريعة جداً اللي بتضغط بياناتك في حزم صغيرة وآمنة من حيث النوع (type-safe).

جرّب الأداة: فك تشفير Protobuf

في جملة واحدة

الـ Protocol Buffers (أو Protobuf) هي صيغة ثنائية (binary format) سريعة بشكل مرعب ومستقلة عن لغات البرمجة، بتُستخدم لعمل serialize للبيانات المُهيكلة—تقدر تعتبرها ابن عم الـ JSON المعضل (gym-bro) اللي مبيتهزرش معاه وبيتواصل بس عن طريق streams ثنائية (binary) مضغوطة ومُحكمة جداً.

المشكلة اللي بيحلها

في أوائل الألفينات، جوجل كان عندها مشكلة ضخمة: كان عندهم عدد مهول من الـ internal services اللي بتكلم بعض، وحجم الداتا بينهم بقى تقيل جداً. وقتها، الـ standard في الصناعة لتواصل الأجهزة ببعضها كان XML. الـ XML ممتاز لو انت غاوي قراية أقواس زاويّة (angle brackets)، بس معروف عنه إنه مليان حشو وبطيء جداً في الـ parsing. كل مرة سيرفر يحب يبعت داتا بسيطة، زي الـ user ID، كان لازم يبعت الـ tags النصية حواليها <userId>12345</userId>، وبعدين السيرفر اللي بيستقبل يضطر يعمل parse للـ string ده، ويطابق الـ tags، ويرجع يحول "12345" لـ integer.

ولما ظهر الـ JSON في الصورة، كان أخف، بس كان لسه بيعاني من نفس المشكلة الأساسية: إنه text format بيبعت strings خام. لو بعت مليون record فيهم الحقل "emailAddress"، فانت كده بتبعت الـ string "emailAddress" مليون مرة على النتورك.

مهندسين جوجل أدركوا إن ده إهدار ضخم للـ bandwidth وموارد الـ CPU. كانوا محتاجين format يكون:

  1. صغير جداً على النتورك (Tiny on the wire): متبعتش أسماء الـ fields، ابعت الداتا بس.
  2. سريع في الـ parsing: الكمبيوتر بيفضل يقرا bits و bytes، مش يقعد يعمل parse لحروف نصية.
  3. مُقيد بنوع البيانات (Strictly typed): لو بعتلك integer، لازم تستلمه integer، من غير ما نعتمد على أمل إن الـ string يحصل له parse صح.
  4. متوافق مع الإصدارات القديمة (Backwards compatible): لازم أقدر أعمل update لـ service لـ format جديد من غير ما أكسر الـ clients القديمة اللي لسه معملهاش تحديث.

الحل بتاعهم كان Protocol Buffers. بدل ما يبعتوا text بيشرح نفسه، الـ Protobuf بيعتمد على "عقد" (schema) متفق عليه مسبقاً. ولأن الراسل والمستقبل عارفين الـ schema دي أصلاً، الداتا الفعلية اللي بتتبعت على النتورك بتتنضف من كل الـ metadata، ومبيتبقاش منها غير القيم الثنائية (binary) الخام والمضغوطة جداً.

إزاي بيشتغل تحت الكبوت

عشان تفهم الـ Protobuf، لازم تقسمه لـ 3 مراحل: العقد (contract)، الكومبايلر (compiler)، وصيغة النقل (wire format).

الـ Schema (ملف .proto)

كل حاجة بتبدأ بملف .proto. ده عبارة عن عقد ملوش دعوة بلغة البرمجة (language-agnostic) بتحدد فيه شكل الداتا بتاعتك بالظبط.

syntax = "proto3";

message Person {
  string name = 1;
  int32 id = 2;
  bool is_active = 3;
}

واخد بالك من الأرقام دي (= 1, = 2)؟ دي اسمها الـ Field Tags. دي بقى الخلطة السرية بتاعة الـ Protobuf. في الـ JSON، الـ key "name" هو اللي بيحدد الـ field. أما في الـ Protobuf، الرقم 1 هو اللي بيحدد الـ field.

مرحلة الكومبايلر

انت مش بتكتب كود الـ parsing بتاع الـ Protobuf بإيدك. انت بتشغل أداة كومبايلر (protoc) على ملف الـ .proto بتاعك، وبتقوله إيه لغة البرمجة اللي عايز تطلع بيها الكود. وهو بيطلعلك native classes للـ C# أو TypeScript أو Python أو Go أو أياً كان اللي بتستخدمه.

لما الأبلكيشن بتاعك يعوز يبعت Person، انت مجرد بتعمل instance من الـ class اللي اتعمله generate، وتملاه بالداتا، وتنادي على ميثود Serialize(). الكود اللي طالع ده هو اللي بيتولى عملية التعبئة الثنائية المعقدة (binary packing).

صيغة النقل (السحر الثنائي)

لما بتعمل serialize لـ object الـ Person ده، إيه اللي بيتبعت فعلياً على النتورك؟

بيستخدم طريقة [Key] [Value]، بس على مستوى الـ bytes. الـ Key هنا مش string؛ ده عبارة عن byte واحد (أو كام byte) بيدمج الـ Field Tag بتاعك مع الـ Wire Type بتاع الداتا.

الـ Wire Type ده بيقول للمستقبل القيمة اللي جاية دي طولها قد إيه. ودي أهم الـ wire types الأساسية:

Wire Type المعنى (Meaning) بيُستخدم لـ (Used For)
0 Varint int32, int64, uint32, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages
5 32-bit fixed32, sfixed32, float

على سبيل المثال، لو خليت is_active = true (يعني Field 3، والـ Wire Type 0)، الـ Protobuf بيعمل encode للـ key (دمج بين 3 و 0) والـ value (1 عشان true) في اتنين byte بس. في الـ JSON، كتابة "is_active": true بتاخد 17 byte.

خدعة الـ Varint

الـ Protobuf بيستخدم حاجة اسمها "Base-128 Varints" عشان يضغط الأرقام. الـ 32-bit integer العادي دايماً بياخد 4 bytes في الميموري، حتى لو كان الرقم مجرد 5. الـ Protobuf بيقول: "ليه نضيع 3 bytes في أصفار ملهاش لازمة؟"

مع الـ Varints، كل byte بيحجز 1 bit (تحديداً الـ Most Significant Bit أو الـ MSB) عشان يشتغل كـ flag معناه: "هل لسه فيه bytes تانية جاية؟". لو الرقم صغير، الـ MSB بيكون 0، والرقم كله بيكفي في byte واحد بالظبط. ده معناه إن الأرقام الصغيرة بتاخد مساحة أقل بكتير جداً على النتورك (the wire).

الـ Decode من غير schema

عشان الـ wire format جواه الـ Wire Type أصلاً، أي parser يقدر يمشي جوه الـ raw Protobuf byte stream ويفهم شكل الداتا حتى من غير ملف الـ .proto. هو مش هيعرف إن Field 1 اسمه "name"، بس هيشوف: "Field 1 عبارة عن string طوله محدد بـ 4 bytes وجواه 'John'". عشان كده تقدر ترمي داتا Protobuf خام (سواء hex أو Base64) في أي decoder وهيطلعلك شجرة شبه الـ JSON مليانة tags وقيم (values) تقدر تستفيد بيها.

قصص من أرض الواقع

أسطول الـ IoT اللي نَفَسه اتقطع

شركة ستارت أب عملت أسطول من الحساسات الزراعية الذكية (smart sensors) عشان تقيس رطوبة التربة عبر مئات الأميال من الأراضي الزراعية. في الأول، الحساسات دي كانت بتبعت ping للسيرفر المركزي كل دقيقة باستخدام JSON payloads عن طريق شبكات الموبايل. خلال شهر واحد، حصلت حاجتين: فواتير باقات الـ 3G ضربت في السما، وبطاريات الحساسات ماتت في أسابيع بدل شهور.

راحوا كتبوا الـ firmware بتاع الحساسات من تاني عشان يعمل serialize للداتا لـ Protocol Buffers. ولأنهم كانوا بيبعتوا أرقام صغيرة و booleans، التخلص من كل الـ JSON strings اللي زي {"sensorId": 14, "moistureLevel": 42} قلل حجم الـ payload بتاعهم بأكثر من 80%. الدرس المستفاد: لما يكون الـ bandwidth غالي وعمر البطارية محدود، إرسال raw strings بيبقى رفاهية متقدرش تدفع تمنها. الـ Binary serialization بيوفر الموارد الفعلية للـ hardware.

الزحمة المرورية العظيمة للـ Microservices في 2018

شركة e-commerce عملاقة قررت تكسر الـ monolithic backend بتاعها لـ 50 microservice منفصلين. واختاروا REST APIs مع JSON للتواصل. فجأة، الـ latency الكلي ضرب. لما عملوا profiling للسيرفرات اكتشفوا صدمة: الـ backend services كانت بتستهلك 30% من قوة الـ CPU في مجرد تحويل الداتا لـ strings وعمل parse للـ JSON وهي بتتنطط من الـ Auth service للـ Cart service لـ Inventory service.

قاموا محولين اتصالاتهم الداخلية لـ gRPC، وده framework مبني بالكامل على الـ Protobuf. ولأن الـ parsing بتاع الـ Protobuf هو فعلياً مجرد قراية لـ memory offsets وتشفيت للـ bits، استهلاك الـ CPU نزل الأرض. والداتا بقت بتطير بين الـ services في لمح البصر. الدرس المستفاد: في الـ backend systems الداخلية اللي عليها ضغط عالي (high-throughput)، الـ text parsing يعتبر ضريبة خفية. الـ Binary serialization هو الثغرة القانونية للهروب من الضريبة دي.

الـ Payload الغامض في الـ Network Dump

باحثة أمنية كانت بتحاول تعمل reverse-engineering لـ API غير موثق لتطبيق موبايل عشان تشوف الداتا اللي بيجمعها. وجهت الترافيك بتاع التطبيق عن طريق proxy، وهي متوقعة تشوف JSON لطيف ومقري. بدل ده، طلع لها هراء مش مفهوم. الـ HTTP headers أكدت شكوكها: Content-Type: application/x-protobuf.

مكنش معاها الـ .proto schema (مطورين التطبيق كانوا مخبينه على سيرفراتهم). لكنها خدت الـ Base64 payload ورمته في raw Protobuf decoder. وطلعلها شجرة مُهيكلة: Field 1: "johndoe", Field 2: "user@example.com", Field 3: 40.7128, Field 4: -74.0060. فوراً اتعرفت على الحقلين 3 و 4 على إنهم إحداثيات GPS. الدرس المستفاد: الـ Protobuf مش مُشفر (encrypted)، هو بس معموله encode. حتى من غير الخريطة (الـ schema)، الفتات اللي بتسيبها الـ Wire Types بتسمحلك تعمل reverse-engineer لهيكل الداتا.

أخطاء شائعة ومطبات

  • إعادة استخدام الـ field tags: الرقم هو المصدر المطلق للحقيقة (source of truth). لو مسحت field (مثلاً string name = 1;)، إياك أبداً ترجع تستخدم الرقم 1 لـ field جديد زي int32 age = 1;. لو client قديم بعت string، وسيرفر جديد حاول يقراه على إنه integer، الدنيا كلها هتضرب في وشك.
  • نسيان عمل reserve للـ fields الممسوحة: عشان تتجنب الغلطة اللي فوق، الـ Protobuf بيقدملك الـ keyword اللي اسمها reserved. لو مسحت field رقم 2، لازم تكتب صراحة reserved 2; في الـ schema بتاعتك. ده هيخلي الكومبايلر يزعق لأي ديفيلوبر يحاول يستخدمه تاني في المستقبل.
  • تغيير الـ data types بتهور: ممكن بأمان تغير بعض الأنواع (زي من int32 لـ int64 لأنهم بيستخدموا نفس الـ Varint wire type)، بس لو غيرت string لـ int32 فده يعتبر breaking change. الـ wire types هنا غير متوافقة مع بعضها أساساً.
  • افتراض إن Protobuf آمن (secure): ده serialization format، مش تشفير (encryption). لو بعت بيانات مستخدم حساسة عبر HTTP باستخدام Protobuf من غير TLS/SSL، أي حد يقدر يعترض الترافيك ويعمل decode للقيم الخام بمنتهى السهولة.
  • استخدامه عمياني في المتصفح: على الرغم من إن فيه تكنولوجيات زي gRPC-web، المتصفحات لغتها الأم هي الـ JSON. إنك تجيب Protobuf لـ frontend web app بيطلب إنك تبعت parsing library تقيلة في الـ JavaScript bundle بتاعتك، وده أحياناً بيضيع ميزة توفير الـ bandwidth اللي بتاخدها من الـ binary format نفسه.

ليه لازم تحطه تحت الرادار بتاعك

مش المفروض تستخدم الـ Protobuf في الـ APIs العامة بتاعتك (public-facing APIs) — الـ JSON لسه هو الملك المتوج هنا لأنه مقري للبشر، مرن، ومفهوم natively لأي ويب براوزر على كوكب الأرض.

لكن، المفروض تلجأ للـ Protobuf فوراً لما تكون متحكم في الطرفين بتوع الماسورة. لو بتبني microservices بتكلم بعضها، أو تطبيقات موبايل بتكلم backend، أو ألعاب multiplayer في الـ real-time، أو أجهزة IoT، الـ Protobuf هنا بيعتبر قوة خارقة (superpower). بيفرض عقود داتا صارمة (strict data contracts) بين التيمات وبعضها (لأن الكل بيعمل compile من نفس ملف الـ .proto)، وبيصطاد أخطاء الـ data-types في وقت الـ compile time، وبيعصر شبكة البنية التحتية بتاعتك عشان ياخد منها أقصى أداء ممكن.

غوص أكتر

  • Official Protocol Buffers Documentation - المصدر الرسمي من جوجل لكتابة الـ schemas واستخدام الكومبايلر.
  • Protobuf Encoding Specifications - غوصة عميقة وممتعة في التفاصيل الدقيقة للـ bitwise operations والـ wire types المستخدمة في الصيغة دي.
  • gRPC Official Site - الـ RPC framework عالي الأداء اللي بيستخدم Protobuf كلغته الافتراضية للتوجيه (routing) والـ payload.
  • Variable-length quantity (Varints) on Wikipedia - الرياضيات والتاريخ ورا إزاي الـ Protobuf بيضغط الأرقام عن طريق سرقة الـ Most Significant Bit.

انتهينا من النظرية. حان وقت التطبيق — 100% في متصفحك.

جرّب الأداة: فك تشفير Protobuf