Skip to content

Protobuf

الموقع الرسمي: Protocol Buffers | Google Developers

مقدمة

البرنامج التعليمي الرسمي: Protocol Buffer Basics: Go | Protocol Buffers | Google Developers

Protocol Buffers هي آلية تسلسل بيانات هيكلية مستقلة عن اللغة والبروتوكول وقابلة للتوسيع،开源تها جوجل في عام 2008، وهي أسرع في عمليات فك الحزم وتغليفها، وتُستخدم غالبًا في مجال الاتصالات RPC. يمكن تعريف الطريقة الهيكلية للبيانات، ثم استخدام الكود المصدري المولد خصيصًا لكتابة البيانات الهيكلية وقراءتها بسهولة من مختلف تدفقات البيانات، وتستخدم في لغات متعددة. فيما يلي سنشير إلى Protocol Buffers بـ protobuf.

protobuf شائع نسبيًا، خاصة في مجال Go، حيث يستخدمه gRPC كآلية تسلسل لنقل البروتوكول.

الصيغة

أولًا، لنلقِ نظرة على مثال لفهم الشكل العام لملف protobuf، بشكل عام صياغته بسيطة جدًا ويمكن إتقانها في دقائق. فيما يلي مثال لملف باسم search.proto، ولاحقة ملفات protobuf هي .proto.

protobuf
syntax = "proto3";

message SearchRequest {
  string query = 1;
  string number = 2;
}

message SearchResult {
  string data = 1;
}

service SearchService {
  rpc Search(SearchRequest) returns(SearchResult);
}
  • السطر الأول syntax = "proto3"; يشير إلى استخدام صياغة proto3، والافتراضي هو استخدام صياغة proto3.
  • طريقة الإعلان عن message مشابهة للهياكل، وهي الهيكل الأساسي في proto
  • SearchRequest يحدد ثلاثة حقول، كل حقل له اسم ونوع
  • service يحدد خدمة، والخدمة تحتوي على واجهة rpc واحدة أو أكثر
  • واجهة rpc يجب أن يكون لها معامل واحد وقيمة إرجاع واحدة فقط، وأنواعها يجب أن تكون message، ولا يمكن أن تكون أنواعًا أساسية.

يجب أيضًا ملاحظة أن كل سطر في ملف proto يجب أن ينتهي بفاصلة منقوطة.

التعليقات

نمط التعليقات متطابق تمامًا مع Go.

protobuf
syntax = "proto3";

/* تعليق
 * تعليق */
message SearchRequest {
  string query = 1; //تعليق
  string number = 2;
}

الأنواع

يمكن لمعدّلات الأنواع أن تظهر فقط في message، ولا يمكن أن تظهر بشكل منفصل.

الأنواع الأساسية

proto TypeGo Type
doublefloat64
floatfloat32
int32int32
int64int64
uint32uint32
uint64uint64
sint32int32
sint64int64
fixed32uint32
fixed64uint64
sfixed32int32
sfixed64int64
boolbool
stringstring
bytes[]byte

المصفوفات

إضافة معدّل repeated قبل النوع الأساسي تشير إلى أن هذا نوع مصفوفة، ويقابل الشرائح في Go.

protobuf
message Company {
  repeated string employee = 1;
}

map

تنسيق تعريف نوع map في protobuf كالتالي

map<key_type, value_type> map_field = N;

key_type يجب أن يكون رقمًا أو سلسلة نصية، value_type ليس له قيود على النوع، لنرَ مثالًا

protobuf
message Person {
  map<string, int64> cards = 1;
}

الحقول

في الواقع، proto ليس نوع قيمة-مفتاح تقليدي، في ملفات proto المُعلنة لن تظهر بيانات ملموسة، ما يتبع = في كل حقل يجب أن يكون رقمًا فريدًا في message الحالي، هذه الأرقام تُستخدم لتحديد وتعريف هذه الحقول في نص الرسالة الثنائية. الأرقام تبدأ من 1، الأرقام 1-15 ستشغل بايت واحد، 16-2047 ستشغل بايتين، لذا حاول إعطاء الأرقام 1-15 للحقول التي تظهر بشكل متكرر لتوفير المساحة، ويجب ترك بعض المساحة للحقول التي قد تظهر بشكل متكرر لاحقًا.

