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

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

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

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

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

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

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

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

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

123GET /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 نصب بشه:

1go get -u google.golang.org/grpc

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

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

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


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

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

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

123456789101112131415161718syntax = "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 تعریف کردیم (که بالاتر توضیح دادیم یونری چیه) و داخل ترمینال از پوشه روت پروژه دستور زیر رو برای ساخت فایل پروتو وارد کردیم:

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

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


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

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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546package 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 همراه با محتوای زیر ایجاد میکنید:

123456789101112131415161718192021222324252627282930313233343536373839package 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 کلاینت یک مسیج به سرور ارسال میکنه و بیشتر از یک ریسپانس از سرور دریافت میکنه (در تعداد ریسپانس‌ها محدودیتی وجود نداره، میتونه نامحدود باشه).

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

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

1234567891011121314151617181920212223242526272829syntax = "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 در خط ۲۸ توجه کنید) و بعد از اون دوباره فایل پروتو رو با دستور پایین جنریت کردیم.

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

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

123456789101112131415161718192021222324func (*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 باز میکنیم و به این صورت تغییرش میدیم:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566package 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

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

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

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

12345678910111213141516171819202122232425262728293031323334353637383940syntax = "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) {};
}

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

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

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

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

12345678910111213141516171819202122func (*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 استفاده کردیم) وگرنه چک میکنه اگر موقع دریافت دیتا مشکل بخوره خطا رو نمایش بده، بعد اون اگر مشکلی نباشه عدد دریافتی از کاربر رو با اعداد دریافتی قبلی جمع میزنیم و یکی به تعداد کل اعداد اضافه میکنیم تا بعد بتونیم میانگین بگیریم.

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

123456789101112131415161718192021222324252627func 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 میشه برای سیستم چت و… نامبرد.

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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051syntax = "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) {};
}

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

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

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

123456789101112131415161718192021222324252627func (*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)، کد زیر رو به کلاینت اضافه میکنیم:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849func 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 دارای یک کد و یک مسیج هستند، تشخیص نوع خطا از روی کد هستش و مسیج بیشتر برای ساده کردن دیباگه.

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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162syntax = "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) {};
}

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

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

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

12345678910111213141516func (*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 استفاده کنید و یک پیام خطای شخصی سازی شده.

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

12345678910111213141516171819func 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 معمول?

نویسنده مطلب: عرفان

منبع مطلب

به فکر سرمایه‌گذاری هستی؟

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

ثبت نام و دریافت جایزه
نظر شما درباره این مطلب

آدرس ایمیل شما منتشر نخواهد شد.