‏gRPC چیه و چطور کار میکنه؟

قبلا از شروع این فصل بهتره قسمت‌های قبل درباره HTTP/2 و پروتوباف رو مطالعه کنید.

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

خب پس شما چندتا سرویس دارید که بهم وصل شدن و باهم صحبت میکنند، حالا برای پیاده کردن این معماری شما به چندتا چیز نیاز دارید:

  • یک استاندارد برای API
  • یک دیتا فرمت استاندارد
  • یک استاندارد برای خطاها
  • لود بالانسر
  • و خیلی چیزهای دیگه...

یکی از محبوب ترین استانداردها برای پیاده کردن API استاندارد REST هست که از جیسون استفاده میکنه. ولی ما در این مطلب از gRPC استفاده میکنیم.

موقع ساخت API باید به خیلی چیزها توجه کنید:

نوع دیتا مدل‌تون چی باشه؟ جیسون؟ XML؟ یا حتی باینری؟

باید به اندپوینت‌ها فکر کنید، برای مثال در رست یه اندپوینت میتونه این شکلی باشه:

GET /api/v1/users/123/posts/456
or
POST /api/v1/users/123/posts

یا چطور میشه از چندتا زبان برنامه نویسی استفاده کرد، به نظر پیچیده میرسه، ولی در gRPC همچین مشکلاتی نداریم.

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



‏ gRPC چیه؟

‏gRPC یک فریمورک رایگان و اپن‌سورس که توسط گوگل توسعه داده شده.

‏gRPC عضو CNCF هستش، مثل داکر، کوبرنتیز. پس پروژه مهمیه و توسعه داده میشه.

و از همه مهمتر مدرن و سریعه، ساخته شده با استاندارد HTTP/2 هست، از استریم دیتا و زبانهای متفاوت پشتیبانی میکنه و ساخت پلاگین‌های اعتبارسنجی کاربران، لودبالانسینگ، لاگ و مانیتورینگ بسیار سادس.


‏RPC چیه؟

‏RPC مخفف Remote Procedure Call هستش، در کدهای کلاینت شما اینطور به نظر میرسه که شما دارید یک فانکشن رو مستقیما توی سرور اجرا می‌کنید. ‏RPC یک کانسپت جدید نیست، برای مثال CORBA اینو از قبل داشت.


چطور شروع کنیم؟

برای شروع شما باید مسیج و سرویس‌هایی در فایل پروتوباف تعریف کنید. (اگر آشنایی ندارید بهتره این مطلب رو بخونید)

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

علاوه بر این شما میتونید توسط یک فایل پروتوباف برای ۱۲ زبان برنامه نویسی کدجنریت کنید و میلیونها ریکويست در ثانیه پردازش کنید (توجه داشته باشید فعلا نمیتونید برای php سرور gRPC راه اندازی کنید)

برای شروع میتونید یه سری به وبسایت رسمی gRPC.org بزنید، حتی نمونه‌هایی برای جاوا اسکریپت (فرانت‌اند) داره که میتونید استفاده کنید. (چون در فرانت‌اند و جاوا اسکریپت تخصصی ندارم واردش نمیشم)

فریمورک gRPC برای جاوا، گولنگ و سی به صورت اختصاصی با این زبان‌ها نوشته شدن، ولی برای بقیه مثل PHP, ruby, C#, python و... از لایبرری C استفاده میکنه، این خیلی مهمه چون اگر آپدیتی برای c بیاد شما میتونید انتظار داشته باشید زبان‌های زیرمجموعه خیلی سریع آپدیت بشن ولی برای گولنگ و جاوا ممکنه مدت بیشتری طول بکشه و برعکس.


انواع API در gRPC

در نوع یونری (unary) شبیه به مدل کلاسیک که میشناسیم کلاینت یه ریکوئست به سرور میزنه و سرور یک پاسخ ارسال میکنه ولی بسیار سریعتر.

درنوع سرور استریمینگ کلاینت یک درخواست ارسال میکنه و سرور با یک ارتباط TCP چندین پاسخ ارسال میکنه.

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

و در نوع آخر سرور و کلاینت می‌تونند به صورت غیر منظم چندیدن درخواست بهم ارسال و دریافت کنند.

در یک فایل پروتو انواع API به این صورت تعریف میشن
در یک فایل پروتو انواع API به این صورت تعریف میشن

در فایل بالا حواستون به کلمه استریم قبل از هر مسیج باشه، این تفاوت بین انواع API رو مشخص میکنه.

درحال حاضر گوگل در ثانیه ۱۰ میلیارد ریکوئست gRPC پردازش میکنه، پس اگر برای گوگل کار میکنه تو اسکیل ماهم کار میکنه و مشکل پرفورمنسی نداریم.


امنیت در gRPC

به صورت پیش‌فرض شما باید برای سرویس‌تون SSL ست کنید، ولی میتونید این قابلیت رو خاموش هم بکنید.

همچنین شما می‌تونید سیستم Auth برای سرویستون تعریف کنید که سعی میکنم تو این مطلب یکم راهنمایی کنم.


‏gRPC VS REST

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

‏gRPC از HTTP/2 استفاده میکنه که در سال ۲۰۱۵ معرفی شده و تاخیر کمی داره، رست از HTTP/1.1 استفاده میکنه که در سال ۱۹۹۷ معرفی شده و تاخیر زیادی در پاسخ دادن داره.

‏gRPC از استریم دیتا پشتیبانی میکنه ولی رست نه.

رست CRUD بیس هست، یعنی برای هر اندپوینت ۴ اکشن اضافه، حذف، دریافت و ویرایش دارید ولی gRPC فانکشن بیس هست، یعنی می‌تونید دقیقا مشخص کنید این اندپوینت خارج از این ۴ عمل چه کاری انجام میده.

