شروع به کار با gRPC-Go - جریان

۱. مقدمه

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

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

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

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

آنچه یاد خواهید گرفت

  • نحوه استفاده از بافرهای پروتکل برای تعریف یک API سرویس.
  • نحوه ساخت یک کلاینت و سرور مبتنی بر gRPC از تعریف Protocol Buffers با استفاده از تولید خودکار کد.
  • آشنایی با ارتباطات استریمینگ کلاینت-سرور با gRPC

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

۲. قبل از شروع

پیش‌نیازها

مطمئن شوید که موارد زیر را نصب کرده‌اید:

  • زنجیره ابزار Go نسخه ۱.۲۴.۵ یا بالاتر. برای دستورالعمل‌های نصب، به بخش شروع به کار Go مراجعه کنید.
  • کامپایلر بافر پروتکل، protoc ، نسخه ۳.۲۷.۱ یا بالاتر. برای دستورالعمل‌های نصب، به راهنمای نصب کامپایلر مراجعه کنید.
  • افزونه‌های کامپایلر بافر پروتکل برای Go و gRPC. برای نصب این افزونه‌ها، دستورات زیر را اجرا کنید:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

متغیر PATH خود را به‌روزرسانی کنید تا کامپایلر بافر پروتکل بتواند افزونه‌ها را پیدا کند:

export PATH="$PATH:$(go env GOPATH)/bin"

کد را دریافت کنید

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

ابتدا، دایرکتوری کاری codelab را ایجاد کنید و cd به آن وارد شوید:

mkdir streaming-grpc-go-getting-started && cd streaming-grpc-go-getting-started

کدلب را دانلود و استخراج کنید:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-go-streaming/start_here

روش دیگر این است که فایل .zip که فقط شامل دایرکتوری codelab است را دانلود کرده و به صورت دستی آن را از حالت فشرده خارج کنید.

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

۳. تعریف پیام‌ها و خدمات

اولین قدم شما تعریف سرویس gRPC برنامه، متدهای RPC آن و انواع پیام‌های درخواست و پاسخ آن با استفاده از Protocol Buffers است. سرویس شما موارد زیر را ارائه خواهد داد:

  • متدهای RPC به نام‌های ListFeatures ، RecordRoute و RouteChat که سرور پیاده‌سازی می‌کند و کلاینت آنها را فراخوانی می‌کند.
  • انواع پیام Point ، Feature ، Rectangle ، RouteNote و RouteSummary هستند که ساختارهای داده‌ای هستند که هنگام فراخوانی متدهای بالا بین کلاینت و سرور رد و بدل می‌شوند.

این متدهای RPC و انواع پیام‌های آن، همگی در فایل routeguide/route_guide.proto از کد منبع ارائه شده تعریف خواهند شد.

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

تعریف انواع پیام

در فایل routeguide/route_guide.proto از کد منبع، ابتدا نوع پیام Point را تعریف کنید. یک Point نشان دهنده یک جفت مختصات طول و عرض جغرافیایی روی نقشه است. برای این codelab، از اعداد صحیح برای مختصات استفاده کنید:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

اعداد 1 و 2 شماره‌های شناسایی منحصر به فرد برای هر یک از فیلدهای موجود در ساختار message هستند.

سپس، نوع پیام Feature را تعریف کنید. یک Feature از یک فیلد string برای نام یا آدرس پستی چیزی در مکانی که توسط Point مشخص شده است، استفاده می‌کند:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

سپس یک پیام Rectangle که یک مستطیل عرض-طول جغرافیایی را نشان می‌دهد، به صورت دو نقطه مورب روبروی هم "lo" و "hi" نمایش داده می‌شود.

message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;

  // The other corner of the rectangle.
  Point hi = 2;
}

همچنین یک پیام RouteNote که نشان دهنده پیامی است که در یک نقطه معین ارسال شده است.

message RouteNote {
  // The location from which the message is sent.
  Point location = 1;

  // The message to be sent.
  string message = 2;
}

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

