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

1. מבוא

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

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

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

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

מה תלמדו

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

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

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

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

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

  • GCC. פועלים לפי ההוראות כאן
  • Rust, גרסה 1.89.0. הוראות ההתקנה מפורטות כאן.

קבל את הקוד

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

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

mkdir grpc-rust-getting-started && cd grpc-rust-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-rust-getting-started/start_here

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

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

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

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

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

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

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

שיטת השירות

קודם נגדיר את שיטות השירות ואז נגדיר את סוגי ההודעות Point ו-Feature. לקובץ proto/routeguide.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 פשוט שבו הלקוח שולח בקשה לשרת ומחכה לתגובה, בדיוק כמו קריאה לפונקציה מקומית.

סוגי הודעות

בקובץ proto/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;
}

4. יצירת קוד הלקוח והשרת

כבר נתנו לך את הקוד שנוצר מהקובץ .proto בספרייה שנוצרה.

כמו בכל פרויקט, אנחנו צריכים לחשוב על התלות שנדרשת לקוד שלנו. בפרויקטים של Rust, יחסי התלות יהיו ב-Cargo.toml. כבר ציינו את התלויות הנדרשות בקובץ Cargo.toml.

אם אתם רוצים ללמוד איך ליצור קוד מקובץ .proto בעצמכם, תוכלו לעיין בהוראות האלה.

הקוד שנוצר מכיל:

  • הגדרות של מבנים לסוגי ההודעות Point ו-Feature.
  • מאפיין שירות שנצטרך להטמיע: route_guide_server::RouteGuide.
  • סוג הלקוח שבו נשתמש כדי לקרוא לשרת: route_guide_client::RouteGuideClient<T>.

בשלב הבא, נטמיע את השיטה GetFeature בצד השרת, כדי שכשהלקוח ישלח בקשה, השרת יוכל להשיב עם תשובה.

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

ב-src/server/server.rs, אפשר להוסיף את הקוד שנוצר להיקף באמצעות מאקרו include_generated_proto! של gRPC ולייבא את המאפיין RouteGuide ואת Point.

mod grpc_pb {
    grpc::include_generated_proto!("generated", "routeguide");
}

pub use grpc_pb::{
    route_guide_server::{RouteGuideServer, RouteGuide},
    Point, Feature,
};

אפשר להתחיל בהגדרת מבנה נתונים (struct) לייצוג השירות. אפשר לעשות את זה ב-src/server/server.rs לעת עתה:

#[derive(Debug)]
pub struct RouteGuideService {
    features: Vec<Feature>,
}

עכשיו צריך להטמיע את מאפיין route_guide_server::RouteGuide מהקוד שנוצר.

Unary RPC

השיטה RouteGuideService מטמיעה את כל שיטות השירות שלנו. הפונקציה get_feature בצד השרת היא המקום שבו מתבצעת העבודה העיקרית: היא מקבלת הודעת Point מהלקוח ומחזירה בהודעת Feature את פרטי המיקום התואמים מתוך רשימה של מקומות מוכרים. הנה ההטמעה של הפונקציה ב-src/server/server.rs:

#[tonic::async_trait]
impl RouteGuide for RouteGuideService {
    async fn get_feature(&self, request: Request<Point>) -> Result<Response<Feature>, Status> {
        println!("GetFeature = {:?}", request);
        let requested_point = request.get_ref();
        for feature in self.features.iter() {
            if feature.location().latitude() == requested_point.latitude() {
                if feature.location().longitude() == requested_point.longitude(){
                    return Ok(Response::new(feature.clone()))
                };
            };    
        }
        Ok(Response::new(Feature::default()))
    }
}

בשיטה, מאכלסים אובייקט Feature במידע המתאים ל-Point הנתון, ואז מחזירים אותו.