‏gRPC برای هر زبانی کدجنریتور داره، درصورتی که رست کدجنریتوری مثل پروتوباف نداره.

‏gRPC خودش یک استاندارده و یک فریمورک داره، درصورتی که برای استفاده از استاندارد رست توی هر زبانی شما باید یک فریمورک خاص استفاده کنید که هرکدوم به شیوه متفاوتی پیاده میشن.


آماده سازی محیط برای توسعه gRPC با زبان گولنگ

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

go get -u google.golang.org/grpc

و برای نصب پروتوباف سری به صفحه گیت پروتوباف میزنیم و میگه برای نصبش باید از دستور پایین استفاده کنیم:

go get -u github.com/golang/protobuf/protoc-gen-go

حالا یک ریپازیتوری روی گیت میسازیم و ساختار پروژه رو تعریف میکنیم و قصدمون ساخت یک ماشین حساب سادس.


ماشین حساب ساده

سه پوشه با اسم‌های calculator_client ، calculator_server ، calculatorpb ایجاد میکنیم.

حالا فایلی با اسم calculator.proto داخل پوشه calculatorpb ایجاد میکنیم.

syntax = "proto3";

package calculator;
option go_package = "calculatorpb";

message SumRequest {
    int32 first_number = 1;
    int32 second_umber = 2;
}

message SumResponse {
    int32 sum_result = 1;
}

service CalculatorService {
    // Unary
    rpc Sum (SumRequest) returns (SumResponse) {};
}

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

protoc calculatorpb/calculator.proto --go_out=plugins=grpc:.

اگر نیازمندی‌های بالا را درست نصب کرده باشید و فایل رو در پوشه درست ایجاد کرده باشید و دستور بالا رو از پوشه روت پروژه اجرا کرده باشید باید فایلی به اسم calculator.pb.go در پوشه calculatorpb ایجاد شده باشه. در مرحله بعد این فایل‌رو به پروژه ایمپورت میکنیم و برای ساخت gRPC سرور و کلاینت ازش استفاده میکنیم.


ایجاد سرور یونری Unary gRPC

فایل main.go در پوشه calculator_server همراه با محتوای زیر ایجاد میکنید:

package main

import (
   "context"
   "fmt"
   "github.com/ErFUN-KH/simple-grpc-project/calculatorpb"
   "google.golang.org/grpc"
   "log"
   "net"
)

type server struct{}

func main() {
   fmt.Println("Server is running...")

   // Make a listener
   lis, err := net.Listen("tcp", "0.0.0.0:50051")
   if err != nil {
      log.Fatalf("Failed to listen: %v", err)
   }

   // Make a gRPC server
   grpcServer := grpc.NewServer()
   calculatorpb.RegisterCalculatorServiceServer(grpcServer, &server{})

   // Run the gRPC server
   if err := grpcServer.Serve(lis); err != nil {
      log.Fatalf("Failed to serve: %v", err)
   }
}

func (*server) Sum(ctx context.Context, req *calculatorpb.SumRequest) (*calculatorpb.SumResponse, error) {
   fmt.Printf("Received Sum RPC: %v", req)

   firstNumber := req.GetFirstNumber()
   secondNumber := req.GetSecondUmber()

   sum := firstNumber + secondNumber

   res := &calculatorpb.SumResponse{
      SumResult: sum,
   }

   return res, nil
}

فکر میکنم تا خط ۱۸ چالشی نداره پس از اینجا شروع میکنم فقط قبلش باید بگم اون server که در خط ۱۲ تعریف کردم تمام اندپوینت‌هارو در خودش نگه میداره و یه جورایی میشه گفت مشابه فایل route در رست‌فوله، در خط ۱۸ یک پورت TCP باز میکنیم تا سرویس gRPC بتونه ازش استفاده کنه.

در خط ۲۴ یک سرور gRPC تعریف کردیم، و در خط ۲۵ سرویس ماشین‌حساب‌رو روش کانفیگ کردیم (که از فایل پروتویی که جنریت کردیم ایمپورت شده) و در خط ۲۸ سرور gRPC رو اجرا کردیم. یک سرور gRPC به صورت کلی همچین چیزیه.

و در خط ۳۳ یک اندپوینت برای جمع اعداد ایجاد کردیم، کدها به قدری سادس که اگر گولنگ بلد باشید هیچ توضیحی نیاز نداره. در خط ۳۶ و ۳۷ اعداد اول و دوم رو دریافت کردیم، بعد از اون باهم جمع بستیم و در خط ۴۱ یک ریسپانس از جنس جمع اعداد ایجاد کردیم و به کاربر فرستادیم.

حالا اگر پروژه رو بیلد بگیرید میبینید که سرور به درستی اجرا میشه.

تا اینجای کار میتونید پروژه رو در گیت ببینید.


ایجاد کلاینت یونری Unary gRPC

فایل main.go در پوشه calculator_client همراه با محتوای زیر ایجاد میکنید:

package main

import (
   "context"
   "fmt"
   "github.com/ErFUN-KH/simple-grpc-project/calculatorpb"
   "google.golang.org/grpc"
   "log"
)

func main() {
   fmt.Println("Client is running...")

   cc, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
   if err != nil {
      log.Fatalf("could not connect to server: %v", err)
   }
   defer cc.Close()

   c := calculatorpb.NewCalculatorServiceClient(cc)

   doSum(c)
}

func doSum(c calculatorpb.CalculatorServiceClient) {
   fmt.Println("Starting to do a sum RPC")

   req := &calculatorpb.SumRequest{
      FirstNumber: 40,
      SecondUmber: 2,
   }

   res, err := c.Sum(context.Background(), req)
   if err != nil {
      log.Fatalf("Error while calling sum RPC: %v", err)
   }

   log.Printf("Response from server: %v", res.SumResult)
}