message RouteSummary {
  // The number of points received.
  int32 point_count = 1;

  // The number of known features passed while traversing the route.
  int32 feature_count = 2;

  // The distance covered in metres.
  int32 distance = 3;

  // The duration of the traversal in seconds.
  int32 elapsed_time = 4;
}

تعریف روش‌های سرویس

برای تعریف یک سرویس، شما یک سرویس نامگذاری شده را در فایل .proto خود مشخص می‌کنید. فایل route_guide.proto دارای یک ساختار service به نام RouteGuide است که یک یا چند متد ارائه شده توسط سرویس برنامه را تعریف می‌کند.

متدهای RPC را در تعریف سرویس خود تعریف کنید و انواع درخواست و پاسخ آنها را مشخص کنید. در این بخش از codelab، بیایید موارد زیر را تعریف کنیم:

ویژگی‌ها

Feature موجود در Rectangle داده شده را دریافت می‌کند. نتایج به جای اینکه به طور همزمان بازگردانده شوند، به صورت جریانی (streamed) ارسال می‌شوند (مثلاً در یک پیام پاسخ با یک فیلد تکراری)، زیرا مستطیل ممکن است ناحیه بزرگی را پوشش دهد و شامل تعداد زیادی ویژگی باشد.

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

rpc ListFeatures(Rectangle) returns (stream Feature) {}

رکوردروت

جریانی از Point ) را در مسیری که پیمایش می‌شود، می‌پذیرد و پس از اتمام پیمایش، یک RouteSummary برمی‌گرداند.

در این مورد، یک RPC استریمینگ سمت کلاینت مناسب به نظر می‌رسد: کلاینت دنباله ای از پیام‌ها را می‌نویسد و آنها را دوباره با استفاده از یک استریم ارائه شده به سرور ارسال می‌کند. پس از اینکه کلاینت نوشتن پیام‌ها را تمام کرد، منتظر می‌ماند تا سرور همه آنها را بخواند و پاسخ خود را برگرداند. شما با قرار دادن کلمه کلیدی stream قبل از نوع درخواست، یک روش استریمینگ سمت کلاینت را مشخص می‌کنید.

rpc RecordRoute(stream Point) returns (RouteSummary) {}

روت‌چت

جریانی از RouteNote های ارسالی را هنگام پیمایش یک مسیر می‌پذیرد، در حالی که RouteNote های دیگری (مثلاً از سایر کاربران) دریافت می‌کند.

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

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

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

rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

۴. تولید کد کلاینت و سرور

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

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       route_guide.proto

این دستور فایل‌های زیر را تولید می‌کند:

  • route_guide.pb.go که شامل توابعی برای ایجاد انواع پیام‌های برنامه و دسترسی به داده‌های آنها و تعریف انواعی است که پیام‌ها را نشان می‌دهند.
  • route_guide_grpc.pb.go ، که شامل توابعی است که کلاینت برای فراخوانی متد gRPC از راه دور سرویس استفاده می‌کند، و توابعی که سرور برای ارائه آن سرویس از راه دور استفاده می‌کند.

در مرحله بعد، متدها را در سمت سرور پیاده‌سازی خواهیم کرد، به طوری که وقتی کلاینت درخواستی ارسال می‌کند، سرور بتواند با یک پاسخ پاسخ دهد.

۵. پیاده‌سازی سرویس

ابتدا بیایید نگاهی به نحوه ایجاد یک سرور RouteGuide بیندازیم. برای اینکه سرویس RouteGuide ما کارش را انجام دهد، دو بخش وجود دارد:

  • پیاده‌سازی رابط سرویس تولید شده از تعریف سرویس ما: انجام "کار" واقعی سرویس ما.
  • اجرای یک سرور gRPC برای گوش دادن به درخواست‌های کلاینت‌ها و ارسال آنها به پیاده‌سازی صحیح سرویس.

بیایید RouteGuide را در server/server.go پیاده‌سازی کنیم.

