شروع با 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 دیگ بخار.

این کد منبع را به عنوان یک بایگانی .ZIP از GitHub دانلود کنید و محتوای آن را باز کنید.

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

3. سرویس را تعریف کنید

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

  • یک روش RPC به نام GetFeature که سرور پیاده سازی می کند و کلاینت فراخوانی می کند.
  • انواع پیام Point و Feature که ساختارهای داده ای هستند که هنگام استفاده از روش GetFeature بین مشتری و سرور رد و بدل می شوند. مشتری مختصات نقشه را به عنوان یک Point در درخواست GetFeature خود به سرور ارائه می دهد و سرور با یک Feature مربوطه پاسخ می دهد که هر چیزی را که در آن مختصات قرار دارد را توصیف می کند.

این روش 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;
}

روش سرویس دهی

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

روش rpc GetFeature در تعریف RouteGuide اضافه کنید. همانطور که قبلاً توضیح داده شد، این روش نام یا آدرس یک مکان را از مجموعه مختصات معینی جستجو می کند، بنابراین از GetFeature بخواهید یک Feature برای یک Point داده شده برگرداند:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

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

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

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

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

تابع GetFeature در سمت سرور جایی است که کار اصلی انجام می شود: این یک پیام Point از مشتری می گیرد و در یک پیام Feature اطلاعات مکان مربوطه را از لیست مکان های شناخته شده برمی گرداند. در اینجا اجرای تابع در server/server.go آمده است:

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _, feature := range s.savedFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }
  // No feature was found, return an unnamed feature
  return &pb.Feature{Location: point}, nil
}

هنگامی که این روش به دنبال درخواستی از یک کلاینت راه دور فراخوانی می شود، تابع یک شی Context که فراخوانی RPC را توصیف می کند، و یک شی بافر پروتکل Point از آن درخواست مشتری ارسال می شود. تابع یک شی بافر پروتکل Feature را برای مکان جستجو شده و در صورت لزوم یک error برمی گرداند.

در این روش، یک شی Feature را با اطلاعات مناسب برای Point داده شده پر کنید، و سپس آن را همراه با یک خطای nil return تا به gRPC بگویید که کار با RPC را تمام کرده‌اید و اینکه شی Feature می‌تواند به مشتری بازگردانده شود.

متد GetFeature نیاز دارد که یک شی routeGuideServer ایجاد و ثبت شود تا درخواست‌های مشتریان برای جستجوی مکان به آن تابع هدایت شوند. این در main() انجام می شود:

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

  var opts []grpc.ServerOption
  grpcServer := grpc.NewServer(opts...)

  s := &routeGuideServer{}
  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 با مکان هایی استفاده کنید که می توان از طریق GetFeature جستجو کرد.
  5. سرویس API را با سرور gRPC ثبت کنید تا تماس‌های RPC به GetFeature به تابع مناسب هدایت شوند.
  6. Serve() روی سرور با جزئیات پورت ما فراخوانی کنید تا منتظر بلاک کردن درخواست های مشتری باشد. این تا زمانی ادامه می یابد که فرآیند کشته شود یا Stop() فراخوانی شود.

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

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

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

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

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_unary/routeguide")

client := pb.NewRouteGuideClient(conn)

روش های خدمات تماس

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

RPC ساده

فراخوانی RPC GetFeature ساده تقریباً به اندازه فراخوانی یک متد محلی است، در این مورد client.GetFeature :

point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
  log.Fatalf("client.GetFeature failed: %v", err)
}

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

log.Println(feature)

در کل تابع main() کلاینت باید به شکل زیر باشد:

func main() {
        flag.Parse()

        // 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()

        // Create a new RouteGuide stub.
        client := pb.NewRouteGuideClient(conn)

        point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
        log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

        // Call GetFeature method on the client.
        feature, err := client.GetFeature(context.TODO(), point)
        if err != nil {
                log.Fatalf("client.GetFeature failed: %v", err)
        }
        log.Println(feature)
}

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

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

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

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

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>

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