תחילת העבודה עם 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. אנחנו עושים את זה באמצעות מהדר מאגר אחסון לפרוטוקולים 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() של צופה התגובה כדי לציין שסיימנו לטפל ב-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. מתקשרים אל start() כדי להפעיל שרת RPC בשביל השירות שלנו.
  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 לא חוסם/אסינכרוני שמבצע קריאות לא חוסמות לשרת, שבהן התגובה מוחזרת באופן אסינכרוני. אפשר לבצע סוגים מסוימים של שיחות סטרימינג רק באמצעות ה-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 מקומית.

אנחנו יוצרים ומאכלסים אובייקט של מאגר אחסון לפרוטוקולים לבקשה (במקרה שלנו 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. המאמרים הבאים