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

1. مقدمه

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

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

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

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

چیزی که یاد خواهید گرفت

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

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

2. قبل از شروع

پیش نیازها

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

  • شورای همکاری خلیج فارس دستورالعمل های اینجا را دنبال کنید
  • Rust نسخه 1.89.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/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 کد منبع ارائه شده تعریف می شوند.

بافرهای پروتکل معمولاً به عنوان پروتوباف شناخته می شوند. برای اطلاعات بیشتر در مورد اصطلاحات 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 نشان دهنده یک جفت مختصات طول و عرض جغرافیایی بر روی نقشه است. برای این کد، از اعداد صحیح برای مختصات استفاده کنید:

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! ماکرو و ویژگی RouteGuide و Point را وارد کنید.

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

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

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

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

در این بخش، ما به ایجاد یک مشتری Rust برای سرویس RouteGuide در src/client/client.rs نگاه خواهیم کرد.

همانطور که در src/server/server.rs انجام دادیم، می‌توانیم کد تولید شده را از طریق include_generated_code! ماکرو و نوع 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(())

همانطور که می بینید، ما روشی را که در ابتدا دریافت کردیم، فراخوانی می کنیم. در پارامترهای روش خود، یک شی بافر پروتکل درخواست (در مورد ما 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. آن را امتحان کنید

ابتدا، برای اجرای Client و Server، بیایید آنها را به عنوان اهداف باینری به جعبه خود اضافه کنیم. ما باید 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. بعدی چه خواهد شد