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

۱. مقدمه

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

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

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

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

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

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

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

۲. قبل از شروع

پیش‌نیازها

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

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

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

برای اینکه مجبور نباشید کاملاً از ابتدا شروع کنید، این codelab چارچوبی از کد منبع برنامه را برای تکمیل شما فراهم می‌کند. مراحل زیر نحوه تکمیل برنامه، از جمله استفاده از افزونه‌های کامپایلر بافر پروتکل برای تولید کد gRPC قالب‌بندی شده را به شما نشان می‌دهد.

ابتدا، دایرکتوری کاری codelab را ایجاد کنید و cd به آن وارد شوید:

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

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

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

۳. تعریف پیام‌ها و خدمات

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

  • متدهای RPC به نام‌های ListFeatures ، RecordRoute و RouteChat که سرور پیاده‌سازی می‌کند و کلاینت آنها را فراخوانی می‌کند.
  • انواع پیام Point ، Feature ، Rectangle ، RouteNote و RouteSummary هستند که ساختارهای داده‌ای هستند که هنگام فراخوانی متدهای بالا بین کلاینت و سرور رد و بدل می‌شوند.

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

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

تعریف انواع پیام

بیایید ابتدا پیام‌هایی را که توسط RPC های ما استفاده خواهند شد، تعریف کنیم. در فایل routeguide/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;
}

سپس یک پیام Rectangle که یک مستطیل عرض-طول جغرافیایی را نشان می‌دهد، به صورت دو نقطه مورب روبروی هم "lo" و "hi" نمایش داده می‌شود.

message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;

  // The other corner of the rectangle.
  Point hi = 2;
}

همچنین یک پیام RouteNote که نشان دهنده پیامی است که در یک نقطه معین ارسال شده است.

message RouteNote {
  // The location from which the message is sent.
  Point location = 1;

  // The message to be sent.
  string message = 2;
}

ما همچنین به یک پیام RouteSummary نیاز داریم. این پیام در پاسخ به یک RecordRoute RPC دریافت می‌شود که در بخش بعدی توضیح داده شده است. این پیام شامل تعداد نقاط دریافت شده، تعداد ویژگی‌های شناسایی شده و کل مسافت طی شده به عنوان مجموع تجمعی فاصله بین هر نقطه است.

message RouteSummary {
  // The number of points received.
  int32 point_count = 1;

  // The number of known features passed while traversing the route.
  int32 feature_count = 2;

  // The distance covered in metres.
  int32 distance = 3;

  // The duration of the traversal in seconds.
  int32 elapsed_time = 4;
}

تعریف روش‌های سرویس

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

متدهای RPC را در تعریف سرویس خود تعریف کنید و انواع درخواست و پاسخ آنها را مشخص کنید. در این بخش از codelab، بیایید موارد زیر را تعریف کنیم:

ویژگی‌ها

Feature موجود در Rectangle داده شده را دریافت می‌کند. نتایج به جای اینکه به طور همزمان بازگردانده شوند، به صورت جریانی (streamed) ارسال می‌شوند (مثلاً در یک پیام پاسخ با یک فیلد تکراری)، زیرا مستطیل ممکن است ناحیه بزرگی را پوشش دهد و شامل تعداد زیادی ویژگی باشد.

یک نوع مناسب برای این RPC، RPC استریمینگ سمت سرور است: کلاینت درخواستی را به سرور ارسال می‌کند و یک استریم برای خواندن دنباله ای از پیام‌ها دریافت می‌کند. کلاینت از استریم برگشتی می‌خواند تا زمانی که دیگر پیامی وجود نداشته باشد. همانطور که در مثال ما می‌بینید، شما با قرار دادن کلمه کلیدی stream قبل از نوع پاسخ، یک روش استریمینگ سمت سرور را مشخص می‌کنید.

rpc ListFeatures(Rectangle) returns (stream Feature) {}

رکوردروت

جریانی از Point ) را در مسیری که پیمایش می‌شود، می‌پذیرد و پس از اتمام پیمایش، یک RouteSummary برمی‌گرداند.

در این مورد، یک RPC استریمینگ سمت کلاینت مناسب به نظر می‌رسد: کلاینت دنباله ای از پیام‌ها را می‌نویسد و آنها را دوباره با استفاده از یک استریم ارائه شده به سرور ارسال می‌کند. پس از اینکه کلاینت نوشتن پیام‌ها را تمام کرد، منتظر می‌ماند تا سرور همه آنها را بخواند و پاسخ خود را برگرداند. شما با قرار دادن کلمه کلیدی stream قبل از نوع درخواست، یک روش استریمینگ سمت کلاینت را مشخص می‌کنید.