پیاده‌سازی RouteGuide

ما باید رابط RouteGuideService تولید شده را پیاده‌سازی کنیم. پیاده‌سازی به این صورت خواهد بود.

type routeGuideServer struct {
        ...
}
...
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
        ...
}
...

func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
        ...
}
...

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
        ...
}

بیایید هر پیاده‌سازی RPC را با جزئیات بررسی کنیم.

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

با یکی از RPCهای استریمینگ ما شروع کنید. ListFeatures یک RPC استریمینگ سمت سرور است، بنابراین باید چندین Feature را به کلاینت خود ارسال کنیم.

func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
  for _, feature := range s.savedFeatures {
    if inRange(feature.Location, rect) {
      if err := stream.Send(feature); err != nil {
        return err
      }
    }
  }
  return nil
}

همانطور که می‌بینید، به جای دریافت اشیاء درخواست و پاسخ ساده در پارامترهای متد، این بار یک شیء درخواست ( Rectangle که کلاینت ما می‌خواهد Features در آن پیدا کند) و یک شیء ویژه RouteGuide_ListFeaturesServer برای نوشتن پاسخ‌هایمان دریافت می‌کنیم. در این متد، هر تعداد شیء Feature که نیاز به بازگشت داشته باشیم را پر می‌کنیم و آنها را با استفاده از متد Send() در RouteGuide_ListFeaturesServer می‌نویسیم. در نهایت، مانند RPC ساده‌مان، یک خطای nil برمی‌گردانیم تا به gRPC بگوییم که نوشتن پاسخ‌ها را تمام کرده‌ایم. در صورت بروز هرگونه خطا در این فراخوانی، یک خطای غیر nil برمی‌گردانیم. لایه gRPC آن را به یک وضعیت RPC مناسب برای ارسال از طریق سیم تبدیل می‌کند.

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

حالا بیایید به چیزی کمی پیچیده‌تر نگاه کنیم: متد استریمینگ سمت کلاینت RecordRoute ، که در آن یک جریان از Points از کلاینت دریافت می‌کنیم و یک RouteSummary واحد را با اطلاعات مربوط به سفر آنها برمی‌گردانیم. همانطور که می‌بینید، این بار این متد اصلاً پارامتر درخواست ندارد. در عوض، یک استریم RouteGuide_RecordRouteServer دریافت می‌کند که سرور می‌تواند از آن برای خواندن و نوشتن پیام‌ها استفاده کند - می‌تواند پیام‌های کلاینت را با استفاده از متد Recv() خود دریافت کند و پاسخ واحد خود را با استفاده از متد SendAndClose() خود بازگرداند.

func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
  var pointCount, featureCount, distance int32
  var lastPoint *pb.Point
  startTime := time.Now()
  for {
    point, err := stream.Recv()
    if err == io.EOF {
      endTime := time.Now()
      return stream.SendAndClose(&pb.RouteSummary{
        PointCount:   pointCount,
        FeatureCount: featureCount,
        Distance:     distance,
        ElapsedTime:  int32(endTime.Sub(startTime).Seconds()),
      })
    }
    if err != nil {
      return err
    }
    pointCount++
    for _, feature := range s.savedFeatures {
      if proto.Equal(feature.Location, point) {
        featureCount++
      }
    }
    if lastPoint != nil {
      distance += calcDistance(lastPoint, point)
    }
    lastPoint = point
  }
}

در بدنه متد، ما از متد Recv() در RouteGuide_RecordRouteServer برای خواندن مکرر درخواست‌های کلاینت خود به یک شیء درخواست (در این مورد یک Point ) استفاده می‌کنیم تا زمانی که دیگر پیامی وجود نداشته باشد: سرور باید خطای برگشتی از Recv() پس از هر فراخوانی بررسی کند. اگر این nil باشد، جریان پیام هنوز خوب است و می‌تواند به خواندن ادامه دهد؛ اگر io.EOF باشد، جریان پیام پایان یافته است و سرور می‌تواند RouteSummary آن را برگرداند. اگر مقدار دیگری داشته باشد، ما خطا را "همانطور که هست" برمی‌گردانیم تا توسط لایه gRPC به وضعیت RPC ترجمه شود.

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