برای کلاینت هم کدها ساده و کمه، در خط ۱۴ یک تماس با سرور gRPC ایجاد کردیم توجه داشته باشید چون فعلا نمیخوایم کلید ssl ست تنظیم کنیم از متد grpc.WithInsecure استفاده کردیم ولی در آینده توضیح میدم چطور ست کنید.

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

و درمحله بعد یک فانکش صدا میکنیم که جمع دو عدد رو از سرور بگیره، در خط ۲۸ دو عدد رو ست کردیم، در خط ۳۳ اندپوینت رو کال کردیم و جواب رو گرفتیم و در خط پایینی پرینت گرفتم.

حالا اول سرور رو بیلد و اجرا کنید و بعد کلاینت رو، می‌بینید اعداد باهم جمع میخورن و در ترمینال نمایش داده میشه. تبریک میگم شما اول سرویس‌تون رو نوشتید :)

از اینجا می‌تونید کل پروژه‌رو روی گیت ببینید.


سرور استریمینگ gRPC

سرور استریمینگ gRPC یک نوع جدید از API هست که با وجود HTTP/2 امکان پذیر شده.

در این نوع از API کلاینت یک مسیج به سرور ارسال میکنه و بیشتر از یک ریسپانس از سرور دریافت میکنه (در تعداد ریسپانس‌ها محدودیتی وجود نداره، میتونه نامحدود باشه).

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

میخوایم یک اندپوینت بنویسیم که فاکتوریل اعداد رو محاسبه کنه، اول از همه فایل پروتو باز میکنیم و به این صورت ادیتش میکنیم:

syntax = "proto3";

package calculator;
option go_package = "calculatorpb";

message SumRequest {
    int32 first_number = 1;
    int32 second_umber = 2;
}

message SumResponse {
    int32 sum_result = 1;
}

message PrimeNumberDecompositionRequest {
    int64 number = 1;
}

message PrimeNumberDecompositionResponse {
    int64 prime_factor = 1;
}

service CalculatorService {
    // Unary
    rpc Sum (SumRequest) returns (SumResponse) {};

    // Server Streaming
    rpc PrimeNumberDecomposition (PrimeNumberDecompositionRequest) returns (stream PrimeNumberDecompositionResponse) {};
}

در خط‌های ۱۵ و ۱۹ دو مسیج جدید و در خط ۲۸ یک سرویس جدید از نوع سرور استریمینگ تعریف کردیم (به کلمه stream در خط ۲۸ توجه کنید) و بعد از اون دوباره فایل پروتو رو با دستور پایین جنریت کردیم.

protoc calculatorpb/calculator.proto --go_out=plugins=grpc:.

حالا فایل calculator_server/main.go باز میکنیم و فانکشن زیر رو به آخرین خط اضافه می‌کنیم:

func (*server) PrimeNumberDecomposition(req *calculatorpb.PrimeNumberDecompositionRequest, stream calculatorpb.CalculatorService_PrimeNumberDecompositionServer) error {
   fmt.Printf("Received PrimeNumberDecomposition RPC: %v\n", req)

   number := req.Number
   divisor := int64(2)

   for number > 1 {
      if number % divisor == 0 {
         err := stream.Send(&calculatorpb.PrimeNumberDecompositionResponse{
            PrimeFactor: divisor,
         })
         if err != nil {
            log.Fatalf("Failed to send response: %v\n", err)
         }

         number = number / divisor
      } else {
         divisor++
         fmt.Printf("Divisor has increased to %v", divisor)
      }
   }

   return nil
}

در خط اول می‌بینیم متدهای این فانکشن با فانکشن قبلی فرق داره، بهتره فایل جنریت شده توسط پروتوباف رو بازکنید و متدهارو چک کنید (یک فانکشن دقیقا با همین اسم و متدها میبینید، می‌تونید کپی پیست کنید) و بعد فانکشن رو تعریف کنید.

در خط‌های بعدی عدد ورودی توسط کلاینت رو دریافت کردیم و فاکتوریل‌شو محاسبه کردیم، در خط ۹ اعداد محاسبه شده رو به کلاینت ارسال کردیم (اگر متدهای استریم رو چک کنید میبینید چندتایی هست که اینجا از send استفاده کردیم ولی با توجه به نوع API متدهای متفاوتی داره).

کارمون با سرور تمومه، فایل calculator_client/main.go باز میکنیم و به این صورت تغییرش میدیم:

package main

import (
   "context"
   "fmt"
   "github.com/ErFUN-KH/simple-grpc-project/calculatorpb"
   "google.golang.org/grpc"
   "io"
   "log"
)

func main() {
   fmt.Println("Client is running...")

   cc, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
   if err != nil {
      log.Fatalf("could not connect to server: %v", err)
   }
   defer cc.Close()

   c := calculatorpb.NewCalculatorServiceClient(cc)

   //doSum(c)

   doServerStreaming(c)
}

func doSum(c calculatorpb.CalculatorServiceClient) {
   fmt.Println("Starting to do a sum Unary RPC")

   req := &calculatorpb.SumRequest{
      FirstNumber: 40,
      SecondUmber: 2,
   }

   res, err := c.Sum(context.Background(), req)
   if err != nil {
      log.Fatalf("Error while calling sum RPC: %v", err)
   }

   log.Printf("Response from server: %v", res.SumResult)
}

