آموزش gRPC در ASP.NET Core - قسمت دوم

آموزش gRPC در ASP.NET Core - قسمت دوم
آموزش gRPC در ASP.NET Core - قسمت دوم


https://vrgl.ir/U1mV1
در این قسمت میخواهیم با تعریف message و service و rpc method آشنا بشیم، همچنین انواع Data Type ها در Protocol Buffer یاد میگیریم.
و فایل های ایجاد شده توسط Protobuf Compiler را به صورت کامل بررسی میکنیم.
تعریف و استفاده از enum ها را هم یاد میگیریم.

به طور کلی 2 نوع رویکرد (approach) برای ساخت و طراحی API وجود دارد.

  1. Code First
  2. Design First (Contract First)
فریمورک gRPC برای ساخت API های RPC از رویکرد دوم یا همان contract-first استفاده میکند، این رویکرد نسبت به code-first جدیدتر، بهتر، پایدارتر و تمیزتر است.

همانطور که در قسمت اول گفته شد:

در ابتدا باید برای هر سرویس gRPC یک قرارداد ایجاد کنیم، این قرارداد در یک فایل با فرمت proto. قرار میگیرد.

از این قرارداد (contract) میتوان به چند شکل مختلف استفاده کرد:

  1. به شکل مستند (document): فایل های proto. دارای syntax آسان و به زبان انسان نزدیک هستند، پس در نتیجه با یک نگاه به این فایل ها میتوان متوجه شد که یک سرویس gRPC چه متدهایی را ارائه میکند.
  2. به شکل Blue Print: فایل های proto. را میتوانیم با استفاده از Protobuf Compiler به زبان های برنامه نویسی مختلف Compile و از آن ها در برنامه خودمان استفاده کنیم.
پس فایل های proto. در آن واحد هم Document و هم Blue Print هستند.
این فایل ها را دقیقا مثل یک Interface سی شارپی فرض کنید.


به طور معمول یک فایل proto:

  1. دارای چند Message است، که به عنوان ورودی و خروجی متد ها قرار می گیرند.
  2. دارای یک Service است، که امضاء متد های RPC در آن قرار می گیرند.


در ابتدا یک پروژه ASP.NET Core میسازیم.

بعد از اینکه پروژه ساخته شد، باید در آن پکیج Grpc.AspNetCore را نصب کنیم.

Install-Package Grpc.AspNetCore -Version 2.35.0 /* Package Manager Console */
dotnet add package Grpc.AspNetCore --version 2.35.0 /* .NET CLI  */

سپس یک Folder به نام Protos در پروژه ایجاد کنید و در آن یک Protocol Buffer File به نام ticket_api.proto بسازید. (نام فایل حتما به صورت lower_snake_case باشد.)

در این مرحله که فایل ticket_api.proto ساخته شد، در آن 2 خط کد مشاهده میکنیم.
خط اول مربوط به ورژن Protocol Buffers است و حتما باید مقدارش مساوی با proto3 باشد.

خط دوم به ما اجازه میدهد namespace کلاس هایی که بعدا از روی این فایل ticket_api.proto ساخته میشوند را به صورت صریح مشخص کنیم.

https://gist.github.com/ArminShoeibi/56fa55dade70906fedf7d38c8f497ab1

در فایل های proto. ای که قرار است برای زبان #C کامپایل شوند میتوانیم دو option قرار دهیم.

  1. option csharp_namespace
  2. option optimize_for
https://gist.github.com/ArminShoeibi/1548f9f96b19f22de328ca09cb84dd0b

پیام ( Message ) چیست ؟

پیام را دقیقا مثل یک DTO فرض کنید، وقتی یک message تعریف میکنیم، در اخر توسط Protobuf Compiler تبدیل به یک کلاس معمولی #C می شود.
Message ها در ورودی و خروجی متدهای RPC استفاده می شوند، در واقع داده ها را بین Client و Server جا به جا میکنند.

پیام (message) فقط می تواند دارای فیلد باشد و از خود هیچ رفتاری ندارد.

نحوه تعریف Message

  1. نوشتن کلمه کلیدی message
  2. تخصیص یک نام (Identifier) به آن (به صورت PascalCase نوشته شود.)
  3. نوشتن پراپرتی (فیلد) های مورد نیاز و تخصیص شماره به آن ها (نام فیلد ها به صورت lower_snake_case نوشته شود.)
https://gist.github.com/ArminShoeibi/ea5fdf68478cbf2223cbb20cd9d71690