rpc RecordRoute(stream Point) returns (RouteSummary) {}

روت‌چت

جریانی از RouteNote های ارسالی را هنگام پیمایش یک مسیر می‌پذیرد، در حالی که RouteNote های دیگری (مثلاً از سایر کاربران) دریافت می‌کند.

این دقیقاً همان نوع کاربرد استریمینگ دوطرفه است. یک RPC استریمینگ دوطرفه، هر دو طرف را وادار می‌کند تا با استفاده از یک استریم خواندنی-نوشتنی، دنباله‌ای از پیام‌ها را ارسال کنند. این دو استریم به‌طور مستقل عمل می‌کنند، بنابراین کلاینت‌ها و سرورها می‌توانند به هر ترتیبی که دوست دارند، بخوانند و بنویسند.

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

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

rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

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

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

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

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

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

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

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

ابتدا بیایید نگاهی به نحوه ایجاد یک سرور RouteGuide بیندازیم. برای اینکه سرویس RouteGuide ما کارش را انجام دهد، دو بخش وجود دارد:

  • پیاده‌سازی رابط سرویس تولید شده از تعریف سرویس ما: انجام "کار" واقعی سرویس ما.
  • اجرای یک سرور gRPC برای گوش دادن به درخواست‌های کلاینت‌ها و ارسال آنها به پیاده‌سازی روش صحیح.

در 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, Rectangle, RouteNote, RouteSummary
};

می‌توانیم با تعریف یک struct برای نمایش سرویس خود شروع کنیم. فعلاً می‌توانیم این کار را در src/server/server.rs انجام دهیم:

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

حالا باید ویژگی route_guide_server::RouteGuide از کد تولید شده خود پیاده‌سازی کنیم.

پیاده‌سازی RouteGuide

ما باید رابط RouteGuide تولید شده را پیاده‌سازی کنیم. پیاده‌سازی به این شکل خواهد بود. این مورد از قبل در قالب وجود دارد.

#[tonic::async_trait]
impl RouteGuide for RouteGuideService {
    async fn list_features(
        &self,
        request: Request<Rectangle>,
    ) -> Result<Response<ListFeaturesStream>, Status> {
        ...
    }

    async fn record_route(
        &self,
        request: Request<tonic::Streaming<Point>>,
    ) -> Result<Response<RouteSummary>, Status> {
        ...
    }

    async fn route_chat(
        &self,
        request: Request<tonic::Streaming<RouteNote>>,
    ) -> Result<Response<RouteChatStream>, Status> {
        ...
    }
}

بیایید هر پیاده‌سازی RPC را با جزئیات بررسی کنیم.

RPC استریمینگ سمت سرور

بیایید با ListFeatures شروع کنیم. این یک RPC استریمینگ سمت سرور است، بنابراین باید چندین Feature را به کلاینت خود ارسال کنیم.

async fn list_features(
        &self,
        request: Request<Rectangle>,
    ) -> Result<Response<ListFeaturesStream>, Status> {
        println!("ListFeatures = {:?}", request);

        let (tx, rx) = mpsc::channel(4);
        let features = self.features.clone();

        tokio::spawn(async move {
            for feature in &features[..] {
                if in_range(&feature.location().to_owned(), request.get_ref()) {
                    println!("  => send {feature:?}");
                    tx.send(Ok(feature.clone())).await.unwrap();
                }
            }
            println!(" /// done sending");
        });

        let output_stream = ReceiverStream::new(rx);
        Ok(Response::new(Box::pin(output_stream)))
    }

همانطور که می‌بینید، ما یک شیء درخواست ( Rectangle که کلاینت ما می‌خواهد Features را در آن پیدا کند) دریافت می‌کنیم. این بار، باید جریانی از مقادیر را برگردانیم. ما یک کانال ایجاد می‌کنیم و یک وظیفه ناهمزمان جدید ایجاد می‌کنیم که در آن یک جستجو انجام می‌دهیم و ویژگی‌هایی را که محدودیت‌های ما را برآورده می‌کنند به کانال ارسال می‌کنیم. نیمه جریان کانال به فراخواننده بازگردانده می‌شود، که در یک tonic::Response پیچیده شده است.

RPC استریمینگ سمت کلاینت

حالا بیایید به چیزی کمی پیچیده‌تر نگاه کنیم: متد استریمینگ سمت کلاینت RecordRoute ، که در آن یک جریان از Points از کلاینت دریافت می‌کنیم و یک RouteSummary واحد با اطلاعات مربوط به سفر آنها برمی‌گردانیم. این متد یک جریان به عنوان ورودی دریافت می‌کند که سرور می‌تواند از آن برای خواندن و نوشتن پیام‌ها استفاده کند. می‌تواند با استفاده از متد next() خود، پیام‌های کلاینت را پیمایش کند و پاسخ واحد خود را بازگرداند.

