عرفان
عرفان
خواندن ۸ دقیقه·۶ سال پیش

پروتوباف (قسمت دوم)

اگه قسمت اول پروتوباف نخوندین پیشنهاد میدم حتما اول قسمت اول رو بخونید:

https://virgool.io/@erfun/protobuf-%DA%86%DB%8C%D9%87-%D9%88-%DA%86%D8%B7%D9%88%D8%B1-%DA%A9%D8%A7%D8%B1-%D9%85%DB%8C%DA%A9%D9%86%D9%87-pbgamxvyh285

آپدیت کردن فایل پروتو

بیزینس شما رشد میکنه و شما باید API هارو آپدیت کنید، بعضی فیلدها تغییر می‌کنند، بعضی فیلد‌ها اضافه و بعضی حدف میشن.

بیاید یه سناریو درنظر بگیریم، امروز موقع ثبت‌نام فقط نام و نام خانوادگی دریافت می‌کنید، ولی بعد یه مدت به شماره تلفن کاربران نیاز پیدا می‌کنید. ولی شما نمی‌تونید در یک لحظه فایل پروتو تمام اپلیکیشن‌هایی که دارن از سرویس شما استفاده می‌کنند آپدیت کنید.

خوشبختانه پروتوبافر اینجاهم به دادمون میرسه تا بتونیم بدون دردسر به توسعه اپ ادامه بدیم، پروتوباف full compatibe هست، اگر با این مفهوم آشنا نیستید به عکس پایین نگاه کنید:

در سناریو یک دیتا با فایل جدید نوشته میشه ولی با فایل قدیمی خونده میشه و مشکلی پیش نمیاد.

در سناریو دوم دیتا با فایل قدیمی نوشته میشه ولی با فایل جدید خونده میشه که بازم مشکلی پیش نمیاد.


ولی اینجا چندتا قانون برای آپدیت فایل پروتو داریم:

  • هیچوقت نباید تگ فیلدهارو تغییر بدیم.
  • شما می‌تونید فیلد جدید اضافه کنید، ولی اپلیکیشن با فایل پروتو قبلی اون فیلد‌هارو درنظر نمی‌گیره.
  • اگر اپلیکیشن شما بخواد فیلدی رو از ریکوئست دریافت کنه ولی نتونه پیداش کنه، مقدار پیش‌فرض درنظر میگیره. (به همین دلیله باید مقدار دیفالت رو بشناسید و مدیریتشون کنید)
  • همچنین فیلدها می‌تونند حذف بشن، ولی نباید دوباره تگ اون فیلد توی اون مسیج استفاده بشه. برای حذف می‌تونید به اول اسم فیلد OBSOLETE_ اضافه کنید یا تگ رو به reservet اضافه کنید.
  • برای تغییر دیتاتایپ (برای مثال از int32 به int64) بهتره از داکیومنت اصلی کمک بگیرید، چون خیلی پیچیدس و بهتره خودتون بخونید. (به نظرم هیچوقت دیتاتایپ رو عوض نکنید، فقط قبلی رو حذف و یک جدید ایجاد کنید)

اضافه کردن فیلد جدید

برای اضافه کردن تگ جدید کافیه فیلدتون رو طبق معمول تعریف کنید و یک تگ جدید براش درنظر بگیرید. برای مثال:

message MyMessage { int32 id = 1; }

به این صورت تغییر میدیم:

message MyMessage { int32 id = 1; string name = 2; }

اگر مسیج جدید به اپلیکشنی با فایل قدیمی ارسال بشه، اپلیکیشن قدیمی متوجه نمیشه تگ ۲ چیه، پس کلا درنظرش نمیگیره.

حالا برعکس، اگر مسیج قدیمی به اپلیکیشنی با فایل جدید ارسال بشه، اپلیکیشن جدید تگ ۲ دریافت نمیکنه، پس مقدار name رو مقدار پیش‌فرض (استرینگ خالی) درنظر میگیره.

