תחילת העבודה עם gRPC-Go

1. מבוא

ב-codelab הזה תשתמשו ב-gRPC-Go כדי ליצור לקוח ושרת שמהווים את הבסיס לאפליקציה למיפוי מסלולים שנכתבה ב-Go.

בסוף המדריך יהיה לכם לקוח שמתחבר לשרת מרוחק באמצעות gRPC כדי לקבל את השם או את הכתובת למשלוח דואר של מה שנמצא בקואורדינטות ספציפיות במפה. אפליקציה מפותחת יכולה להשתמש בעיצוב הזה של לקוח-שרת כדי למנות או לסכם נקודות עניין לאורך מסלול.

השירות מוגדר בקובץ Protocol Buffers, שישמש ליצירת קוד boilerplate ללקוח ולשרת, כדי שהם יוכלו לתקשר זה עם זה. כך תוכלו לחסוך זמן ומאמץ בהטמעת הפונקציונליות הזו.

הקוד שנוצר מטפל לא רק במורכבויות של התקשורת בין השרת ללקוח, אלא גם בסריאליזציה ובדה-סריאליזציה של הנתונים.

מה תלמדו

  • איך משתמשים ב-Protocol Buffers כדי להגדיר API של שירות.
  • איך ליצור לקוח ושרת מבוססי gRPC מהגדרה של Protocol Buffers באמצעות יצירת קוד אוטומטית.
  • הבנה של תקשורת בין שרתים ללקוחות באמצעות gRPC.

ה-codelab הזה מיועד למפתחי Go שחדשים ב-gRPC או שרוצים לרענן את הידע שלהם ב-gRPC, או לכל מי שמתעניין בבניית מערכות מבוזרות. לא נדרש ניסיון קודם ב-gRPC.

‫2. לפני שמתחילים

דרישות מוקדמות

ודאו שהתקנתם את הפריטים הבאים:

  • ערכת הכלים של Go בגרסה 1.24.5 ואילך. הוראות התקנה מופיעות במאמר תחילת העבודה עם Go.
  • הקומפיילר של מאגר אחסון לפרוטוקולים, 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"

קבל את הקוד

כדי שלא תצטרכו להתחיל מאפס, ב-codelab הזה מופיע סקפולד של קוד המקור של האפליקציה שתוכלו להשלים. בשלבים הבאים מוסבר איך לסיים את האפליקציה, כולל שימוש בתוספים של קומפיילר פרוטוקול החוצץ כדי ליצור את קוד ה-boilerplate של gRPC.

מורידים את קוד המקור הזה כארכיון ZIP מ-GitHub ומחלצים את התוכן שלו.

לחלופין, אפשר להוריד את קוד המקור המלא מ-GitHub אם לא רוצים להקליד את ההטמעה.

3. הגדרת השירות

השלב הראשון הוא להגדיר את שירות gRPC של האפליקציה, את שיטת ה-RPC ואת סוגי ההודעות של הבקשה והתגובה באמצעות Protocol Buffers. השירות שלכם יספק:

  • שיטת RPC שנקראת GetFeature, שהשרת מטמיע והלקוח מפעיל.
  • סוגי ההודעות Point ו-Feature שהן מבני נתונים שמועברים בין הלקוח לשרת כשמשתמשים בשיטה GetFeature. הלקוח מספק קואורדינטות של מפה כ-Point בבקשת GetFeature שלו לשרת, והשרת משיב עם Feature תואם שמתאר את מה שנמצא בקואורדינטות האלה.

שיטת ה-RPC הזו וסוגי ההודעות שלה יוגדרו בקובץ routeguide/route_guide.proto של קוד המקור שסופק.

פרוטוקול Buffers ידוע בדרך כלל בשם 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 שמגדיר שיטה אחת או יותר שסופקו על ידי השירות של האפליקציה.

מוסיפים את השיטה 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 באמצעות מהדר פרוטוקול ה-Buffer. בספרייה 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 של Protocol Buffer מהבקשה של הלקוח. הפונקציה מחזירה אובייקט של מאגר פרוטוקולים Feature למיקום שנבדק, וגם error לפי הצורך.

בשיטה, מאכלסים אובייקט Feature במידע המתאים ל-Point הנתון, ואז return אותו יחד עם שגיאת nil כדי לציין ל-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, שזה המקום שבו מטמיעים את קוד הלקוח.

כדי להפעיל את ה-methods של השירות המרוחק, קודם צריך ליצור ערוץ 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 באמצעות ה-method‏ 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) מחכה לתגובה מהשרת, ותחזיר תגובה או שגיאה.

Simple RPC

הקריאה ל-RPC הפשוט GetFeature היא כמעט פשוטה כמו הקריאה ל-method מקומית, במקרה הזה 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)
}

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. המאמרים הבאים