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

1. מבוא

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

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

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

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

מה תלמדו

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

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

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

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

  • JDK בגרסה 8 ואילך

קבל את הקוד

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

קודם יוצרים את ספריית העבודה של ה-codelab ועוברים אליה:

mkdir grpc-java-getting-started && cd grpc-java-getting-started

מורידים ומחלצים את ה-Codelab:

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-java-getting-started/start_here

אפשרות אחרת היא להוריד את קובץ ה-‎ .zip שמכיל רק את ספריית ה-codelab ולבטל את הדחיסה שלו באופן ידני.

קוד המקור המלא זמין ב-GitHub אם אתם רוצים לדלג על הקלדת ההטמעה.

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

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

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

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

פרוטוקול Buffers ידוע בדרך כלל בשם protobufs. מידע נוסף על המינוח של gRPC זמין במאמר מושגי ליבה, ארכיטקטורה ומחזור חיים של gRPC.

בדוגמה הזו אנחנו יוצרים קוד Java, ולכן ציינו אפשרות של קובץ java_package ושם למחלקת Java ב-.proto:

option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";

סוגי הודעות

בקובץ 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‏ protoc עם פלאגין מיוחד של gRPC Java. כדי ליצור שירותי gRPC, צריך להשתמש בקומפיילר proto3 (שתומך בתחביר של proto2 וגם של proto3).

כשמשתמשים ב-Gradle או ב-Maven, תוסף ה-build‏ protoc יכול ליצור את הקוד הנדרש כחלק מה-build. ב-README של grpc-java מוסבר איך ליצור קוד מקובצי .proto משלכם.

סיפקנו סביבת Gradle והגדרות בקוד המקור של ה-codelab כדי לבנות את הפרויקט הזה.

בתוך הספרייה grpc-java-getting-started, מריצים את הפקודה הבאה:

$ chmod +x gradlew
$ ./gradlew generateProto

הכיתות הבאות נוצרות מהגדרת השירות שלנו:

  • Feature.java,‏ Point.java ואחרים שמכילים את כל קוד מאגר הפרוטוקולים לאכלוס, לסריאליזציה ולאחזור של סוגי הודעות הבקשה והתגובה שלנו.
  • RouteGuideGrpc.java שמכיל (יחד עם קוד שימושי אחר) מחלקת בסיס להטמעה בשרתי RouteGuide, ‏ RouteGuideGrpc.RouteGuideImplBase, עם כל השיטות שמוגדרות בשירות RouteGuide ובמחלקות stub לשימוש הלקוחות.

5. הטמעה של השרת

קודם נראה איך יוצרים RouteGuide שרת. יש שני חלקים בתהליך שבו שירות RouteGuide מבצע את העבודה שלו:

  • הטמעה של ממשק השירות שנוצר מהגדרת השירות שלנו, שמבצע את ה "עבודה" בפועל של השירות שלנו.
  • הפעלת שרת gRPC להאזנה לבקשות מלקוחות ולשליחתן להטמעה הנכונה של השירות.

הטמעה של RouteGuide

כפי שאפשר לראות, לשרת שלנו יש מחלקה RouteGuideService שמרחיבה את המחלקה המופשטת RouteGuideGrpc.RouteGuideImplBase שנוצרה:

private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}

צירפנו את 2 הקבצים הבאים לאתחול השרת עם תכונות:

./src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java

./src/main/resources/io/grpc/examples/routeguide/route_guide_db.json

נבחן עכשיו בפירוט הטמעה פשוטה של RPC.

Unary RPC

RouteGuideService מטמיע את כל שיטות השירות שלנו. במקרה הזה, זה רק GetFeature(), מקבל הודעה Point מהלקוח ומחזיר בהודעה Feature את פרטי המיקום התואמים מתוך רשימה של מקומות מוכרים.

@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
  responseObserver.onNext(checkFeature(request));
  responseObserver.onCompleted();
}

השיטה getFeature() מקבלת שני פרמטרים:

  • Point: הבקשה.
  • StreamObserver<Feature>: אובייקט לצפייה בתגובה, שהוא ממשק מיוחד שהשרת יכול להשתמש בו כדי להתקשר עם התגובה שלו.

כדי להחזיר את התגובה שלנו ללקוח ולסיים את השיחה:

  1. אנחנו יוצרים ומאכלסים אובייקט תגובה של Feature כדי להחזיר אותו ללקוח, כפי שמצוין בהגדרת השירות שלנו. בדוגמה הזו, אנחנו עושים את זה בשיטה פרטית נפרדת checkFeature().
  2. אנחנו משתמשים בשיטה onNext() של response observer כדי להחזיר את Feature.
  3. אנחנו משתמשים בשיטה onCompleted() של response observer כדי לציין שסיימנו לטפל ב-RPC.

הפעלת השרת

אחרי שמטמיעים את כל שיטות השירות, צריך להפעיל שרת gRPC כדי שהלקוחות יוכלו להשתמש בשירות. אנחנו כוללים ב-boilerplate שלנו את יצירת האובייקט ServerBuilder:

ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)