func doServerStreaming(c calculatorpb.CalculatorServiceClient) {
   fmt.Println("Starting to do a PrimeDecomposition server streaming RPC")

   req := &calculatorpb.PrimeNumberDecompositionRequest{
      Number: 12,
   }

   stream, err := c.PrimeNumberDecomposition(context.Background(), req)
   if err != nil {
      log.Fatalf("Error while calling PrimeDecomposition RPC: %v", err)
   }

   for {
      res, err := stream.Recv()
      if err == io.EOF {
         break
      }
      if err != nil {
         log.Printf("Error while streaming PrimeDecomposition RPC: %v", err)
      }
      fmt.Println(res.PrimeFactor)
   }
}

در خط ۲۳ اندپوینت قبلی رو غیرفعال کردیم و در خط بعدی اندپوینت جدیدی رو صدا زدیم، در خط ۴۴ مثل اندپوینت قبلی یک ریکوئست پروتوباف درست کردیم بعد از اون اندپوینت محاسبه فاکتوریل رو از سرور کال کردیم، ولی برای گرفتن پاسخ از سرور یک حلقه تعریف کردیم، در خط ۵۷ دیتای استریم رو دریافت کردیم (اینجا از متد Recv استفاده کردیم که اگه یادتون باشه توی سرور از متد send استفاده کرده بودیم)، درخط بعد شرط گذاشتیم اگر استریم به پایان رسید از حلقه خارج بشیم، بعد از اون یک شرط گذاشتیم اگر به هر دلیلی استریم خطا داشت، خطا رو نمایش بده و بعد از اون اگر مشکلی وجود نداشت جواب دریافتی از سرور رو چاپ کردیم.

کدهای این بخش در گیت‌هاب.


کلاینت استریمینگ gRPC

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

موارد استفاده هم میشه برای آپلود فایل و... استفاده کرد.

پس (برای محاسبه کردن میانگین یک مجموعه اعداد) فایل پروتو به این صورت تغییر میدیم:

syntax = "proto3";

package calculator;
option go_package = "calculatorpb";

message SumRequest {
    int32 first_number = 1;
    int32 second_umber = 2;
}

message SumResponse {
    int32 sum_result = 1;
}

message PrimeNumberDecompositionRequest {
    int64 number = 1;
}

message PrimeNumberDecompositionResponse {
    int64 prime_factor = 1;
}

message ComputeAverageRequest {
    int32 number = 1;
}

message ComputeAverageResponse {
    double average = 1;
}

service CalculatorService {
    // Unary
    rpc Sum (SumRequest) returns (SumResponse) {};

    // Server Streaming
    rpc PrimeNumberDecomposition (PrimeNumberDecompositionRequest) returns (stream PrimeNumberDecompositionResponse) {};

    // Client Streaming
    rpc ComputeAverage (stream ComputeAverageRequest) returns (ComputeAverageResponse) {};
}

در خط‌های ۲۳ و ۲۷ دو مسیج جدید تعریف کردیم و در خط ۳۹ یک اندپوینت کلاینت استریمینگ تعریف کردیم.

حالا با دستور زیر دوباره فایل پروتو رو برای استفاده در کد، جنریت میکنیم:

protoc calculatorpb/calculator.proto --go_out=plugins=grpc:.

حالا فایل calculator_server/main.go باز میکنیم و فانکشن زیر رو بهش اضافه میکنیم:

func (*server) ComputeAverage(stream calculatorpb.CalculatorService_ComputeAverageServer) error {
   fmt.Printf("Received ComputeAverage RPC\n")

   sum := float64(0)
   count := float64(0)

   for {
      req, err := stream.Recv()

      if err == io.EOF {
         return stream.SendAndClose(&calculatorpb.ComputeAverageResponse{
            Average: sum / count,
         })
      }
      if err != nil {
         log.Fatalf("Error while reading client stream: %v", err)
      }

      sum += float64(req.GetNumber())
      count++
   }
}

اگر فایل calculator/calculator.proto باز کنید و کلمه Server interface سرچ کنید میتونید فانکشن ComputeAverage همراه با متدهاشو ببینید و استفاده کنید، عین کاری که من میکنم.

در خط ۴ و ۵ دو متغییر جمع تمام اعداد و تعدادشون ایجاد کردیم.

و بعد یک حلقه ایجاد کردیم و دیتاهای ارسالی از سمت کاربر رو دریافت کردیم، بعد از اون یک شرط گذاشتیم اگر دریتاهای ارسالی تموم شده باشه جواب رو به کاربر ارسال کنه (از متد SendAndClose استفاده کردیم) وگرنه چک میکنه اگر موقع دریافت دیتا مشکل بخوره خطا رو نمایش بده، بعد اون اگر مشکلی نباشه عدد دریافتی از کاربر رو با اعداد دریافتی قبلی جمع میزنیم و یکی به تعداد کل اعداد اضافه میکنیم تا بعد بتونیم میانگین بگیریم.


حالا در بخش کلاینت فانکش زیر رو اضافه و در تابع اصلی صداش میکنیم:

func doClientStreaming(c calculatorpb.CalculatorServiceClient) {
   fmt.Println("Starting to do a ComputeAverage client streaming RPC")

   stream, err := c.ComputeAverage(context.Background())
   if err != nil {
      log.Fatalf("Error while calling stream RPC: %v", err)
   }

   numbers := []int32{2, 5, 7, 9, 12, 57}

   for _, number := range numbers {
      fmt.Printf("Sending number: %v\n", number)
      err := stream.Send(&calculatorpb.ComputeAverageRequest{
         Number:number,
      })
      if err != nil {
         log.Fatalf("Error while sending stream: %v", err)
      }
   }

   res, err := stream.CloseAndRecv()
   if err != nil {
      log.Fatalf("Error while receiving response: %v", err)
   }

   fmt.Printf("The average is: %v\n", res.GetAverage())
}