شماره فیلد (Field Number) چیست ؟

شماره فیلد یکی از مهم ترین بخش های Protocol Buffer است، تخصیص شماره به فیلد های یک message اجباری است.

وقتی که داده هایمان توسط Protocol Buffer به آرایه ای از byte ها(binary format) سریالایز میشوند، از این اعداد برای شناسایی فیلد ها استفاده میشود.

در JSON اشیا به صورت Key-Value Pair هستند و Key ها هم به صورت string هستند، ولی در proto به جای استفاده از string به عنوان Key از اعداد استفاده میشود به دلیل حجم کمتر.

شماره فیلد های 1 تا 15 حجمشان یک byte است، در هر message از شماره 1 تا 15 را باید به فیلد های پر مصرف و پر کاربرد اختصاص بدهیم.

برای مثال در یک message بیست (20) فیلد داریم، بهتر است که حتما شماره 1 تا 15 را به فیلدهایی اختصاص بدهیم که بیشتر در Client و Server استفاده میشوند، برای مثال 5 فیلد از آن 20 فیلد مربوط به Audit هستند و خیلی کم استفاده میشوند یا اصلا استفاده نمیشوند ولی در message وجود دارند، در این صورت نباید شماره 1 تا 15 را به آن فیلد های Audit اختصاص بدهیم.
چون شماره فیلد های 1 تا 15 حجمشان یک byte است بهتر است حتما فیلد های پر استفاده از این اعداد استفاده کنند.

شماره فیلدهای 16 تا 2047 حجم شان دو byte است. از عدد های بالاتر هم می توانید استفاده کنید ولی پیشنهاد میشود اینکار را نکنید.


انواع Data Type های Value Type در Message

به طور پیشفرض پانزده Data Type از نوع Value Type در فایل proto. وجود دارد و ما میتوانیم از آن ها برای فیلد هایمان استفاده کنیم.

در تصویر پایین مشاهده میکنید که 15 عدد Data Type از نوع Value Type در یک فایل proto. قابل استفاده است، و این 15 عدد در آخر به 9 عدد Data Type در سی شارپ تبدیل میشوند.

در کد و تصویر زیر همانطور که مشاهده میکنید، از هر پانزده Data Type موجود در یک message استفاده کردیم.

https://gist.github.com/ArminShoeibi/0204502b76cc3ec2fcdd34c6cd92c035
"محمد جواد ابراهیمی":
در اینجا چند نکته درباره نوع داده های protobuf حائز اهمیت هست:

1️⃣ نوع های unsigned که تکلیفشون معلومه، فقط اعداد مثبت رو قبول میکنن مثلا نوع uint32 به uint درسی شارپ ترجمه میشه و فقط میتونه اعداد مثبت رو داشته باشه.

2️⃣ نوع int32 به int در سی شارپ ترجمه میشه و میتونه هم اعداد منفی و هم مثبت رو داشته باشه ولی برای اعداد منفی غیر بهینه عمل میکنه چرا که اعداد منفی رو به 10 بایت انکود میکنه که مشابه یک عدد مثبت خیلی بزرگ رفتار میشه باهاش.

3️⃣ نوع sint32 به int در سی شارپ ترجمه میشه و میتونه هم اعداد منفی و هم مثبت رو داشته باشه ولی برای اعداد منفی خیلی بهتر عمل میکنه چرا که از الگوریتم ZigZag استفاده میکنه و اعداد منفی رو به اعداد مثبت زیگ زاگی انکود میکنه به این صورت که عدد 1- میشه 1؛ عدد 1 میشه 2؛ عدد 2- میشه 3؛ عدد 3+ میشه 4
1-  --->  1
1+  --->  2
2-  --->  3
2+  --->  4
...
2147483647+  --->  4294967294
2147483648-  --->  4294967295
در نتیجه برای اعداد منفی فضا و پردازش کمتری رو میطلبه ولی برای اعداد مثبت فضا و پردازش بیشتری رو میطلبه

? نتیجه گیری:
از uint32  استفاده کنید اگر اعداد تون  "فقط مثبت" هستند.
از sint32 استفاده کنید اگر به مثبت و منفی نیاز دارید ولی اعداد تون "معمولا منفی" هستند.
از int32 استفاده کنید اگر به مثبت و منفی نیاز دارید ولی اعداد تون "به ندرت منفی" هستند.