مقادیر پیش‌فرض به ما اجازه میدن بدون آپدیت سرویس‌های وابسته فایل پروتو توسعه بدیم و فیلدهایی اضافه یا کم کنیم.


تغییر نام فیلد

تغییر نام یک فیلد خیلی خیلی سادس، فقط کافیه نام یک فیلد رو تغییر بدید، همین. برای مثال میخوای مقدار name رو به first_name تغییر بدیم:

message MyMessage { int32 id = 1; string name = 2; }

فایل بالا رو به این صورت تغییر میدیم:

message MyMessage { int32 id = 1; string first_name = 2; }

تمام اپلیکیشن‌های قبلی به کارشون ادامه میدن، مشکلی پیش نمیاد و نیاز به آپدیت فایل پروتو بقیه سرویس‌ها ندارید. همونطور که قبلا گفتیم فقط تگ مهمه و اسم فیلدها موقع کدنویسی به کار میاد.


حذف فیلد

برای حذف یک فیلد میتونید از داخل فایل حذفش کنید برای مثال فیلد name رو حذف میکنیم:

message MyMessage { int32 id = 1; string first_name = 2; }

و به این صورت حذف میکنیم:

message MyMessage { int32 id = 1; }

و تمام، فیلد حذف شد، اگر کد شما نتونه فیلد موردنظر رو پیدا کنه مقدار پیش‌فرض براش درنظر میگیره، و اگر دیتای فایل قدیمی رو با مسیج جدید بخونیم، مقدار first_name درنظر گرفته نمیشه و حذف میشه.

وقتی شما یک تگ رو حذف می‌کنید دیگه نباید هیچوقت از اون تگ و اسم فیلد استفاده کنید، به اصلاح میگن reserv کردن.

برای رزرو کردن یک تگ به این صورت عمل میکنیم:

message MyMessage { int32 id = 1; reserved 2; reserved "first_name"; }

دلیل انجام این کار اینه که یادمون باشه این تگ و اسم دیگه هیچوقت نباید داخل این مسیج استفاده بشن تا کانفلیکتی پیش نیاد.

یا روش دیگه برای حذف یک فید به این صورته:

message MyMessage { int32 id = 1; string OBSOLETE_first_name = 2; }

این روش در عمل فرقی با روش قبلی نداره، هرکدوم که مایل هستید می‌تونید انجام بدید.


رزرو فیلد‌ها

شما میتونید تگ یا اسم فیلدهارو به دلایل مختلف رزرو کنید و حتی رنجی از تگ‌هارو رزرو کنید:

message Foo { reserved 2, 5, 10 to 14; reserved "Foo", "Bar"; }

ولی یادتون باشه نمی‌تونید تگ و اسم فیلد باهم میکس و رزرو کنید، حتما باید جدا جدا باشن.

نکته مهم اینه هیچوقت و هیچوقت مقادیر رزرو رو حذف نکنید، برای مثال بعد چندماه یا چندسال نگید خب خیلی وقته از این رزروها میگذره و دیگه بهش نیازی ندارم پس بذار حذفش کنم، نه، شما همیشه بهشون نیاز دارید، همیشه ممکنه به باگ یا کانفلیکت بخورید.


مقادیر عددی

احتمالا موقع نوشتن فایل پروتو گیج بشید که باید از کدوم یکی نوع مقدار استفاده کنید، برای مثال فقط برای اعداد گزینه‌های: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64 می‌تونید انتخاب کنید.

برای روشن‌تر شدن موضوع توصیه میکنم از اینجا به داکیومنت اصلی نگاهی بندازید، کمی اسکرول دان کنید جدولی میبینید که توضیح کاملی از تایپ موردنظر تو زبانی که کار می‌کنید داره. اگر بازهم متوجه نشدید سعی میکنم توضیح کوتاهی درموردش بدم.

  • تمام مقادیر دو نوع ۳۲ بیت و ۶۴ بیت دارن.
  • تمام نوع‌ها ممکنه بتونند اعداد منفی بگیرن یا نگیرن. (Signed/Unsigned)
  • تمام این تایپ‌ها موقع کامپایل تبدیل به تایپ قابل فهم برای زبان‌های برنامه شما میشن، پس حتما از جدول این صفحه استفاده کنید.