در خط ۴ اندپوینت میانگین گیری در سرور رو صدا زدیم، در خط ۱۱ یک حلقه ایجاد کردیم تا دیتاهارو به سرور ارسال کنه، در خط ۲۱ استریم رو قطع و پیام سرور رو دریافت میکنیم (از متد CloseAndRecv استفاده کردیم) و اگر خطایی اتفاق نیفته میانگین اعداد رو چاپ میکنیم.

کدهای این بخش رو در گیت‌هاب ببینید.


استریمینگ دوطرفه

در ارتباط دوطرفه یا Bi Directional کلاینت میتونه هر تعداد که میخواد ریکوئست به سرور ارسال کنه و سرور هم میتونه به هر کدوم از ریکويست‌هایی که میخواد پاسخ بده.

از موارد استفاده این نوع از API میشه برای سیستم چت و... نامبرد.

مثل همیشه فایل پروتو آپدیت میکنیم:

syntax = "proto3";

package calculator;
option go_package = "calculatorpb";

message SumRequest {
    int32 first_number = 1;
    int32 second_umber = 2;
}

message SumResponse {
    int32 sum_result = 1;
}

message PrimeNumberDecompositionRequest {
    int64 number = 1;
}

message PrimeNumberDecompositionResponse {
    int64 prime_factor = 1;
}

message ComputeAverageRequest {
    int32 number = 1;
}

message ComputeAverageResponse {
    double average = 1;
}

message FindMaximumRequest {
    int32 number = 1;
}

message FindMaximumResponse {
    int32 maximum = 1;
}

service CalculatorService {
    // Unary
    rpc Sum (SumRequest) returns (SumResponse) {};

    // Server Streaming
    rpc PrimeNumberDecomposition (PrimeNumberDecompositionRequest) returns (stream PrimeNumberDecompositionResponse) {};

    // Client Streaming
    rpc ComputeAverage (stream ComputeAverageRequest) returns (ComputeAverageResponse) {};

    // BiDi Streaming
    rpc FindMaximum(stream FindMaximumRequest) returns (stream FindMaximumResponse) {};
}

و بعد کدهارو جنریت میکنیم:

protoc calculatorpb/calculator.proto --go_out=plugins=grpc:.

حالا فایل calculator_server/main.go باز میکنیم و فانکشن زیر رو بهش اضافه میکنیم:

func (*server) FindMaximum(stream calculatorpb.CalculatorService_FindMaximumServer) error {
   fmt.Printf("Received FindMaximum RPC\n")

   maximum := int32(0)

   for {
      req, err := stream.Recv()
      if err == io.EOF {
         return nil
      }
      if err != nil {
         log.Fatalf("Error while reading client stream: %v", err)
         return err
      }

      if req.Number > maximum {
         maximum = req.Number
         err := stream.Send(&calculatorpb.FindMaximumResponse{
            Maximum: maximum,
         })
         if err != nil {
            log.Fatalf("Error while sending client stream: %v", err)
            return err
         }
      }
   }
}

اول از همه یک متغییر ایجاد کردیم تا مقدار بزرگترین عدد توش ذخیره کنیم.

بعد اون یک حلقه ایجاد کردیم تا استریم دیتارو دریافت و پردازش کنیم، در خط اول این حلقه استریم رو دریافت کردیم، بعد اون به پایان رسیدن استریم رو چک کردیم که اگر استریم تموم شده باشه از حلقه خارج میشه، بعد اون خطاهایی که ممکنه به علت قطع شبکه یا... پیش بیاد رو چک کردیم، بعد از اون اگر همه چیز درست باشه چک میکنیم اگر عدد دریافتی از ماکسیمومی که ما داشتیم بزرگتر بود یک ریسپانس به کلاینت ارسل میکنیم و اگر کوچیکتر بود هیچ اکشنی نداریم. (در اینجا اگر متدهای استریم رو چک کنید میبینید که هم سند داریم و هم رسیو داریم، درصورتی که در API های قبلی فقط یکی رو داشتیم)

حالا برای کلاینت از گو روتین استفاده میکنیم که حتما باید بلد باشید وگرنه درک این قسمت براتون سخت میشه (concurrency)، کد زیر رو به کلاینت اضافه میکنیم:

func doBiDiStreaming(c calculatorpb.CalculatorServiceClient) {
   fmt.Println("Starting to do a FindMaximum BiDi streaming RPC")

   stream, err := c.FindMaximum(context.Background())
   if err != nil {
      log.Fatalf("Error while calling stream RPC: %v", err)
   }

   waitingForChannel := make(chan struct{})

   // send go routine
   go func() {
      numbers := []int32{2, 8, 1, 5, 37, 28, 42}

      for _, number := range numbers {
         err := stream.Send(&calculatorpb.FindMaximumRequest{
            Number: number,
         })
         if err != nil {
            log.Fatalf("Error while sending stream: %v", err)
         }
         time.Sleep(1000 * time.Millisecond)
      }

      err := stream.CloseSend()
      if err != nil {
         log.Fatalf("Error while closing stream: %v", err)
      }
   }()

   // receive go routine
   go func() {
      for {
         res, err := stream.Recv()
         if err == io.EOF {
            break
         }
         if err != nil {
            log.Fatalf("Error while receving stream: %v", err)
            break
         }

         fmt.Printf("New maximun is: %v\n", res.Maximum)
      }
      close(waitingForChannel)
   }()

   <-waitingForChannel
}