?نکته:
همین قضیه برای int64 و sint64 و uint64 هم صدق میکنه.
نوع های fixed هم طولشون ثابت هست (بر خلاف نوع های غیر fixed که طولشون متغیر varint هست) مثلا نوع fixed32 و sfixed32 همیشه 4 بایت اشغال میکنند و نوع های fixed64 و sfixed64 هم همیشه 8 بایت.

سرویس (Service) چیست ؟

سرویس (service) را دقیقا مثل یک Interface سی شارپی فرض کنید که فقط در آن امضای Method ها را قرار می دهیم، و فقط هم محل قرارگیری امضای Method هایمان است.

به متد هایی که در سرویس قرار میگیرند RPC Method می گویند.

نحوه تعریف سرویس (service)

  1. نوشتن کلمه کلیدی service
  2. تخصیص یک نام به آن (به صورت PascalCase نوشته شود.)
  3. نوشتن متدهای مورد نیاز (همراه با پیشوند rpc)
https://gist.github.com/ArminShoeibi/d9892d24254396f1c703057415204043

حالا که message و service و rpc method های مورد نیاز را ساختیم، باید این فایل proto. را به صورت Server ای Compile کنیم.

? نکته: یک فایل proto. را میتوان به 2 صورت Compile کرد.
1. کامپایل مخصوص Client
2. کامپایل مخصوص Server

کامپایل فایل ticket_api.proto به صورت Server و بررسی فایل های ایجاد شده توسط protoc در پروژه Server

ابتدا وارد Connected Services پروژه GrpcServer می شویم.

سپس بر روی دکمه Add کلیک میکنیم.

و در مرحله بعدی گزینه gRPC را انتخاب میکنیم.

سپس بر روی دکمه Browse کلیک میکنیم و فایل ticket_api.proto را در FileDialog انتخاب میکنیم.
و بعد نوع کلاس هایی که قرار است توسط protoc ایجاد شوند را بر روی Server قرار میدهیم.

در مرحله بعدی بر روی دکمه Close کلیک میکنیم و سپس پروژه را Save میکنیم.

بعد از اینکه عمل Save را انجام دادید باید پروژه مورد نظر یا سولوشن را Build کنید.

در مراحل بالا دقیقا چه کاری کردیم ؟

در تصاویر بالا فایل ticket_api.proto را به صورت Server برای پروژه مان Compile کردیم و در این حین در پشت صحنه اتفاق هایی افتاد.

  1. مقدار Build Action فایل ticket_api.proto به Protobuf Compiler تغییر کرد.
  2. کلاس هایی از روی این فایل مخصوص به Grpc Server تولید شد.

حالا بعد از هر بار تغییر فایل ticket_api.proto و Build کردن پروژه، کلاس های تولید شده آپدیت و ویرایش میشوند.

در این مرحله میخواهیم فایل ها و کلاس هایی که توسط کامپایل شدن فایل ticket_api.proto ساخته شدند را بررسی کنیم.

در ابتدا بر روی پروژه کلیک چپ کنید و سپس گزینه Show All Files را Enable کنید.

سپس در Solution Explorer وارد مسیر obj/Debug/net5.0/Protos شوید.
مشاهده میکنید که 2 فایل در آن با فرمت cs. وجود دارد.

  1. فایل اول بدون پسوند Grpc که محل قرارگیری message ها است، message ها توسط protoc به کلاس های معمولی سی شارپ تبدیل میشوند.
  2. فایل دوم با پسوند Grpc که محل قرارگیری service است و متد CreateTicket در آن قرار گرفته است.

نام این 2 فایل از روی نام ticket_api.proto برداشته شده است، Protobuf Compiler این نام را از فرمت lower_snake_case به PascalCase تغییر داده و از آن برای این 2 فایل سی شارپی استفاده کرده است.

وقتی که یک فایل proto. مخصوص به Server کامپایل میشود، متدهای قرار گرفته در service را باید حتما override و سپس Implement کنیم.

این متد ها در یک کلاسی به نام ServiceName+Base قرار گرفته اند، دقیقا protoc نام Service را بر میدارد و یک پسوند Base به آن اضافه میکند.

در اینجا سرویسی که در فایل ticket_api.proto قرار داده بودیم نامش TicketService بود و الان باید توقع این را داشته باشیم که protoc یک کلاس به نام TicketServiceBase برای ما ایجاد کرده باشد.

پس همیشه در سمت سرور باید کلاسی که نامش از نام سرویس + Base تشکیل شده است را استفاده کنیم.