درک این تایپ‌ها کمک میکنه تا برنامه‌ای با پرفورمنس بالا بنویسید.

  • تایپ int32 و sint32 میتونه مقادیر -۲،۱۴۷،۴۸۳،۶۴۸ تا ۲،۱۴۷،۴۸۳،۶۴۷ در خودش ذخیره کنه
  • تایپ uint32 میتونه مقادیر ۰ تا ۴،۲۹۴،۹۶۷،۲۹۵ در خودش ذخیره کنه.
  • تایپ int64 و sint64 میتونه مقادیر -۹،۲۲۳،۳۷۲،۰۳۶،۸۵۴،۷۷۵،۸۰۸ تا ۹،۲۲۳،۳۷۲،۰۳۶،۸۵۴،۷۷۵،۸۰۷ در خودش ذخیره کنه.
  • تایپ uint64 میتونه مقادیر ۰ تا ۱۸،۴۴۶،۷۴۴،۰۷۳،۷۰۹،۵۵۱،۶۱۵ در خودش ذخیره کنه.


بنابراین یادتون باشه تایپ‌های uint32 و uint64 یا به اصطلاح Unsigned نمی‌تونند مقادیر منفی در خودشون ذخیره کنند.

تایپ‌های int32 و int64 به صورت رسمی اعداد منفی قبول نمی‌کنند. (در این تایپ اعداد منفی فضای بیشتری اشغال می‌کنند)

تایپ‌های sint32 و sint64 اعداد منفی قبول می‌کنند. (با تکنیکی به اسم ZigZag اعداد منفی فضای کمی اشغال می‌کنند)

تایپ fixed32 همیشه ۴ بایت اشغال می‌کنه، اگر عددتون بزرگتر از ۲۶۸،۴۳۵،۴۵۶ بود کاراییش بهتر از uint32 هست، بهتره از این تایپ استفاده کنید.

تایپ fixed64 همیشه ۸ بایت اشتغال می‌کنه، اگر عددتون بزرگتر از ۷۲،۰۵۷،۵۹۴،۰۳۷،۹۲۷،۹۴۰ بود کاراییش بهتر از uint64 هست، بهتره از این تایپ استفاده کنید.


تایپ OneOf

تایپ OneOf میگه فقط یکی از متغییرهایی که براش تعریف میکنیم می‌تونه مقدار داشته باشه.

message MyMessage { int32 id = 1; oneof example_oneof { string my_string = 2; bool my_bool = 3; } }

در مسیج بالا داریم میگیم یه آیدی داریم و بعد از اون یکی از مقادیر my_string یا my_bool داریم، پس وقتی داریم این دو مقدار رو میخونیم فقط ممکنه یکی از اون‌ها مقداری روش ست شده باشه. و اما چند نکته:

فیلدهای oneof نمیتونند repated باشن.

توسعه oneof میتونه خیلی پیچیده باشه.

در موقع خواندن مسیج oneof، همه فیلدها نال خواهد بود، بجز آخرین فیلدی که موقع ارسال درخواست ست کردید. پس اگر my_string و my_bool ست کنید فقط my_bool داری مقدار خواهد بود و my_string نال خواهد بود.


تایپ map

در این تایپ می‌تونید اطلاعات رو به صورت key/value تعریف کنید (مشابه جیسون). از تمامی تایپ‌ها غیر از float/double پشتیابی میکنه، برای مثال:

message MyMessage { map<int32, Profile> profiles = 1; } message Profile { int32 id = 1; string name = 2; int32 phone_number = 3; }

در مثال بالا ما مجموعه‌ای از یوزرها به همراه پروفایلشون داریم، که کلید هر پروفایل یوزرآیدی و مقدارش مسیج دیگه‌ای به عنوان پروفایله. و اما چند نکته:

فیلدهای map نمیتونند repated باشن.