در نهایت، بیایید نگاهی به RPC استریمینگ دوطرفه RouteChat() خود بیندازیم.

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
  for {
    in, err := stream.Recv()
    if err == io.EOF {
      return nil
    }
    if err != nil {
      return err
    }
    key := serialize(in.Location)

    s.mu.Lock()
    s.routeNotes[key] = append(s.routeNotes[key], in)
    // Note: this copy prevents blocking other clients while serving this one.
    // We don't need to do a deep copy, because elements in the slice are
    // insert-only and never modified.
    rn := make([]*pb.RouteNote, len(s.routeNotes[key]))
    copy(rn, s.routeNotes[key])
    s.mu.Unlock()

    for _, note := range rn {
      if err := stream.Send(note); err != nil {
        return err
      }
    }
  }
}

این بار یک جریان RouteGuide_RouteChatServer دریافت می‌کنیم که، مانند مثال جریان‌سازی سمت کلاینت، می‌تواند برای خواندن و نوشتن پیام‌ها استفاده شود. با این حال، این بار مقادیر را از طریق جریان متد خود برمی‌گردانیم در حالی که کلاینت هنوز در حال نوشتن پیام‌ها در جریان پیام خود است. سینتکس خواندن و نوشتن در اینجا بسیار شبیه به روش جریان‌سازی کلاینت ما است، با این تفاوت که سرور از متد send() جریان به جای SendAndClose() استفاده می‌کند زیرا چندین پاسخ می‌نویسد. اگرچه هر طرف همیشه پیام‌های طرف دیگر را به ترتیبی که نوشته شده‌اند دریافت می‌کند، اما هم کلاینت و هم سرور می‌توانند به هر ترتیبی بخوانند و بنویسند - جریان‌ها کاملاً مستقل عمل می‌کنند.

سرور را شروع کنید

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

lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
  log.Fatalf("failed to listen: %v", err)
}

grpcServer := grpc.NewServer()

s := &routeGuideServer{routeNotes: make(map[string][]*pb.RouteNote)}
s.loadFeatures()
pb.RegisterRouteGuideServer(grpcServer, s)
grpcServer.Serve(lis)

در اینجا مراحل انجام کار در main() آورده شده است:

  1. پورت TCP مورد استفاده برای گوش دادن به درخواست‌های کلاینت راه دور را با استفاده از lis, err := net.Listen(...) مشخص کنید. به طور پیش‌فرض، برنامه از پورت TCP 50051 استفاده می‌کند، همانطور که توسط متغیر port یا با وارد کردن سوئیچ --port در خط فرمان هنگام اجرای سرور مشخص شده است. اگر پورت TCP نتواند باز شود، برنامه با یک خطای مهلک به پایان می‌رسد.
  2. با استفاده از grpc.NewServer(...) یک نمونه از سرور gRPC ایجاد کنید و نام این نمونه را grpcServer بگذارید.
  3. یک اشاره‌گر به routeGuideServer ایجاد کنید، ساختاری که نشان‌دهنده سرویس API برنامه است و اشاره‌گر را s.
  4. از s.loadFeatures() برای پر کردن آرایه s.savedFeatures استفاده کنید.
  5. پیاده‌سازی سرویس خود را در سرور gRPC ثبت کنید.
  6. تابع Serve() روی سرور با جزئیات پورت خود فراخوانی کنید تا یک انتظار مسدودکننده برای درخواست‌های کلاینت انجام دهید؛ این کار تا زمانی که فرآیند متوقف شود یا Stop() فراخوانی شود، ادامه می‌یابد.

تابع loadFeatures() نگاشت‌های مختصات به مکان خود را از server/testdata.go دریافت می‌کند.

۶. مشتری را ایجاد کنید