async fn record_route(
        &self,
        request: Request<tonic::Streaming<Point>>,
    ) -> Result<Response<RouteSummary>, Status> {
        println!("RecordRoute");
        let mut stream = request.into_inner();
        let mut summary = RouteSummary::default();
        let mut last_point = None;
        let now = Instant::now();

        while let Some(point) = stream.next().await {
            let point = point?;
            println!("  ==> Point = {point:?}");

            // Increment the point count
            summary.set_point_count(summary.point_count() + 1);

            // Find features
            for feature in &self.features[..] {
                if feature.location().latitude() == point.latitude() {
                    if feature.location().longitude() == point.longitude(){
                        summary.set_feature_count(summary.feature_count() + 1);
                    }
                }
            }

            // Calculate the distance
            if let Some(ref last_point) = last_point {
                let new_dist = summary.distance() + calc_distance(last_point, &point);
                summary.set_distance(new_dist);
            }
            last_point = Some(point);
        }
        summary.set_elapsed_time(now.elapsed().as_secs() as i32);
        Ok(Response::new(summary))
    }

در بدنه متد، ما از متد next() در استریم برای خواندن مکرر درخواست‌های کلاینت خود به یک شیء درخواست (در این مورد یک Point ) استفاده می‌کنیم تا زمانی که دیگر پیامی وجود نداشته باشد. اگر این مقدار None باشد، استریم هنوز در وضعیت خوبی است و می‌تواند به خواندن ادامه دهد.

RPC استریمینگ دوطرفه

در نهایت، بیایید نگاهی به RPC استریمینگ دوطرفه RouteChat() خود بیندازیم.

async fn route_chat(
        &self,
        request: Request<tonic::Streaming<RouteNote>>,
    ) -> Result<Response<RouteChatStream>, Status> {
        println!("RouteChat");

        let mut notes: HashMap<(i32, i32), Vec<RouteNote>> = HashMap::new();
        let mut stream = request.into_inner();

        let output = async_stream::try_stream! {
            while let Some(note) = stream.next().await {
                let note = note?;
                let location = note.location();
                let key = (location.latitude(), location.longitude());
                let location_notes = notes.entry(key).or_insert(vec![]);
                location_notes.push(note);
                for note in location_notes {
                    yield note.clone();
                }
            }
        };
        Ok(Response::new(Box::pin(output)))
    }

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

ما جریان output را با استفاده از try_stream! ایجاد می‌کنیم، که نشان می‌دهد جریان می‌تواند خطاها را برگرداند.

سرور را شروع کنید

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

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

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

ابتدا، کد تولید شده را در محدوده‌ی عملکرد (scope) قرار دهید.

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

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

روش‌های سرویس تماس

حالا بیایید نگاهی به نحوه فراخوانی متدهای سرویس خود بیندازیم. در gRPC-Rust، RPCها در حالت مسدودکننده/همزمان عمل می‌کنند، به این معنی که فراخوانی RPC منتظر پاسخ سرور می‌ماند و یا پاسخی را برمی‌گرداند یا خطایی را نشان می‌دهد.

RPC استریمینگ سمت سرور

اینجا جایی است که ما متد استریمینگ سمت سرور ListFeatures فراخوانی می‌کنیم، که جریانی از اشیاء Feature جغرافیایی را برمی‌گرداند.

async fn print_features(client: &mut RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
    let rectangle = proto!(Rectangle {
        lo: proto!(Point {
            latitude: 400_000_000,
            longitude: -750_000_000,
        }),
        hi: proto!(Point {
            latitude: 420_000_000,
            longitude: -730_000_000,
        }),
    });

    let mut stream = client
        .list_features(Request::new(rectangle))
        .await?
        .into_inner();

    while let Some(feature) = stream.message().await? {
        println!("FEATURE: Name = \"{}\", Lat = {}, Lon = {}",
            feature.name(),
            feature.location().latitude(),
            feature.location().longitude());
        }
    Ok(())
}

ما یک درخواست به متد ارسال می‌کنیم و یک نمونه از ListFeaturesStream دریافت می‌کنیم. کلاینت می‌تواند از استریم ListFeaturesStream برای خواندن پاسخ‌های سرور استفاده کند. ما از متد message() در ListFeaturesStream برای خواندن مکرر پاسخ‌های سرور به یک شیء بافر پروتکل پاسخ (در این مورد یک Feature ) استفاده می‌کنیم تا زمانی که دیگر پیامی وجود نداشته باشد.

