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

1. مقدمه

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

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

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

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

چیزی که یاد خواهید گرفت

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

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

2. قبل از شروع

پیش نیازها

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

  • Go toolchain نسخه 1.24.5 یا بالاتر. برای دستورالعمل‌های نصب، به Go's Getting Start مراجعه کنید.
  • کامپایلر بافر پروتکل، protoc ، نسخه 3.27.1 یا بالاتر. برای دستورالعمل‌های نصب، به راهنمای نصب کامپایلر مراجعه کنید.
  • افزونه های کامپایلر بافر پروتکل برای 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"

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

برای اینکه مجبور نباشید به طور کامل از ابتدا شروع کنید، این کد لبه داربستی از کد منبع برنامه را برای شما فراهم می کند تا آن را تکمیل کنید. مراحل زیر به شما نشان می دهد که چگونه برنامه را تکمیل کنید، از جمله استفاده از افزونه های کامپایلر بافر پروتکل برای تولید کد 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 است دانلود کرده و به صورت دستی آن را از حالت فشرده خارج کنید.

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

3. پیام ها و خدمات را تعریف کنید

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

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

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

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

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

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

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 داده شده را به دست می آورد. نتایج به‌جای بازگردانی یک‌باره پخش می‌شوند (مثلاً در یک پیام پاسخ با یک فیلد تکراری)، زیرا مستطیل ممکن است یک منطقه بزرگ را پوشش دهد و دارای تعداد زیادی ویژگی باشد.

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

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

RecordRoute

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

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

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

روت چت

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

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

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

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

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

4. کد مشتری و سرور تولید کنید

سپس، کد 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 راه دور سرویس استفاده می کند، و توابعی که توسط سرور برای ارائه آن سرویس راه دور استفاده می شود.

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

5. سرویس را پیاده سازی کنید

ابتدا بیایید نحوه ایجاد سرور 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 بگوییم که نوشتن پاسخ‌ها به پایان رسیده است. اگر در این فراخوانی خطایی رخ دهد، یک خطای غیر صفر را برمی‌گردانیم. لایه 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 دریافت می کنیم که، مانند مثال پخش جریانی در سمت کلاینت، می تواند برای خواندن و نوشتن پیام ها استفاده شود. با این حال، این بار ما مقادیر را از طریق جریان روش خود در حالی که مشتری هنوز در حال نوشتن پیام به جریان پیام خود است، برمی گردانیم. نحو برای خواندن و نوشتن در اینجا بسیار شبیه به روش پخش جریانی مشتری ما است، با این تفاوت که سرور به جای SendAndClose() از متد send() استریم استفاده می کند زیرا چندین پاسخ را می نویسد. اگرچه هر یک از طرفین همیشه پیام های طرف مقابل را به ترتیبی که نوشته شده دریافت می کند، هم مشتری و هم سرور می توانند به هر ترتیبی بخوانند و بنویسند - جریان ها کاملاً مستقل عمل می کنند.

سرور را راه اندازی کنید

هنگامی که همه روش‌های خود را پیاده‌سازی کردیم، همچنین باید یک سرور 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 با استفاده از grpc.NewServer(...) ایجاد کنید و نام این نمونه را grpcServer بگذارید.
  3. یک اشاره گر به routeGuideServer ، ساختاری که سرویس API برنامه را نشان می دهد، ایجاد کنید و اشاره گر را s.
  4. از s.loadFeatures() برای پر کردن آرایه s.savedFeatures استفاده کنید.
  5. اجرای سرویس ما را با سرور gRPC ثبت کنید.
  6. Serve() روی سرور با جزئیات پورت ما فراخوانی کنید تا منتظر بلاک کردن درخواست های مشتری باشد. این تا زمانی ادامه می یابد که فرآیند کشته شود یا Stop() فراخوانی شود.

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

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

اکنون 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 راه‌اندازی شد، برای انجام RPC‌ها از طریق فراخوانی تابع Go به یک کلاینت نیاز داریم. ما آن خرد را با استفاده از روش 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 ساده، روش را یک متن و یک درخواست ارسال می کنیم. با این حال، به‌جای بازگرداندن یک شی پاسخ، نمونه‌ای از RouteGuide_ListFeaturesClient را برمی‌گردانیم. مشتری می تواند از جریان RouteGuide_ListFeaturesClient برای خواندن پاسخ های سرور استفاده کند. ما از روش RouteGuide_ListFeaturesClient 's Recv() برای خواندن مکرر پاسخ‌های سرور به یک شی بافر پروتکل پاسخ (در این مورد Feature ) استفاده می‌کنیم تا زمانی که دیگر پیامی وجود نداشته باشد: مشتری باید خطای بازگشتی از Recv() را بعد از هر تماس بررسی کند. اگر nil ، جریان همچنان خوب است و می تواند به خواندن ادامه دهد. اگر io.EOF باشد، جریان پیام به پایان رسیده است. در غیر این صورت باید یک خطای RPC وجود داشته باشد که از طریق err منتقل می شود.

جریان RPC سمت مشتری

روش پخش سمت سرویس گیرنده RecordRoute شبیه روش سمت سرور است، با این تفاوت که ما فقط روش را به یک متن منتقل می کنیم و یک جریان 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 ، ما فقط روش را به یک شیء متنی منتقل می کنیم و جریانی را دریافت می کنیم که می توانیم از آن برای نوشتن و خواندن پیام ها استفاده کنیم. با این حال، این بار در حالی که سرور هنوز در حال نوشتن پیام‌ها به جریان پیام خود است، مقادیر را از طریق جریان روش خود برمی‌گردانیم.

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

7. آن را امتحان کنید

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

  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)

8. بعدی چه خواهد شد