اکنون client/client.go را ویرایش کنید، جایی که کد کلاینت را پیاده‌سازی خواهید کرد.

برای فراخوانی متدهای سرویس راه دور، ابتدا باید یک کانال gRPC برای ارتباط با سرور ایجاد کنیم. ما این کار را با ارسال رشته URI هدف سرور (که در این مورد صرفاً آدرس و شماره پورت است) به grpc.NewClient() در تابع main() کلاینت به صورت زیر انجام می‌دهیم:

// Set up a connection to the gRPC server.
conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
  log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

آدرس سرور که توسط متغیر serverAddr تعریف می‌شود، به طور پیش‌فرض localhost:50051 است و می‌تواند توسط سوئیچ --addr در خط فرمان هنگام اجرای کلاینت لغو شود.

اگر کلاینت نیاز به اتصال به سرویسی داشته باشد که به اعتبارنامه‌های احراز هویت مانند اعتبارنامه‌های TLS یا JWT نیاز دارد، می‌تواند یک شیء DialOptions را به عنوان پارامتر به grpc.NewClient ارسال کند که شامل اعتبارنامه‌های مورد نیاز است. سرویس RouteGuide به هیچ اعتبارنامه‌ای نیاز ندارد.

پس از راه‌اندازی کانال gRPC، به یک stub کلاینت نیاز داریم تا RPCها را از طریق فراخوانی‌های تابع Go انجام دهیم. ما این stub را با استفاده از متد NewRouteGuideClient که توسط فایل route_guide_grpc.pb.go تولید شده از فایل .proto برنامه ارائه شده است، دریافت می‌کنیم.

import (pb "github.com/grpc-ecosystem/codelabs/getting_started_streaming/routeguide")

client := pb.NewRouteGuideClient(conn)

روش‌های سرویس تماس

حالا بیایید نگاهی به نحوه فراخوانی متدهای سرویس خود بیندازیم. در gRPC-Go، RPCها در حالت مسدودکننده/همزمان عمل می‌کنند، به این معنی که فراخوانی RPC منتظر پاسخ سرور می‌ماند و یا پاسخی را برمی‌گرداند یا خطایی را نشان می‌دهد.

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

اینجا جایی است که ما متد استریمینگ سمت سرور ListFeatures فراخوانی می‌کنیم، که جریانی از اشیاء Feature جغرافیایی را برمی‌گرداند.

rect := &pb.Rectangle{ ... }  // initialize a pb.Rectangle
log.Printf("Looking for features within %v", rect)
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
  log.Fatalf("client.ListFeatures failed: %v", err)
}
for {
  // For server-to-client streaming RPCs, you call stream.Recv() until it
  // returns io.EOF.
  feature, err := stream.Recv()
  if err == io.EOF {
    break
  }
  if err != nil {
    log.Fatalf("client.ListFeatures failed: %v", err)
  }
  log.Printf("Feature: name: %q, point:(%v, %v)", feature.GetName(),
    feature.GetLocation().GetLatitude(), feature.GetLocation().GetLongitude())
}

همانند RPC ساده، ما یک context و یک request به متد ارسال می‌کنیم. با این حال، به جای دریافت یک شیء پاسخ، یک نمونه از RouteGuide_ListFeaturesClient را برمی‌گردانیم. کلاینت می‌تواند از جریان RouteGuide_ListFeaturesClient برای خواندن پاسخ‌های سرور استفاده کند. ما از متد Recv() در RouteGuide_ListFeaturesClient برای خواندن مکرر پاسخ‌های سرور به یک شیء بافر پروتکل پاسخ (در این مورد یک Feature ) استفاده می‌کنیم تا زمانی که دیگر پیامی وجود نداشته باشد: کلاینت باید خطای err برگردانده شده از Recv() را پس از هر فراخوانی بررسی کند. اگر nil ، جریان هنوز خوب است و می‌تواند به خواندن ادامه دهد. اگر io.EOF باشد، جریان پیام پایان یافته است. در غیر این صورت باید یک خطای RPC وجود داشته باشد که از طریق err منتقل می‌شود.

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

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