RPC استریمینگ سمت کلاینت

در اینجا برای record_route ، ما یک بردار از نقاط را به یک جریان تبدیل می‌کنیم. سپس این جریان را به عنوان یک درخواست به record_route() ارسال می‌کنیم و پس از پردازش کامل جریان توسط سرور، یک پاسخ RouteSummary دریافت می‌کنیم.

async fn run_record_route(client: &mut RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
    let mut rng = rand::rng();
    let point_count: i32 = rng.random_range(2..100);

    let mut points = vec![];
    for _ in 0..=point_count {
        points.push(random_point(&mut rng))
    }

    println!("Traversing {} points", points.len());
    let request = Request::new(tokio_stream::iter(points));

    match client.record_route(request).await {
        Ok(response) => {
            let response = response.into_inner();
            println!("SUMMARY: Feature Count = {}, Distance = {}", response.feature_count(), response.distance())},
        Err(e) => println!("something went wrong: {e:?}"),
    }

    Ok(())
}

RPC استریمینگ دوطرفه

در نهایت، بیایید نگاهی به استریمینگ دوطرفه RPC خود RouteChat() بیندازیم. ما یک درخواست استریم که در آن می‌نویسیم را به متد ارسال می‌کنیم و استریمی را که می‌توانیم پیام‌ها را از آن بخوانیم، برمی‌گردانیم. این بار در حالی که سرور هنوز در حال نوشتن پیام‌ها در استریم پیام‌های خود است، مقادیر را از طریق استریم متد خود برمی‌گردانیم.

async fn run_route_chat(client: &mut RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
    let start = time::Instant::now();
    let outbound = async_stream::stream! {
        let mut interval = time::interval(Duration::from_secs(1));
        for _ in 0..10 {
            let time = interval.tick().await;
            let elapsed = time.duration_since(start);
            let note = proto!(RouteNote {
                location: proto!(Point {
                    latitude: 409146138 + elapsed.as_secs() as i32,
                    longitude: -746188906,
                }),
                message: format!("at {elapsed:?}"),
            });
            yield note;
        }
    };
    let response = client.route_chat(Request::new(outbound)).await?;
    let mut inbound = response.into_inner();
    while let Some(note) = inbound.message().await? {
        println!("Note: Latitude = {}, Longitude = {}, Message = \"{}\"",
            note.location().latitude(),
            note.location().longitude(),
            note.message());
        }
    Ok(())
}

اگرچه هر طرف همیشه پیام‌های طرف دیگر را به ترتیبی که نوشته شده‌اند دریافت می‌کند، اما هم کلاینت و هم سرور می‌توانند به هر ترتیبی بخوانند و بنویسند - جریان‌ها کاملاً مستقل عمل می‌کنند.

فراخوانی متدهای کمکی

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

در 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!("\n*** SERVER STREAMING ***");
    print_features(&mut client).await?;

    println!("\n*** CLIENT STREAMING ***");
    run_record_route(&mut client).await?;

    println!("\n*** BIDIRECTIONAL STREAMING ***");
    run_route_chat(&mut client).await?;

    Ok(())
}

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

ابتدا، برای اجرای کلاینت و سرور، آنها را به عنوان اهداف دودویی به جعبه خود اضافه می‌کنیم. باید Cargo.toml خود را بر این اساس ویرایش کنیم:

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

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

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

سپس، دستورات زیر را از دایرکتوری‌های کاری خود اجرا کنید:

  1. سرور را در یک ترمینال اجرا کنید:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server 
  1. کلاینت را از یک ترمینال دیگر اجرا کنید:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client

خروجی مانند این را خواهید دید:

*** SERVER STREAMING ***
FEATURE: Name = "Patriots Path, Mendham, NJ 07945, USA", Lat = 407838351, Lon = -746143763
FEATURE: Name = "101 New Jersey 10, Whippany, NJ 07981, USA", Lat = 408122808, Lon = -743999179
FEATURE: Name = "U.S. 6, Shohola, PA 18458, USA", Lat = 413628156, Lon = -749015468
...
*** CLIENT STREAMING ***
Traversing 86 points
SUMMARY: Feature Count = 0, Distance = 803709356

*** BIDIRECTIONAL STREAMING ***
Note: Latitude = 409146138, Longitude = -746188906, Message = "at 112.45µs"
Note: Latitude = 409146139, Longitude = -746188906, Message = "at 1.00011245s"
Note: Latitude = 409146140, Longitude = -746188906, Message = "at 2.00011245s"

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