אחרי שמטמיעים את השיטה הזו, צריך גם להפעיל שרת gRPC כדי שהלקוחות יוכלו להשתמש בשירות. מחליפים את main() בזה.

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:10000".parse().unwrap();
    println!("RouteGuideServer listening on: {addr}");
    let route_guide = RouteGuideService {
        features: load(),
    };
    let svc = RouteGuideServer::new(route_guide);
    Server::builder().add_service(svc).serve(addr).await?;
    Ok(())
}

זה מה שקורה באזור main(), שלב אחר שלב:

  1. מציינים את היציאה שבה רוצים להשתמש כדי להאזין לבקשות של לקוחות
  2. יצירת RouteGuideService עם תכונות שנטענו על ידי הפעלת פונקציית העזר load()
  3. יוצרים מופע של שרת gRPC באמצעות RouteGuideServer::new() באמצעות השירות שיצרנו.
  4. רושמים את הטמעת השירות בשרת gRPC.
  5. מתקשרים אל serve() בשרת עם פרטי היציאה שלנו כדי לבצע המתנה חוסמת עד שהתהליך יופסק.

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

בקטע הזה נראה איך ליצור לקוח Rust לשירות RouteGuide ב-src/client/client.rs.

כמו שעשינו ב-src/server/server.rs, אפשר להוסיף את הקוד שנוצר להיקף באמצעות מאקרו include_generated_code! של gRPC ולייבא את הסוג RouteGuideClient.

mod grpc_pb {
    grpc::include_generated_proto!("generated", "routeguide");
}

use grpc_pb::{
    route_guide_client::RouteGuideClient,
    Point,
};

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

ב-gRPC-Rust, קריאות RPC פועלות במצב חסימה/סינכרוני, כלומר קריאת ה-RPC מחכה שהשרת יגיב, ותחזיר תגובה או שגיאה.

כדי להפעיל שיטות שירות, קודם צריך ליצור ערוץ לתקשורת עם השרת. כדי ליצור את הקישור הזה, קודם יוצרים נקודת קצה, מתחברים לנקודת הקצה הזו ומעבירים את הערוץ שנוצר כשמתחברים אל RouteGuideClient::new() באופן הבא:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create endpoint to connect to
    let endpoint = Endpoint::new("http://[::1]:10000")?; 
    let channel = endpoint.connect().await?;             

    // Create a new client
    let mut client = RouteGuideClient::new(channel);
    Ok(())
}

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

Simple RPC

הפעלת ה-RPC הפשוט GetFeature היא כמעט פשוטה כמו הפעלת method מקומי. תעשה את זה בmain().

println!("*** SIMPLE RPC ***");
let point = proto!(Point{
    latitude: 409_146_138,
    longitude: -746_188_906
});
let response = client
    .get_feature(Request::new(point))
    .await?.into_inner();
Ok(())

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

println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
    response.name(),
    response.location().latitude(),
    response.location().longitude());

בסך הכול, הפונקציה main() של הלקוח צריכה להיראות כך:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    //Create endpoint to connect to
    let endpoint = Endpoint::new("http://[::1]:10000")?; 
    let channel = endpoint.connect().await?;             

    // Create a new client
    let mut client = RouteGuideClient::new(channel); 

    println!("*** SIMPLE RPC ***");
    let point = proto!(Point{
        latitude: 409_146_138,
        longitude: -746_188_906
    });
    let response = client
        .get_feature(Request::new(point))
        .await?.into_inner();

    println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
        response.name(),
        response.location().latitude(),
        response.location().longitude());
    Ok(())
}

7. רוצה לנסות?

קודם כול, כדי להריץ את הלקוח והשרת, נוסיף אותם כיעדים בינאריים ל-crate. אנחנו צריכים לערוך את Cargo.toml בהתאם ולהוסיף את הפרטים הבאים:

[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"

[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"

לאחר מכן, מריצים את הפקודות הבאות מספריית העבודה:

  1. מריצים את השרת במסוף אחד:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server 
  1. מריצים את הלקוח ממסוף אחר:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client

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

*** SIMPLE RPC ***

FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906

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