شروع کار با gRPC-Rust

۱. مقدمه

در این آزمایشگاه کد، شما از gRPC-Rust برای ایجاد یک کلاینت و سرور استفاده خواهید کرد که پایه و اساس یک برنامه مسیریابی نوشته شده با Rust را تشکیل می‌دهند.

در پایان این آموزش، شما یک کلاینت خواهید داشت که با استفاده از پیاده‌سازی رسمی Rust از پروتکل gRPC به یک سرور راه دور متصل می‌شود تا نام یا آدرس پستی یک مکان را در مختصات خاص روی نقشه دریافت کند. یک برنامه کامل ممکن است از این طراحی کلاینت-سرور برای شمارش یا خلاصه کردن نقاط مورد علاقه در طول یک مسیر استفاده کند.

این سرویس در یک فایل Protocol Buffers تعریف شده است که برای تولید کد تکراری برای کلاینت و سرور استفاده می‌شود تا بتوانند با یکدیگر ارتباط برقرار کنند و در زمان و تلاش شما برای پیاده‌سازی آن قابلیت صرفه‌جویی شود.

این کد تولید شده نه تنها پیچیدگی‌های ارتباط بین سرور و کلاینت، بلکه سریال‌سازی و از سریال‌زدایی داده‌ها را نیز برطرف می‌کند.

آنچه یاد خواهید گرفت

  • نحوه استفاده از بافرهای پروتکل برای تعریف یک API سرویس.
  • نحوه ساخت یک کلاینت مبتنی بر gRPC از تعریف پروتکل بافر با استفاده از تولید خودکار کد.
  • آشنایی با ارتباطات کلاینت-سرور با gRPC

این آزمایشگاه کد برای توسعه‌دهندگان Rust که تازه با gRPC آشنا شده‌اند یا به دنبال مرور gRPC هستند، یا هر کسی که به ساخت سیستم‌های توزیع‌شده علاقه‌مند است، مناسب است. هیچ تجربه قبلی gRPC لازم نیست.

۲. قبل از شروع

پیش‌نیازها

مطمئن شوید که موارد زیر را نصب کرده‌اید:

  • شورای همکاری خلیج فارس. دستورالعمل‌های اینجا را دنبال کنید.
  • گیت: دستورالعمل نصب اینجا .
  • Rust ، نسخه ۱.۸۸.۰. دستورالعمل‌های نصب را اینجا دنبال کنید.

کد را دریافت کنید

برای اینکه مجبور نباشید کاملاً از ابتدا شروع کنید، این codelab چارچوبی از کد منبع برنامه را برای تکمیل شما فراهم می‌کند. مراحل زیر نحوه تکمیل برنامه، از جمله استفاده از افزونه‌های کامپایلر بافر پروتکل برای تولید کد 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 است را دانلود کرده و به صورت دستی آن را از حالت فشرده خارج کنید.

اگر می‌خواهید از تایپ کردن پیاده‌سازی صرف‌نظر کنید، کد منبع تکمیل‌شده در گیت‌هاب موجود است.

۳. تعریف سرویس

اولین قدم شما تعریف سرویس gRPC برنامه، متد RPC آن و انواع پیام‌های درخواست و پاسخ آن با استفاده از Protocol Buffers است. سرویس شما موارد زیر را ارائه خواهد داد:

  • یک متد RPC به نام GetFeature که سرور پیاده‌سازی می‌کند و کلاینت آن را فراخوانی می‌کند.
  • انواع پیام Point و Feature هستند که ساختارهای داده‌ای هستند که هنگام استفاده از متد GetFeature بین کلاینت و سرور رد و بدل می‌شوند. کلاینت مختصات نقشه را به عنوان یک Point در درخواست GetFeature خود به سرور ارائه می‌دهد و سرور با یک Feature مربوطه که هر آنچه را که در آن مختصات قرار دارد توصیف می‌کند، پاسخ می‌دهد.

این متد RPC و انواع پیام‌های آن، همگی در فایل proto/routeguide.proto از کد منبع ارائه شده تعریف خواهند شد.

بافرهای پروتکل معمولاً به عنوان protobufs شناخته می‌شوند. برای اطلاعات بیشتر در مورد اصطلاحات gRPC، به مفاهیم اصلی، معماری و چرخه حیات gRPC مراجعه کنید.

روش خدمات

بیایید ابتدا متدهای سرویس خود را تعریف کنیم و سپس انواع پیام‌های Point و Feature را تعریف کنیم. فایل proto/routeguide.proto دارای یک ساختار service به نام RouteGuide است که یک یا چند متد ارائه شده توسط سرویس برنامه را تعریف می‌کند.

متد GetFeature rpc را به تعریف 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;
}

۴. کد کلاینت و سرور را تولید کنید

ما قبلاً کد تولید شده از فایل .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 مراجعه کنید.

در مرحله بعد، متدهای سرویس را روی سرور پیاده‌سازی خواهیم کرد.

۵. پیاده‌سازی سرویس

در 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 از کد تولید شده خود پیاده‌سازی کنیم.

RPC ساده Unary

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. با فراخوانی تابع کمکی load() یک RouteGuideService با ویژگی‌های بارگذاری شده در آن ایجاد کنید.
  3. با استفاده از سرویسی که ایجاد کردیم، یک نمونه از سرور gRPC با استفاده از RouteGuideServer::new() ایجاد کنید.
  4. پیاده‌سازی سرویس خود را در سرور gRPC ثبت کنید.
  5. تابع serve() روی سرور با جزئیات پورت خود فراخوانی کنید تا یک انتظار مسدودکننده تا زمان خاتمه فرآیند انجام شود.

۶. مشتری را ایجاد کنید

در این بخش، به ایجاد یک کلاینت 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::builder() یک Channel ایجاد می‌کنیم و آدرس سرور ( 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) ارسال می‌کنیم. انتظار برای مقدار Future برگشتی، پاسخ 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(())
}

۷. امتحانش کنید

برای اجرای کلاینت و سرور خود، ابتدا تأیید کنید که هر دو هدف دودویی در 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

۸. قدم بعدی چیست؟

۹. مشارکت‌کنندگان در این آزمایشگاه کد

  • کتی ژائو
  • لوسیو فرانکو
  • اروند برایت
  • ناتانیل فورد