يجب أن تتبع الحقول في message واحد القواعد التالية

  • singular: الافتراضي هو هذا النوع من الحقول، في message جيد البنية، يمكن أن يكون هناك 0 أو 1 فقط من هذا الحقل، أي لا يمكن تكرار نفس الحقل. الإعلان التالي سيؤدي لخطأ.

    protobuf
    syntax = "proto3";
    
    message SearchRequest {
      string query = 1;
      string number = 2;
      string number = 3;//حقل مكرر
    }
  • optional: مشابه لـ singular، لكن يمكن التحقق بشكل صريح مما إذا كانت قيمة الحقل قد تم تعيينها، وقد يكون هناك الحالتان التاليتان

    • set: سيتم تسلسله
    • unset: لن يتم تسلسله
  • repeated: يمكن أن يظهر هذا النوع من الحقول 0 مرات أو أكثر، وسيتم الاحتفاظ بالقيم المكررة بالترتيب (بصراحة هو مصفوفة، يمكن السماح لنفس نوع القيمة بالظهور عدة مرات، والاحتفاظ بها بترتيب الظهور، أي الفهرس)

  • map: حقل من نوع قيمة-مفتاح، طريقة الإعلان كالتالي

    protobuf
    map<string,int32> config = 3;

الحقول المحجوزة

الكلمة المفتاحية reserve يمكنها الإعلان عن حقول محجوزة، بعد الإعلان عن رقم حقل محجوز، لن يتمكن من استخدامه كرقم أو اسم لحقل آخر، وسيحدث خطأ عند الترجمة. الجواب الرسمي من جوجل هو: إذا حذف ملف proto بعض الأرقام في نسخة جديدة، فقد يعيد مستخدمون آخرون استخدام هذه الأرقام المحذوفة في المستقبل، لكن إذا تم التبديل لأرقام النسخة القديمة فسيؤدي ذلك لعدم تطابق الأرقام مع الحقول وبالتالي حدوث أخطاء، الحقول المحجوزة يمكنها تذكيرك في وقت الترجمة بعدم استخدام هذا الحقل المحجوز، وإلا لن تنجح الترجمة.

protobuf
syntax = "proto3";

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  reserved "a"; //الإعلان عن حقل محجوز باسم محدد
  reserved 1 to 2; //الإعلان عن تسلسل أرقام كحقول محجوزة
  reserved 3,4; //الإعلان
}

وبهذا، لن ينجح ترجمة هذا الملف.

الحقول المهملة

إذا كان حقل ما مهملًا، يمكن كتابته كالتالي.

protobuf
message Body {
  string name = 1 [deprecated = true];
}

التعدادات

يمكن الإعلان عن ثوابت تعداد واستخدامها كنوع للحقل، يجب ملاحظة أن العنصر الأول في التعداد يجب أن يكون صفرًا، لأن القيمة الافتراضية لعناصر التعداد هي العنصر الأول.

protobuf
syntax = "proto3";

enum Type {
  GET = 0;
  POST = 1;
  PUT = 2;
  DELETE = 3;
}

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  Type type = 5;
}

عندما يكون هناك عناصر تعداد بنفس القيمة داخل التعداد، يمكن استخدام أسماء مستعارة للتعداد

protobuf
syntax = "proto3";

enum Type {
  option allow_alias = true; //يجب تفعيل خيار السماح باستخدام الأسماء المستعارة
  GET = 0;
  GET_ALIAS = 0; //اسم مستعار لعنصر تعداد GET
  POST = 1;
  PUT = 2;
  DELETE = 3;
}

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  Type type = 5;
}

