شروع با 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 قالب‌بندی شده را به شما نشان می‌دهد.

این کد منبع را به صورت یک فایل فشرده .ZIP از گیت‌هاب دانلود کنید و محتویات آن را از حالت فشرده خارج کنید.

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

۳. تعریف سرویس

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

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

این متد 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;
}

روش خدمات

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

متد GetFeature rpc را به تعریف 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 ساده که در آن کلاینت یک درخواست به سرور ارسال می‌کند و منتظر پاسخ می‌ماند، درست مانند یک فراخوانی تابع محلی.

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

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

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

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

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

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

اکنون 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، به یک stub کلاینت نیاز داریم تا RPCها را از طریق فراخوانی‌های تابع Go انجام دهیم. ما این stub را با استفاده از متد 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)
}

کلاینت متد را روی stub ایجاد شده قبلی فراخوانی می‌کند. برای پارامترهای متد، کلاینت یک شیء بافر پروتکل درخواست 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)
}

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

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

  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:<>

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