اگر خیلی ساده بخوام توضیح بدم باید بگم دوتا تابع به صورت همزمان برای دریافت و ارسال دیتا ایجاد کردیم، داخل هرکدوم یک حلقه نوشتیم، برای قسمت ارسال دیتا توسط گو روتین اول یک آرایه از اعداد درست کردیم، توسط یک حلقه تمام اعداد آرایه رو به سرور ارسال کردیم و گفتیم بعد هر ارسال یک ثانیه صبر کن (تا تاخیر داشته باشه و استریم رو بهتر درک کنیم) و بعد از تمام شدن حلقه گفتیم استریم ارسال دیتارو ببند.

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

کدهای این بخش رو در گیت‌هاب ببینید.



خطاها

وقتی برای API مشکلی پیش بیاد سرور ما باید خطا برگردونه، در رست از کدهای خطای HTTP استفاده میکنیم ۲۰۰ برای موفقت آمیز بودن، ۳۰۰ برای ری‌دایرکت کردن، ۴۰۰ برای خطاهای داخلی و ۵۰۰ برای خطاهای سرور.

برای دیدن خطاهای gRPC میتونید از داکیومنت رسمی کمک بگیرید، همینطور که میبینید خطاها در gRPC دارای یک کد و یک مسیج هستند، تشخیص نوع خطا از روی کد هستش و مسیج بیشتر برای ساده کردن دیباگه.

برای نمونه میخوایم اندپوینتی بنویسم که ریشه دوم اعداد رو بهمون برگردونه، بازم مثل همیشه اول فایل پروتو آپدیت میکنیم:

syntax = "proto3";

package calculator;
option go_package = "calculatorpb";

message SumRequest {
    int32 first_number = 1;
    int32 second_umber = 2;
}

message SumResponse {
    int32 sum_result = 1;
}

message PrimeNumberDecompositionRequest {
    int64 number = 1;
}

message PrimeNumberDecompositionResponse {
    int64 prime_factor = 1;
}

message ComputeAverageRequest {
    int32 number = 1;
}

message ComputeAverageResponse {
    double average = 1;
}

message FindMaximumRequest {
    int32 number = 1;
}

message FindMaximumResponse {
    int32 maximum = 1;
}

message SquareRootRequest {
    int32 number = 1;
}

message SquareRootResponse {
    double number_root = 1;
}

service CalculatorService {
    // Unary
    rpc Sum (SumRequest) returns (SumResponse) {};

    // Server Streaming
    rpc PrimeNumberDecomposition (PrimeNumberDecompositionRequest) returns (stream PrimeNumberDecompositionResponse) {};

    // Client Streaming
    rpc ComputeAverage (stream ComputeAverageRequest) returns (ComputeAverageResponse) {};

    // BiDi Streaming
    rpc FindMaximum (stream FindMaximumRequest) returns (stream FindMaximumResponse) {};

    // Error Handing
    rpc SquareRoot (SquareRootRequest) returns (SquareRootResponse) {};
}

و در مرحله بعد فایلشو جنریت میکنیم:

protoc calculatorpb/calculator.proto --go_out=plugins=grpc:.

حالا برای سرور این اندپوینت رو اینجاد میکنیم:

func (*server) SquareRoot(ctx context.Context, req *calculatorpb.SquareRootRequest) (*calculatorpb.SquareRootResponse, error) {
   fmt.Println("Received SquareRoot RPC")

   number := req.Number

   if number < 0 {
      return nil, status.Errorf(
         codes.InvalidArgument,
         fmt.Sprintf("Received a negative number: %v", number),
         )
   }

   return &calculatorpb.SquareRootResponse{
      NumberRoot: math.Sqrt(float64(number)),
   }, nil
}

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

حالا برای کلاینت:

func doSquareRoot(c calculatorpb.CalculatorServiceClient, number int32) {
   fmt.Println("Starting to do a SquareRoot Unary RPC")

   res, err := c.SquareRoot(context.Background(), &calculatorpb.SquareRootRequest{Number: number})
   if err != nil {
      resError, ok := status.FromError(err)
      if ok {
         // Actual error from gRPC (user error)
         fmt.Printf("Error message from server: %v\n", resError.Message())
         fmt.Println(resError.Code())
         if resError.Code() == codes.InvalidArgument {
            log.Fatalln("We probably sent a negative number")
         }
      } else {
         log.Fatalf("Big error calling SquareRoot: %v", err)
      }
   }
   fmt.Printf("Result of square root of %v: %v\n\n", number, res.NumberRoot)
}

در کلاینت هم همه چی شبیه قبله با این تفاوت که در متدهای دریافتی یک عدد هم دریافت کردیم تا بتونیم دو دفعه در تابع main صداش کنیم، یکبار همراه با خطا و یک دفعه بدون خطا.

کدها مثل صدا کردن یونری در قبله ولی بعد از چک کردن خطا یک شرط دیگم گذاشتیم، اینجا با کمک status.FromError چک میکنیم آیا خطای دریافتی از استاندارد gRPC هست یا نه، اگر بود پیام خطا و کد رو چاپ میکنیم، اگر نبود خطا رو چاپ میکنیم.

کدهای این بخش رو از گیت‌هاب بخونید.


ددلاین - Dead line

یک ددلاین مشخص میکنه یک RPC حداکثر ممکنه چقدر طول بکشه تا جواب کلاینت رو بده و اگر در طول این مدت پاسخی ارسال نشه خطای DEADLINE_EXCEEDED برگردونه.

داکیومنت رسمی gRPC شدیدا توصیه میکنه برای هر تمام کلاینت‌های RPC ددلاین مشخص کنید.

احتمالا این سوال براتون پیش میاد که ددلاین‌هارو باید چقدر ست کنیم؟ این به شما بستگی داره، فکر میکنید چقدر طول میکشه تا API شما پردازش رو انجام بده؟ برای یک API معمولی ممکنه ۱۰۰ میلی ثانیه کافی باشه، یا اگر خیلی کند باشه ۱ ثانیه باید کافی باشه، ولی به شما بستگی داره که فکر میکنید چقدر ممکنه طول بکشه، هیچ قانونی براش نداریم میتونید حتی ۵ دقیقه هم ست کنید.

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