// Create a random number of random points
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points
var points []*pb.Point
for i := 0; i < pointCount; i++ {
  points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
c2sStream, err := client.RecordRoute(context.TODO())
if err != nil {
  log.Fatalf("client.RecordRoute failed: %v", err)
}
// Stream each point to the server.
for _, point := range points {
  if err := c2sStream.Send(point); err != nil {
    log.Fatalf("client.RecordRoute: stream.Send(%v) failed: %v", point, err)
  }
}
// Close the stream and receive the RouteSummary from the server.
reply, err := c2sStream.CloseAndRecv()
if err != nil {
  log.Fatalf("client.RecordRoute failed: %v", err)
}
log.Printf("Route summary: %v", reply)

RouteGuide_RecordRouteClient یک متد Send() دارد که می‌توانیم از آن برای ارسال درخواست‌ها به سرور استفاده کنیم. پس از اتمام نوشتن درخواست‌های کلاینت به استریم با استفاده از Send() ، باید CloseAndRecv() را روی استریم فراخوانی کنیم تا به gRPC اطلاع دهیم که نوشتن را تمام کرده‌ایم و منتظر دریافت پاسخ هستیم. وضعیت RPC خود را از خطای برگردانده شده از CloseAndRecv() دریافت می‌کنیم. اگر وضعیت nil باشد، اولین مقدار برگشتی از CloseAndRecv() یک پاسخ معتبر از سرور خواهد بود.

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

در نهایت، بیایید به استریمینگ دوطرفه RPC خود RouteChat() نگاهی بیندازیم. همانند مورد RecordRoute ، ما فقط یک شیء context را به متد ارسال می‌کنیم و یک استریم دریافت می‌کنیم که می‌توانیم از آن برای نوشتن و خواندن پیام‌ها استفاده کنیم. با این حال، این بار مقادیر را از طریق استریم متد خود برمی‌گردانیم در حالی که سرور هنوز در حال نوشتن پیام‌ها در استریم پیام‌های آنها است.

biDiStream, err := client.RouteChat(context.Background())
if err != nil {
  log.Fatalf("client.RouteChat failed: %v", err)
}
// this channel is used to wait for the receive goroutine to finish.
recvDoneCh := make(chan struct{})
// receive goroutine.
go func() {
  for {
    in, err := biDiStream.Recv()
    if err == io.EOF {
      // read done.
      close(recvDoneCh)
      return
    }
    if err != nil {
      log.Fatalf("client.RouteChat failed: %v", err)
    }
    log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
  }
}()
// send messages simultaneously.
for _, note := range notes {
  if err := biDiStream.Send(note); err != nil {
    log.Fatalf("client.RouteChat: stream.Send(%v) failed: %v", note, err)
  }
}
biDiStream.CloseSend()
// wait for the receive goroutine to finish.
<-recvDoneCh

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

۷. امتحانش کنید

با اجرای دستورات زیر در دایرکتوری کاری برنامه، تأیید کنید که سرور و کلاینت به درستی با یکدیگر کار می‌کنند:

  1. سرور را در یک ترمینال اجرا کنید:
cd server
go run .
  1. کلاینت را از یک ترمینال دیگر اجرا کنید:
cd client
go run .

خروجی مانند این را خواهید دید، که برای وضوح بیشتر، مهرهای زمانی حذف شده‌اند:

Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 >
name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 >
...
name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 >
Traversing 56 points.
Route summary: point_count:56 distance:497013163
Got message First message at point(0, 1)
Got message Second message at point(0, 2)
Got message Third message at point(0, 3)
Got message First message at point(0, 1)
Got message Fourth message at point(0, 1)
Got message Second message at point(0, 2)
Got message Fifth message at point(0, 2)
Got message Third message at point(0, 3)
Got message Sixth message at point(0, 3)

۸. قدم بعدی چیست؟