هیچگونه مرتب سازی روی مپ اتفاق نمیفته، چون مپ key/value هستش.


تایپ timestamp

پروتوبافر مجموعه‌ای از تایپ‌های مخصوص خودش رو داره با عنوان well known types که در تمام زبانهای برنامه نویسی پشتیبانی میشن. اینجا می‌تونید لیست کامل این تایپ‌هارو ببینید.

یکی از این تایپ‌ها timestamp هست، که فیلدهای ثانیه و نانوثانیه داره. قبل استفاده این نوع تایپ حتما باید ایمپورتش کنید، برای مثال:

import "google/protobuf/timestamp.proto"; message MyMessage { google.protobuf.Timestamp my_field = 1; }

آپشن‌ها

آپشن‌ها زمان کامپایل به کار میان و برای هر زبان ویژگی خاصی رو اضافه می‌کنند، برای مثال میشه گفت در زمان تولید کد برای زبان گولنگ اسم پکیج گو رو فلان چیز ست کنه، از اونجایی که لیست طولانی داره و تعدادشون بالاست توصیه میکنم داکیومنت رو برای زبان مورد نظرتون بخونید، برای مثال:

syntax = "proto3"; package google.protobuf; option csharp_namespace = "Google.Protobuf.WellKnownTypes"; option cc_enable_arenas = true; option go_package = "github.com/golang/protobuf/ptypes/duration"; option java_package = "com.google.protobuf"; option java_outer_classname = "DurationProto"; option java_multiple_files = true; option objc_class_prefix = "GPB";

سرویس‌ها

خب میخوایم درمورد ویژگی اصلی پروتوبافر حرف بزنیم، در فایل پروتو بجز مسیج‌ها میتونید سرویس‌ها روهم تعریف کنید، سرویس، اندپوینتی هست که کاربر (یا بقیه سرویس‌ها) میتونه بهش دسترسی داشته باشه، برای مثال:

syntax = "proto3"; message SearchRequest { int32 person_id = 1; } message SearchResponse { int32 person_id = 1; string person_name = 2; } service SearchService { rpc Search(SearchRequest) returns (SearchResponse); }

در این فایل یک سرویس سرچ تعریف کردیم و گفتیم انتظار داریم یک مسیج سرویس ریکوئست دریافت کنیم و در جواب یک مسیج سرچ ریسپانس برمیگردونیم، سادس درسته؟ در اصل این شیوه ساخت یک API در فایل پروتو هست، در اینجا به یک فریمورک نیاز داریم تا این سرویس رو هندل کنه و کدهاشو جنریت کنه، در پروتو ورژن ۳ از فریمورک gRPC گوگل کمک میگیره، ولی شما میتونید روی اینترنت فریمورک‌های دیگم پیدا کنید.

خب اینم از یک مثال، فرض کنید شما یک سرور جاوا دارید (ولی میتونه ره زبان دیگه‌ای باشه، به جز php) که یک سرویسی رو ارائه میده، حالا هر کلاینتی با هر زبانی، برای مثال گولنگ، پایتون، جاوا و... میتونه با کمک فایلهای پروتو کدهای کلاینت gRPC جنریت کنه، و کلاینت یک ریکوئست پروتوبافر تولید میکنه برای سرور میفرسته، و سرور در پاسخ یک ریسپانس پروتوبافر ارسال میکنه. پس پروتوبافر اینجا یک شیوه انتقال دیتا توی شبکه‌اس که تمام زبان‌هارو ساپورت میکنه که همین نقطه قوتش شده.


در پایان می‌تونید از اینجا با استایل گاید پروتوباف آشنا بشید.

در قسمت بعدی درمورد فریمورک gRPC بیشتر حرف میزنیم و چند نمونه عملی اجرا میکنیم.

microservicesgrpcprotocol buffersprotobuf
یه دولوپر که سعی می‌کنه عمیق و کم هزینه باشه، از هرچیزی که بلدم می‌نویسم تا مطمئن‌شم درست یادش گرفتم.
شاید از این پست‌ها خوشتان بیاید