אנחנו יוצרים את השירות ב-constructor:

  1. מציינים את היציאה שבה רוצים להשתמש כדי להאזין לבקשות של לקוחות באמצעות השיטה forPort() של הכלי לבנייה (הוא ישתמש בכתובת עם תו כללי).
  2. יוצרים מופע של מחלקת הטמעת השירות RouteGuideService ומעבירים אותו לשיטה addService() של הכלי ליצירת מופעים.
  3. מפעילים את הפונקציה build() ב-builder כדי ליצור שרת RPC לשירות.

בקטע הקוד הבא מוצג אופן היצירה של אובייקט ServerBuilder.

/** Create a RouteGuide server listening on {@code port} using {@code featureFile} database. */
public RouteGuideServer(int port, URL featureFile) throws IOException {
    this(Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()),
        port, RouteGuideUtil.parseFeatures(featureFile));
  }

בקטע הקוד הבא מוצג אופן יצירת אובייקט שרת עבור שירות RouteGuide.

/** Create a RouteGuide server using serverBuilder as a base and features as data. */
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
  this.port = port;
  server = serverBuilder.addService(new RouteGuideService(features))
      .build();
}

מטמיעים שיטת התחלה שמפעילה את start בשרת שיצרנו למעלה.

public void start() throws IOException {
  server.start();
  logger.info("Server started, listening on " + port);
}

מטמיעים שיטה להמתנה לסיום הפעולה של השרת, כדי שהפעולה לא תסתיים באופן מיידי.

/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
  if (server != null) {
    server.awaitTermination();
  }
}

כפי שאפשר לראות, אנחנו בונים את השרת ומתחילים להשתמש בו באמצעות ServerBuilder.

ב-method העיקרי אנחנו:

  1. יוצרים מכונת RouteGuideServer.
  2. כדי להפעיל שרת RPC בשירות שלנו, צריך להתקשר למספר start().
  3. כדי להפסיק את השירות, מתקשרים למספר blockUntilShutdown().
 public static void main(String[] args) throws Exception {
    RouteGuideServer server = new RouteGuideServer(8980);
    server.start();
    server.blockUntilShutdown();
  }

6. יצירת הלקוח

בקטע הזה נראה איך ליצור לקוח לשירות RouteGuide.

יצירת מופע של stub

כדי לקרוא לשיטות של שירות, קודם צריך ליצור stub. יש שני סוגים של stubs, אבל אנחנו צריכים להשתמש רק ב-stub שחוסם מודעות ב-codelab הזה. יש שני סוגים:

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

קודם צריך ליצור ערוץ gRPC ואז להשתמש בערוץ כדי ליצור את ה-stub.

יכולנו להשתמש ב-ManagedChannelBuilder ישירות כדי ליצור את הערוץ.

ManagedChannelBuilder.forAddress(host, port).usePlaintext().build

אבל נשתמש בשיטת עזר שמקבלת מחרוזת עם hostname:port.

Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();

עכשיו אפשר להשתמש בערוץ כדי ליצור את ה-stub לחסימה. ב-codelab הזה יש לנו רק RPCs חוסמים, לכן אנחנו משתמשים בשיטה newBlockingStub שסופקה בכיתה RouteGuideGrpc שיצרנו מ-.proto.

blockingStub = RouteGuideGrpc.newBlockingStub(channel);

הפעלת שיטות של שירות

עכשיו נראה איך קוראים לשיטות של השירות.

Simple RPC

הפעלת ה-RPC הפשוט GetFeature היא כמעט פשוטה כמו הפעלת method מקומי.

אנחנו יוצרים אובייקט של פרוטוקול buffer של בקשה (במקרה שלנו Point) ומאכלסים אותו, מעבירים אותו לשיטת getFeature() ב-stub החוסם ומקבלים בחזרה Feature.

אם מתרחשת שגיאה, היא מקודדת כ-Status, שאפשר לקבל מ-StatusRuntimeException.

Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();

Feature feature;
try {
  feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

הקוד המשותף מתעד הודעה שמכילה את התוכן, בהתאם לשאלה אם הייתה תכונה בנקודה שצוינה או לא.

7. כדאי לנסות!

  1. בתוך הספרייה start_here, מריצים את הפקודה הבאה:
$ ./gradlew installDist

הפעולה הזו תבצע קומפילציה של הקוד, תארוז אותו בקובץ JAR ותיצור את הסקריפטים שמריצים את הדוגמה. הם ייווצרו בספרייה build/install/start_here/bin/. התסריטים הם: route-guide-server ו-route-guide-client.

השרת צריך לפעול לפני שמפעילים את הלקוח.

  1. מריצים את השרת:
$ ./build/install/start_here/bin/route-guide-server
  1. מריצים את הלקוח:
$ ./build/install/start_here/bin/route-guide-client

יוצג פלט כמו זה, בלי חותמות זמן כדי שיהיה ברור:

INFO: *** GetFeature: lat=409,146,138 lon=-746,188,906
INFO: Found feature called "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" at 40.915, -74.619
INFO: *** GetFeature: lat=0 lon=0
INFO: Found no feature at 0, 0

8. המאמרים הבאים