اینجا یک پست درمورد ددلاین با مثال از وبلاگ رسمی gRPC هست که پیشنهاد میکنم ببینید.

حالا میخوایم جمع دو عدد رو با ددلاین انجام بدیم، مثل همیشه اول فایل پروتو آپدیت میکنیم:

syntax = "proto3";

package calculator;
option go_package = "calculatorpb";

message SumRequest {
    int32 first_number = 1;
    int32 second_umber = 2;
}

message SumResponse {
    int32 sum_result = 1;
}

message PrimeNumberDecompositionRequest {
    int64 number = 1;
}

message PrimeNumberDecompositionResponse {
    int64 prime_factor = 1;
}

message ComputeAverageRequest {
    int32 number = 1;
}

message ComputeAverageResponse {
    double average = 1;
}

message FindMaximumRequest {
    int32 number = 1;
}

message FindMaximumResponse {
    int32 maximum = 1;
}

message SquareRootRequest {
    int32 number = 1;
}

message SquareRootResponse {
    double number_root = 1;
}

message SumWithDeadLineRequest {
    int32 first_number = 1;
    int32 second_umber = 2;
}

message SumWithDeadLineResponse {
    int32 sum_result = 1;
}

service CalculatorService {
    // Unary
    rpc Sum (SumRequest) returns (SumResponse) {};

    // Server Streaming
    rpc PrimeNumberDecomposition (PrimeNumberDecompositionRequest) returns (stream PrimeNumberDecompositionResponse) {};

    // Client Streaming
    rpc ComputeAverage (stream ComputeAverageRequest) returns (ComputeAverageResponse) {};

    // BiDi Streaming
    rpc FindMaximum (stream FindMaximumRequest) returns (stream FindMaximumResponse) {};

    // Error Handing
    rpc SquareRoot (SquareRootRequest) returns (SquareRootResponse) {};

    // Dead Line
    rpc SumWithDeadLine (SumWithDeadLineRequest) returns (SumWithDeadLineResponse) {};
}

بعد از اون کدهارو جنریت میکنیم:

protoc calculatorpb/calculator.proto --go_out=plugins=grpc:.

حالا برای سرور به اینصورت عمل میکنیم:

func (*server) SumWithDeadLine(ctx context.Context, req *calculatorpb.SumWithDeadLineRequest) (*calculatorpb.SumWithDeadLineResponse, error) {
   fmt.Printf("Received SumWithDeadLine RPC: %v\n", req)

   for i := 0; i < 3; i++ {

      if ctx.Err() == context.Canceled {
         fmt.Println("The client canceled the request")
         return nil, status.Error(codes.Canceled, "The client canceled the request")
      }

      time.Sleep(1 * time.Second)
   }

   firstNumber := req.GetFirstNumber()
   secondNumber := req.GetSecondUmber()

   sum := firstNumber + secondNumber

   res := &calculatorpb.SumWithDeadLineResponse{
      SumResult: sum,
   }

   return res, nil
}

سرور مشابه همون جمع دو عدده فقط یک حلقه بهش اضافه کردیم، حلقه ۳ بار اجرا میشه و هربار چک میکنه ددلاین به پایان رسیده یا نه، اگر رسیده باشه خطارو چاپ میکنه اگر نرسیده باشه ۱ ثانیه تاخیر ایجاد میکنه.

و برای کلاینت:

func doSumWithDeadLine(c calculatorpb.CalculatorServiceClient, time time.Duration) {
   fmt.Println("Starting to do a SumWithDeadLine RPC")

   req := &calculatorpb.SumWithDeadLineRequest{
      FirstNumber: 40,
      SecondUmber: 2,
   }

   ctx, cancel := context.WithTimeout(context.Background(), time)
   defer cancel()

   res, err := c.SumWithDeadLine(ctx, req)
   if err != nil {
      resError, ok := status.FromError(err)
      if ok {
         if resError.Code() == codes.DeadlineExceeded {
            log.Fatalln("Timeout was hit! Deadline was exceeded")
         } else {
            log.Fatalf("Unexpected error: %v", err)
         }
      } else {
         log.Fatalf("Error while calling sum RPC: %v", err)
      }
   }

   log.Printf("Response from server: %v\n\n", res.SumResult)
}

تمام کد مشابه جمع بستن دو عدده، با این تفاوت که یک context.WithTimeout تعریف کردیم و تایم اوت رو توش ست کردیم.

بعد از اون در قسمت خطاها چک کردیم اگر ددلاین به پایان رسیده خطا رو چاپ کنه. به همین سادگی. از اونجایی که سرور ۳ ثانیه تاخیر داره ما دو دفعه این فانکشن رو صدا میزنیم، یکبار با ددلاین ۵ ثانیه یکبار با ددلاین ۳ قانیه، مورد اول باید پاس بشه و مورد دوم باید خطای ددلاین بده.

کدهای این بخش رو در گیت‌هاب ببینید.


‏SSL

در پروداکشن بهتره سرویس‌های gRPC شما با SSL بهم متصل بشن، این پیش فرض HTTP/2 هستش و gRPC هم بهش احترام میذاره.

‏ssl بهتون کمک میکنه اینکریپشن e2e داشته باشید و هیچکس نتونه دیتای ارسالی سرویس‌ها بهم رو شنود کنه یا تغییر بده.

