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

۱. مقدمه

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

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

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

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

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

  • نحوه استفاده از بافرهای پروتکل برای تعریف یک API سرویس.
  • نحوه ساخت یک کلاینت و سرور مبتنی بر gRPC از تعریف Protocol Buffers با استفاده از تولید خودکار کد.
  • آشنایی با ارتباطات کلاینت-سرور با 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/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-rust-getting-started/start_here

روش دیگر این است که فایل .zip که فقط شامل دایرکتوری codelab است را دانلود کرده و به صورت دستی آن را از حالت فشرده خارج کنید.

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

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

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

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

این متد RPC و انواع پیام‌های آن، همگی در فایل proto/route_guide.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/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;
}

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

ما قبلاً کد تولید شده از فایل .proto در دایرکتوری تولید شده را به شما داده‌ایم.

مانند هر پروژه دیگری، باید به وابستگی‌های ضروری برای کد خود فکر کنیم. برای پروژه‌های Rust، وابستگی‌ها در Cargo.toml قرار خواهند گرفت. ما قبلاً وابستگی‌های ضروری را در فایل Cargo.toml فهرست کرده‌ایم.

اگر می‌خواهید نحوه‌ی تولید کد از فایل .proto را خودتان یاد بگیرید، به این دستورالعمل‌ها مراجعه کنید.

کد تولید شده شامل موارد زیر است:

  • تعاریف ساختار برای انواع پیام Point و Feature .
  • یک ویژگی سرویس که باید پیاده‌سازی کنیم: route_guide_server::RouteGuide .
  • یک نوع کلاینت که برای فراخوانی سرور از آن استفاده خواهیم کرد: route_guide_client::RouteGuideClient<T> .

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

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

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

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

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

در این بخش، به ایجاد یک کلاینت 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(())
}

در این تابع، وقتی کلاینت را ایجاد می‌کنیم، کانال عمومی ایجاد شده در بالا را با قطعه کد تولید شده که متدهای خاص تعریف شده در سرویس .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(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(())
}

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

ابتدا، برای اجرای کلاینت و سرور، آنها را به عنوان اهداف دودویی به جعبه خود اضافه می‌کنیم. باید 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

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