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

1. מבוא

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

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

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

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

מה תלמדו

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

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

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

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

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

  • GCC. פועלים לפי ההוראות כאן.
  • ‫Git: הוראות התקנה כאן.
  • חלודה, גרסה 1.88.0. פעל לפי הוראות ההתקנה כאן.

קבל את הקוד

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

ראשית, צרו את ספריית העבודה של codelab ותנו לתוכה את הקוד: cd:

mkdir grpc-rust-getting-started && cd grpc-rust-getting-started

הורד וחלץ את קוד המעבדה:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/2026.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-2026/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/routeguide.proto של קוד המקור שסופק.

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

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 בספריית generated/, כולל כל התוספות שביצעת לעיל. עם זאת, נרצה להקדיש רגע להסבר כיצד יצירת הקוד עובדת.

קובץ ה-.proto שלנו מתאר את כל המבנים והפונקציות שבהם משתמש לקוח או שרת. אנו משתמשים בסקריפט בנייה של Cargo (build.rs) יחד עם הארגז של grpc-protobuf-build כדי ליצור קוד זה באופן אוטומטי.

ב-Cargo.toml נוסיף grpc-protobuf-build מתחת ל-[build-dependencies].

ב-build.rs, אנו מגדירים את grpc_protobuf_build::CodeGen לקמפל את proto/routeguide.proto לתוך ספריית generated/. השורות המרכזיות נמצאות כאן:

grpc_protobuf_build::CodeGen::new()
    .include("proto")
    .input("routeguide.proto")
    .output_dir("generated")
    .compile()
    .unwrap();

הפקודה הזו מפעילה את יצירת הקוד של תיבת grpc_protobuf_build, ומעבירה לה את routeguide.proto. הוספנו את זה לקוד כדי שהפעולה תתבצע רק כשמועבר feature flag, כך שההגדרה תתבצע מחדש רק כשרוצים בכך. אין צורך להריץ את הפקודה הזו עכשיו, כי כבר יצרנו בשבילך את הקוד.

cargo build --bin routeguide-server --features regenerate_proto

כשמריצים את הפקודה cargo build, ‏ build.rs מהדר את ההגדרות של מאגר אחסון לפרוטוקולים בספרייה generate/, כולל:

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

מידע נוסף זמין במדריך בנושא protoc-gen-rust-grpc.

בשלב הבא, נטמיע את שיטות השירות בשרת.

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

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

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

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 מהקוד שנוצר.

Simple 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()))
    }
}

אחרי שמטמיעים את השיטה הזו, צריך גם להפעיל שרת 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_proto! של gRPC ולייבא את הסוג RouteGuideClient.

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

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

שיטות שירות התקשרות

ב-gRPC-Rust, קבצי RPC הם אסינכרוניים ואינם חוסמים, ומשתמשים בתחביר async/await של Rust כדי להמתין לתגובות מהשרת.

כדי לקרוא למתודות שירות, ראשית אנו יוצרים Channel באמצעות Channel::builder(), תוך ציון כתובת השרת (dns:///[::1]:10000) ופרטי החיבור (LocalChannelCredentials). לאחר מכן אנו מעבירים את הערוץ ל-RouteGuideClient::new() כדי ליצור מופע של הלקוח שלנו:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a new gRPC channel
    let channel = Channel::builder(
        "dns:///[::1]:10000",
        Arc::new(LocalChannelCredentials::new()),
    )
    .build();

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



}

בפונקציה זו, RouteGuideClient::new() קושר את הערוץ הגנרי לקובץ הלקוח שנוצר שמיישם את המתודות שהוגדרו בהגדרת השירות שלנו ב-.proto.

RPC פשוט

קריאה לפונקציה RPC פשוטה GetFeature היא פשוטה כמו קריאה למתודה מקומית. הוסף את זה ב-main():

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

ב-gRPC-Rust, אנו מעבירים את הודעת protobuf Point ישירות ל-client.get_feature(point). המתנה לעתיד המוחזר מניבה את תגובת Feature ישירות, ללא צורך בקריאות נוספות למתודה.

לאחר מכן, הדפיסו את השדות מהתגובה:

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>> {
    // 1. Create a new gRPC channel
    let channel = Channel::builder(
        "dns:///[::1]:10000",
        Arc::new(LocalChannelCredentials::new()),
    )
    .build();

    // 2. Instantiate the generated RouteGuideClient
    let client = RouteGuideClient::new(channel);

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

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

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

כדי להפעיל את הלקוח והשרת, תחילה ודא ששני היעדים הבינאריים מוגדרים ב-Cargo.toml:

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

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

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

  1. הפעל את השרת בטרמינל אחד:
cargo run --bin routeguide-server
  1. הפעל את הלקוח מטרמינל אחר:
cargo run --bin routeguide-client

תראה פלט כזה, עם השמטת חותמות זמן לשם הבהירות:

*** SIMPLE RPC ***
Response = Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Latitude = 409146138, Longitude = -746188906

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

9. תורמים ל-Codelab הזה

  • קאתי ז'או
  • לוצ'יו פרנקו
  • ארווינד ברייט
  • נתנאל פורד