دو نوع اتصال امن برای SSL داریم، یکی اینکریپشن و یکی آتنتیکیشن، اینجا فقط اینکریپشن رو توضیح میدم، خودتون میتونید آتنتیکیشن از اینجا یا داکیومنت رسمی یادبگیرین (به بقیه داکیومنتام نگاه کنید، احتمالا براتون جالبه).

برای ایجاد کلیدهای SSL باید با openssl آشنا باشید، البته دستورات مربوط به کلیدهارو داخل makefile گذاشتم میتونید با دستور make ssl کلیدهارو بدون دردسر جنریت کنید.

اگر با Makefile آشنا نیستید باید بگم می‌تونید با دستور apt install make روی لینوکس یا با دستور brew install make روی مک نصبش کنید، حالا برای ساخت کلید فقط کافیه دستور make ssl بزنید، خودش به ترتیب دستورات لازم رو اجرا میکنه. (البته حسواستون باشه در روت پروژه باشید)

حالا فایل سرور رو باز میکنیم و تابع main رو به این صورت تغییر میدیم:

func main() {
   fmt.Println("Server is running...")

   // Make a listener
   lis, err := net.Listen("tcp", "0.0.0.0:50051")
   if err != nil {
      log.Fatalf("Failed to listen: %v", err)
   }

   // SSL config
   certFile := "../ssl/server.crt"
   keyFile := "../ssl/server.pem"
   creds, sslErr := credentials.NewServerTLSFromFile(certFile, keyFile)
   if sslErr != nil {
      log.Fatalf("Faild loading certificates: %v", sslErr)
   }
   opts := grpc.Creds(creds)

   // Make a gRPC server
   grpcServer := grpc.NewServer(opts)
   calculatorpb.RegisterCalculatorServiceServer(grpcServer, &server{})

   // Run the gRPC server
   if err := grpcServer.Serve(lis); err != nil {
      log.Fatalf("Failed to serve: %v", err)
   }
}

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

حالا تابع main کلاینت رو به این صورت تغییر میدیم:

func main() {
   fmt.Println("Client is running...")

   // SSL config
   certFile := "../ssl/ca.crt"
   creds, sslErr := credentials.NewClientTLSFromFile(certFile, "api.example.com")
   if sslErr != nil {
      log.Fatalf("Error while loading CA trust certifiate: %v", sslErr)
   }
   opts := grpc.WithTransportCredentials(creds)

   cc, err := grpc.Dial("localhost:50051", opts)
   if err != nil {
      log.Fatalf("could not connect to server: %v", err)
   }
   defer cc.Close()

   c := calculatorpb.NewCalculatorServiceClient(cc)

   doSum(c)
}

میبینید اینجام به همون صورت ولی با کنفیگ کلاینت کلید SSL رو ست کردیم.

تنها نکته اینه توی makefile یک متغییر به اسم داشتیم که SERVER_CN بهش مقدار api.example.com دادیم و اینجا توی کلاینت هم باید همون مقدار رو برای کانفیگ ssl ست کنیم.

کدهای این بخش رو از گیت‌هاب ببینید.


‏gRPC Reflection

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

ولی برای محیط دولوپمنت ممکنه فقط بخواید بدونید سرور چه API هایی داره یا میخواید خیلی سریع یک API رو صدا کنید و نتیجه رو ببینید بدون اینکه مجبور به کد زدن باشید، این قابلیت توسط gRPC رفلکشن در دسترستونه، با این قابلیت این امکان رو دارید بدون داشتن فایل پروتو ببینید سرور چه سرویس‌هایی داره و یکی یکی شون رو امتحان کنید.

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

func main() {
   fmt.Println("Server is running...")

   // Make a listener
   lis, err := net.Listen("tcp", "0.0.0.0:50051")
   if err != nil {
      log.Fatalf("Failed to listen: %v", err)
   }

   // SSL config
   tls := false
   opts := []grpc.ServerOption{}
   if tls {
      certFile := "../ssl/server.crt"
      keyFile := "../ssl/server.pem"
      creds, sslErr := credentials.NewServerTLSFromFile(certFile, keyFile)
      if sslErr != nil {
         log.Fatalf("Faild loading certificates: %v", sslErr)
      }
      opts = append(opts, grpc.Creds(creds))
   }

   // Make a gRPC server
   grpcServer := grpc.NewServer(opts...)
   calculatorpb.RegisterCalculatorServiceServer(grpcServer, &server{})

   // Register reflection service on gRPC server.
   reflection.Register(grpcServer)

   // Run the gRPC server
   if err := grpcServer.Serve(lis); err != nil {
      log.Fatalf("Failed to serve: %v", err)
   }
}

همینطور که میبینید فقط با اضافه کردن یک خط (reflection.Register(grpcServer رفلکشن به سرور اضافه شد. ولی نکته اینه نمیشه با حالت ssl به رفلکشن وصل شد پس حالت خاموش کردن ssl رو اضافه کردیم.

خب مرحله بعدی نصب evans هستش، سری به پیجش بزنید و برای سیستمتون نصبش کنید.

حالا بعد از اجرای سرور می‌تونید با دستور evans -p 50051 -r به سرور وصل بشید، با دستور show package می‌تونید می‌تونید لیست پکیج‌هارو ببینید، با دستور package calculator وارد پکیج ماشین حساب میشیم، با دستور show service لیست سرویس‌ها رو میبینم و با دستور call Sum به API جمع دو عدد در ماشین حساب دسترسی داریم و میتونیم تستش کنیم.

شاید کار کردن با ترمینال براتون سخته و دنبال ابزاری مثل پست‌من هستید، باید بگم همچین ابزاری به اسم bloomrpc وجود داره ولی از رفلکشن ساپورت نمیکنه و باید فایل‌های پروتو رو بهش بدید ولی خیلی سادس، در تصویر پایین میتونید محیطشو ببینید:



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