تا الان چه کارهایی انجام دادیم:

  1. ساخت پروژه ASP.NET Core
  2. نصب پکیج Grpc.AspNetCore
  3. ایجاد فایل ticket_api.proto
  4. تعریف message و service
  5. کامپایل فایل ticket_api.proto به صورت Server

کامپایل فایل ticket_api.proto به صورت Client و بررسی فایل های ایجاد شده توسط protoc در پروژه Client

ابتدا در همان Solution یک پروژه Console میسازیم.

به این پروژه پسوند GrpcClient را میدهیم.

سپس باید در پروژه کنسولی که به عنوان GrpcClient ساختیم، 3 عدد پکیج نصب کنیم.

Install-Package Grpc.Tools -Version 2.35.0

NuGet Gallery | Grpc.Net.ClientFactory 2.35.0

NuGet Gallery | Google.Protobuf 3.14.0

Install-Package Grpc.Tools -Version 2.35.0
Install-Package Google.Protobuf -Version 3.14.0
Install-Package Grpc.Net.ClientFactory -Version 2.35.0

در این مرحله باید فایل ticket_api.proto را هم در این پروژه به صورت Client کامپایل کنیم، دقیقا طبق همان مراحل بالا از بخش Connected Service این فایل را کامپایل میکنیم.

وقتی که مراحل بالا انجام شد، یک Link از فایل ticket_api.proto که در پروژه GrpcServer قرار گرفته بود به پروژه GrpcClient ایجاد میشود.

می توانید این Link را در فایل csproj پروژه GrpcClient مشاهده کنید.

راه دیگری هم وجود دارد که می توانیم در ابتدا یک کپی از فایل ticket_api.proto بگیریم و سپس یک Folder به نام Protos در پروژه GrpcClient بسازیم و سپس آن فایل را paste کنیم، و بعد از روی آن فایل مراحل Connected Service را انجام دهیم.


خب بعد از اینکه مراحل Connected Service تمام شد، و یک link از فایل ticket_api.proto در پروژه GrpcClient ایجاد شد، سپس باید فایل های ایجاد شده توسط protoc را بررسی کنیم.

اول Show All Files را برای پروژه GrpcClient را Enable کنید.

سپس همانطور که مشاهده میکنید، همان 2 فایلی که در پروژه Server ایجاد شدند، در این پروژه هم ایجاد شدند دقیقا با همان نام و فرمت.
فایل اول دارای Message ها است.

فایل دوم دارای service است.

فایل دوم یعنی TicketApiGrpc دارای یک کلاس است که نامش از نام سرویس + Client تشکیل میشود.

یعنی service ما که در فایل ticket_api.proto قرار گرفته بود نامش TicketService بود، الان باید توقع داشته باشیم که کلاسی به نام TicketServiceClient را برای ما ایجاد شده باشد.

وظیفه Client این است که یک نمونه از کلاس TicketServiceClient بسازد و متدهای موجود در آن را فراخوانی کند و از آن ها استفاده کند.

خب کل فرق کامپایل به صورت Client و به صورت Server رو میتونید در تصویر زیر مشاهده کنید.

بررسی Enumeration در Protobuf

در فایل proto. می توانیم Enum های دلخواه خودمان را تعریف کنیم و از آن ها به عنوان یک Data Type برای فیلد هایمان استفاده کنیم.

نحوه تعریف Enum

  1. نوشتن کلمه کلیدی enum
  2. تخصیص نام به enum صورت PascalCase
  3. تعریف constant ها به صورت UPPERCASE و تخصیص عدد به آن ها
نکته: Enum را می توانیم در 2 جا تعریف کنیم.
1. در داخل message
2. در بیرون message

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

در کد و تصویر زیر مشاهده می کنید که TicketStatus را بیرون از message ها تعریف کردیم و از آن در داخل TicketCreationResultDto استفاده کردیم.

در تصویر و کد زیر مشاهده میکنید که TicketStatus را در داخل message تعریف کردیم.

همانطور که در زبان #C می توانیم در هر enum چند فیلد با نام های مختلف و مقدار مساوی داشته باشیم، می توانیم همین رفتار را هم در enum های Protobuf داشته باشیم.

ولی باید این ویژگی را در enum های Protobuf به صورت صریح فعال کنیم.

سورس کد پروژه:

https://github.com/ArminShoeibi/DNZ.TicketingSystem
مقالات بیشتر در کانال دات نت زوم
https://t.me/DotNetZoom