الرسائل المتداخلة

protobuf
message Outer {                  // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

يمكن تداخل الإعلان عن message داخل message، تمامًا مثل الهياكل المتداخلة.

Package

يمكنك إضافة معدّل حزمة اختياري لملف protobuf لمنع تعارض الأسماء بين أنواع رسائل البروتوكول.

protobuf
package foo.bar;
message Open { ... }

ثم يمكنك استخدام اسم الحزمة عند تعريف حقول نوع الرسالة:

protobuf
message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

Import

الاستيراد يسمح لملفات protobuf متعددة بمشاركة التعريفات، صياغته كالتالي، ولا يمكن حذف امتداد الملف عند الاستيراد.

protobuf
import "a/b/c.proto";

عند الاستيراد تُستخدم المسارات النسبية، هذا المسار النسبي لا يشير إلى المسار النسبي بين ملف الاستيراد والملف المستورد، بل يعتمد على مسار المسح المحدد عند إنشاء الكود بواسطة مترجم protoc. لنفترض وجود هيكل ملفات كالتالي

pb_learn
│  common.proto

├─monster
│      monster.proto

└─player
        health.proto
        player.proto

إذا كنا نريد إنشاء كود دليل player فقط، وحددنا دليل player فقط عند مسح المسار، فإن الاستيراد المتبادل بين health.proto و player.proto يمكن كتابة اسم الملف مباشرة، مثل استيراد player.proto لـ health.proto.

protobuf
import "health.proto";

إذا قام player.proto باستيراد common.proto أو ملفات من دليل monster، فستفشل الترجمة، لذا الكتابة التالية خاطئة تمامًا، لأن المترجم لا يستطيع العثور على هذه الملفات.

go
import "../common.proto"; // كتابة خاطئة

TIP

بالمناسبة، الرموز ..، . غير مسموح بها في مسارات الاستيراد.

لنفترض أننا حددنا pb_learn كمسار مسح عند الترجمة، فيمكن حينها استيراد ملفات من أدلة أخرى عبر المسار النسبي، والمسار الفعلي المستورد هو العنوان المطلق للملف بالنسبة للعنوان النسبي لـ pb_learn. لنرَ مثال استيراد player.proto لملفات أخرى.

protobuf
import "common.proto";
imrpot "monster/monster.proto";
import "player/health.proto";

حتى health.proto في نفس الدليل يجب استخدام المسار النسبي. لذا في مشروع واحد، عادةً ننشئ مجلدًا منفصلًا لتخزين جميع ملفات protobuf، ونحدده كمسار مسح عند الترجمة، وجميع سلوكيات الاستيراد في هذا الدليل تعتمد على مساره النسبي.

TIP

إذا كنت تستخدم محرر goland، للدليل protobuf الذي أنشأته بنفسك، لن يتمكن من التحليل افتراضيًا، وسيظهر باللون الأحمر، لجعل goland يتعرف عليه يجب تعيين مسار المسح يدويًا، ومبدأه نفس ما شرحناه أعلاه، طريقة الإعداد كالتالي، افتح الإعدادات التالية

File | Settings | Languages & Frameworks | Protocol Buffers

في Import Paths أضف مسار المسح يدويًا، هذا المسار يجب أن يكون نفس المسار المحدد عند الترجمة.

Any

نوع Any يسمح لك باستخدام الرسائل كنوع مضمن دون الحاجة لتعريف proto الخاص بها، يمكننا استيراد نوع معرف من جوجل مباشرة، وهو مدمج ولا يحتاج لكتابة يدوية.

protobuf
import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

جوجل أيضًا عرفت مسبقًا أنواعًا كثيرة أخرى، انتقل إلى protobuf/ptypes at master · golang/protobuf (github.com) لمعرفة المزيد، تشمل بشكل رئيسي

  • تغليف الأنواع الأساسية
  • نوع الوقت
  • نوع Duration

تعريفات protobuf الخاصة بها يجب أن تكون في دليل include لمترجم protoc.

OneOf

شرح الوثائق الرسمية هنا معقد جدًا، ببساطة يعني أن حقلًا قد يكون له عدة أنواع محتملة عند النقل، لكن في النهاية نوع واحد فقط قد يُستخدم، ولا يُسمح بظهور حقول بمعدّل repeated داخله، وهذا يشبه union في لغة C.

protobuf
message Stock {
    // Stock-specific data
}

message Currency {
    // Currency-specific data
}

message ChangeNotification {
  int32 id = 1;
  oneof instrument {
    Stock stock = 2;
    Currency currency = 3;
  }
}

Service

الكلمة المفتاحية service يمكنها تعريف خدمة RPC، خدمة RPC تحتوي على عدة واجهات rpc، وتنقسم الواجهات إلى واجهات أحادية وواجهات انسيابية.

protobuf
message Body {
  string name = 1;
}

service ExampleService {
  rpc DoSomething(Body) returns(Body);
}

الواجهات الانسيابية تنقسم إلى انسيابية أحادية وثنائية الاتجاه، وعادةً تُعدّل بالكلمة المفتاحية stream، لنرَ المثال التالي.

protobuf
message Body {
  string name = 1;
}

service ExampleService {
  // انسيابية من العميل
  rpc DoSomething(stream Body) returns(Body);
  // انسيابية من الخادم
  rpc DoSomething1(Body) returns(stream Body);
  // انسيابية ثنائية الاتجاه
  rpc DoSomething2(stream Body) returns(stream Body);
}

ما يسمى بالانسيابية هو إرسال البيانات بشكل متبادل على مدى طويل في اتصال واحد، بدلًا من نمط السؤال والجواب البسيط مثل الواجهات الأحادية.

Empty

empty في الواقع هو message فارغ، يقابل الهيكل الفارغ في Go، نادرًا ما يُستخدم لتعديل الحقول، يُستخدم بشكل أساسي للإشارة إلى أن واجهة rpc معينة لا تحتاج لمعاملات أو ليس لها قيمة إرجاع.

protobuf
syntax = "proto3";

import "google/protobuf/empty.proto";

service EmptyService {
  rpc Do(google.protobuf.Empty) returns(google.protobuf.Empty);
}

Option

option عادةً يُستخدم للتحكم في بعض سلوكيات protobuf. مثل التحكم في حزمة الكود المصدري Go المُولّد، يمكن الإعلان عنه كالتالي.

protobuf
option go_package = "github/jack/sample/pb_learn;pb_learn"

ما قبل الفاصلة المنقوطة هو مسار استيراد الملفات المصدرية الأخرى بعد توليد الكود، وما بعد الفاصلة المنقوطة هو اسم الحزمة للملف المُولّد.

يمكنه عمل بعض التحسينات، هناك عدة قيم متاحة، لا يمكن تكرار الإعلان.

  • SPEED، أعلى درجة تحسين، أكبر حجم كود مُولّد، هذا هو الافتراضي.
  • CODE_SIZE، سيقلل حجم الكود المُولّد، لكنه سيعتمد على الانعكاس للتسلسل
  • LIFE_RUNEIMTE، أصغر حجم كود، لكنه سينقص بعض الميزات.

فيما يلي حالة استخدام

protobuf
option optimize_for = SPEED;

بالإضافة إلى ذلك، يمكن لـ option إضافة بعض المعلومات الوصفية لـ message و enum، ويمكن الحصول على هذه المعلومات عبر الانعكاس، وهذا مفيد جدًا عند التحقق من المعاملات.

الترجمة

الترجمة أي توليد الكود، ما سبق كان مجرد تعريف لملفات protobuf، للاستخدام الفعلي يجب تحويلها لكود مصدري بلغة معينة، نستخدم مترجم protoc لذلك، وهو يدعم لغات متعددة.

التثبيت

لتنزيل المترجم انتقل إلى protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format (github.com) لتنزيل أحدث إصدار Release، عادةً يكون ملفًا مضغوطًا

protoc-25.1-win64
│  readme.txt

├─bin
│      protoc.exe

└─include
    └─google
        └─protobuf
            │  any.proto
            │  api.proto
            │  descriptor.proto
            │  duration.proto
            │  empty.proto
            │  field_mask.proto
            │  source_context.proto
            │  struct.proto
            │  timestamp.proto
            │  type.proto
            │  wrappers.proto

            └─compiler
                    plugin.proto

بعد التنزيل، أضف دليل bin إلى متغيرات البيئة، لتتمكن من استخدام أمر protoc، بعد الانتهاء تحقق من الإصدار، المخرجات الطبيعية تعني نجاح التثبيت

bash
$ protoc --version
libprotoc 25.1

المترجم المنزّل لا يدعم لغة Go افتراضيًا، لأن توليد كود Go هو ملف تنفيذي منفصل، بينما اللغات الأخرى مدمجة معًا، لذا نثبّت إضافة لغة Go، تُستخدم لترجمة تعريفات protocbuf لكود مصدري Go.

bash
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

إذا كنت تحتاج أيضًا لتوليد كود خدمة gRPC، ثبّت الإضافة التالية

bash
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

بعد التثبيت تحقق من الإصدار

bash
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.3.0

$ protoc-gen-go --version
protoc-gen-go.exe v1.31.0

هذه الإضافات هي أيضًا ملفات تنفيذية منفصلة، لكن لا يمكن استدعاؤها إلا عبر protoc، ولا يمكن تنفيذها بشكل منفصل.

(this program should be run by protoc, not directly)

بالإضافة إلى ذلك هناك العديد من الإضافات الأخرى، مثل إضافة توليد وثائق واجهة openapi وغيرها، يمكنك البحث عنها بنفسك إذا كنت مهتمًا.

التوليد

لنأخذ المثال السابق، الهيكل كالتالي

pb_learn
│  common.proto

├─monster
│      monster.proto

└─player
        health.proto
        player.proto

لتوليد الكود، يجب تحديد ثلاثة معاملات إجمالًا

  1. مسار المسح، يُرشد المترجم من أين البحث عن ملفات protobuf وكيف تحليل مسارات الاستيراد
  2. مسار التوليد، أين توضع الملفات بعد الترجمة
  3. الملفات المستهدفة، تحديد الملفات المستهدفة المراد ترجمتها.

قبل البدء تأكد من إعداد go_package في ملفات protobuf بشكل صحيح، استخدم protoc -h لعرض المعاملات المدعومة، الأكثر استخدامًا هو -I أو --proto_path، يمكن استخدامه عدة مرات لتحديد مسارات مسح متعددة، مثل

bash
$ protoc --proto_path=./pb_learn --proto_path=./third_party

تحديد مسار المسح فقط ليس كافيًا، يجب أيضًا تحديد مسار التوليد والملفات المستهدفة protobuf، هنا نولّد ملفات go لذا نستخدم معامل --go_out، مدعوم من إضافة protoc-gen-go التي نزّلناها سابقًا.

bash
$ cd pb_learn

$ protoc --proto_path=. --go_out=. common.proto

$ ls
common.pb.go  common.proto  monster/  player/

معامل --go_out هو مسار التوليد المحدد، . يشير للمسار الحالي، common.proto هو الملف المحدد للترجمة. إذا كنت تريد توليد كود grpc (بشرط تثبيت إضافة grpc)، يمكنك إضافة معامل --go-grpc_out (إذا لم يكن هناك تعريف service في ملف protobuf، لن يتم توليد الملف المقابل).

bash
$ protoc --proto_path=. --go_out=. --go-grpc_out=. common.proto

$ ls
common.pb.go  common.proto  common_grpc.pb.go  monster/  player/

common.pb.go هو تعريف نوع protobuf المُولّد، common_grpc.pb.go هو كود gRPC المُولّد، وهو مبني على الأول، إذا لم يتم توليد تعريف للغة المقابلة، فلا يمكن توليد كود gRPC.

إذا كنت تريد ترجمة جميع ملفات protobuf في هذا الدليل، يمكنك استخدام الرمز *، مثل

bash
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./*.proto

إذا كنت تريد تضمين جميع الملفات، يمكنك استخدام الرمز **، مثل ./**/*.proto.

bash
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./*.proto

لكن، هذه الطريقة تنطبق فقط على الصدفة التي تدعم هذا النوع من الرموز، مثل في Windows، cmd أو powershell لا يدعمان هذه الكتابة

powershell
D> protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./**/*.proto
Invalid file name pattern or missing input file "./**/*.proto"

لحسن الحظ gitbash يدعم العديد من أوامر linux، ويمكنه جعل windows يدعم هذه الصيغة. لتجنب كتابة أوامر متكررة في كل مرة، يمكن وضعها في makefile

makefile
.PHONY: all

proto_gen:
  protoc --proto_path=. \
       --go_out=paths=source_relative:. \
       --go-grpc_out=paths=source_relative:. \
       ./**/*.proto ./*.proto

يمكن ملاحظة وجود paths=source_relative:. إضافي، هذا إعداد لنمط مسار توليد الملفات، هناك عدة خيارات متاحة

  • paths=import، هذا هو الافتراضي، سيتم توليد الملفات في الدليل المحدد بواسطة import، ويمكن أن يكون أيضًا مسار وحدة. مثل وجود ملف protos/buzz.proto، تحديد paths=example.com/project/protos/fizz، فسيتم في النهاية توليد example.com/project/protos/fizz/buzz.pb.go.
  • module=$PREFIX، عند التوليد، سيتم حذف بادئة المسار. في المثال أعلاه، إذا تم تحديد البادئة example.com/project، فسيتم في النهاية توليد protos/fizz/buzz.pb.go، هذا النمط يُستخدم بشكل أساسي لتوليده مباشرة في الوحدة (يبدو أنه لا يوجد فرق كبير).
  • paths=source_relative، الملفات المُولّدة ستحتفظ بنفس الهيكل النسبي لملفات protobuf في الدليل المحدد.

النقطتان : تفصل بين نمط المسار ومسار التوليد المحدد.

|  common.proto
|  common.pb.go

├─monster
│      monster.pb.go
│      monster.proto

└─player
        health.pb.go
        health.proto
        health_grpc.pb.go
        player.pb.go
        player.proto

الانعكاس

من خلال options يمكن توسيع enum و message، أولًا استورد "google/protobuf/descriptor.proto"

protobuf
import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  optional string string_name = 123456789;
}

enum Integer {
  INT64 = 0[
    (string_name) = "int_64"
  ];
}

هذا يكافئ إضافة معلومات وصفية لقيمة التعداد هذه. ونفس الشيء ينطبق على message، كالتالي

protobuf
import "google/protobuf/descriptor.proto";

extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}

message MyMessage {
  option (my_option) = "Hello world!";
}

هذا يكافئ انعكاس protobuf، بعد توليد الكود يمكن الوصول إليه عبر Descriptor، كالتالي

go
func main() {
  message := pb_learn.MyMessage{}
  message.ProtoReflect().Descriptor().Options().ProtoReflect().Range(func(descriptor protoreflect.FieldDescriptor, value protoreflect.Value) bool {
    fmt.Println(descriptor.FullName(), ":", value)
    return true
  })
}

المخرجات

my_option:"Hello world!"

هذه الطريقة يمكن تشبيهها بإضافة tag للهياكل في Go، وهي متشابهة إلى حد كبير، وفقًا لهذه الطريقة يمكن أيضًا تنفيذ وظيفة التحقق من المعاملات، فقط تحتاج لكتابة القواعد في options، والتحقق عبر Descriptor.

Golang تم تحريره بواسطة www.golangdev.cn