Getting Started with gRPC-Rust - Streaming

1. مقدمة

في هذا الدرس التطبيقي حول الترميز، ستستخدم gRPC-Rust لإنشاء عميل وخادم يشكّلان الأساس لتطبيق يحدّد المسارات مكتوب بلغة Rust.

في نهاية هذا الدليل التعليمي، سيكون لديك عميل يتصل بخادم بعيد باستخدام gRPC للحصول على معلومات حول الميزات على مسار العميل، وإنشاء ملخّص لمسار العميل، وتبادل معلومات المسار، مثل آخر الأخبار عن حركة المرور، مع الخادم والعملاء الآخرين.

يتم تحديد الخدمة في ملف بتنسيق Protocol Buffers، وسيتم استخدام هذا الملف لإنشاء رمز نص نموذجي للعميل والخادم حتى يتمكّنا من التواصل مع بعضهما البعض، ما يوفّر عليك الوقت والجهد في تنفيذ هذه الوظيفة.

لا يهتم هذا الرمز الذي تم إنشاؤه بتعقيدات الاتصال بين الخادم والعميل فحسب، بل أيضًا بتسلسل البيانات وإلغاء تسلسلها.

ماذا ستتعلّم؟

  • كيفية استخدام "مخازن البروتوكولات المؤقتة" (Protocol Buffers) لتحديد واجهة برمجة تطبيقات الخدمة
  • كيفية إنشاء عميل وخادم يستندان إلى gRPC استنادًا إلى "مخازن البروتوكولات المؤقتة" من خلال إنشاء الرموز البرمجية آليًا
  • فهم عملية التواصل بين العميل والخادم باستخدام gRPC

هذا الدرس التطبيقي حول الترميز موجّه لمطوّري Rust المبتدئين في gRPC أو الذين يريدون تجديد معلوماتهم في المجال، أو أي شخص آخر مهتم بتطوير أنظمة موزّعة. لا يُشترط توفّر خبرة سابقة في gRPC.

2. قبل البدء

المتطلبات الأساسية

تأكَّد من تثبيت ما يلي:

  • GCC. اتّبِع التعليمات هنا.
  • Rust، أحدث إصدار اتّبِع تعليمات التثبيت هنا.

الحصول على الشفرة‏

كي لا تضطر إلى البدء من الصفر تمامًا، يوفّر لك هذا الدرس التطبيقي حول الترميز بنية أساسية للرمز المصدر الخاص بالتطبيق لتتمكّن من إكماله. ستوضّح لك الخطوات التالية كيفية إكمال التطبيق، بما في ذلك استخدام مكوّنات برنامج تجميع مخازن البروتوكولات المؤقتة لإنشاء رمز gRPC النموذجي.

أولاً، أنشئ دليل عمل الدرس التطبيقي و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 الذي يحتوي على دليل الدرس العملي فقط وفك ضغطه يدويًا.

يتوفّر الرمز المصدر المكتمل على GitHub إذا كنت تريد تخطّي كتابة عملية التنفيذ.

3- تحديد الرسائل والخدمات

تتمثّل خطوتك الأولى في تحديد خدمة gRPC للتطبيق وطُرق استدعاء الإجراء عن بُعد (RPC) وأنواع رسائل الطلبات والردود باستخدام مخازن البروتوكولات المؤقتة. ستوفّر خدمتك ما يلي:

  • طُرق استدعاء الإجراء عن بُعد التي تحمل الأسماء ListFeatures وRecordRoute وRouteChat والتي ينفّذها الخادم ويستدعيها العميل
  • أنواع الرسائل Point وFeature وRectangle وRouteNote وRouteSummary، وهي عبارة عن بنى بيانات يتم تبادلها بين العميل والخادم عند استدعاء الطرق المذكورة أعلاه.

سيتم تحديد طرق "استدعاء الإجراء عن بُعد" هذه وأنواع الرسائل الخاصة بها في ملف 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) ضِمن تعريف الخدمة، مع تحديد أنواع الطلبات والردود. في هذا القسم من الدرس العملي، لنحدّد ما يلي:

ListFeatures

يحصل على Feature المتاحة ضمن Rectangle المحدّدة. يتم بث النتائج بدلاً من عرضها دفعة واحدة (مثلاً في رسالة ردّ تتضمّن حقلًا متكرّرًا)، لأنّ المستطيل قد يغطّي مساحة كبيرة ويحتوي على عدد كبير من العناصر.

النوع المناسب لهذا الإجراء عن بُعد هو إجراء عن بُعد للبث من جهة الخادم: يرسل العميل طلبًا إلى الخادم ويتلقّى بثًا لقراءة سلسلة من الرسائل. يقرأ العميل من البث الذي تم إرجاعه إلى أن لا تتبقى أي رسائل. كما هو موضّح في مثالنا، يمكنك تحديد طريقة البث من جهة الخادم من خلال وضع الكلمة الرئيسية stream قبل نوع الرد.

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

RecordRoute

تقبل هذه السمة سلسلة من Point على مسار يتم اجتيازه، وتعرض RouteSummary عند اكتمال عملية الاجتياز.

يبدو أنّ طلب إجراء مكالمة RPC ببث من جهة العميل مناسب في هذه الحالة: يكتب العميل سلسلة من الرسائل ويرسلها إلى الخادم، وذلك باستخدام بث يتم توفيره مرة أخرى. بعد أن ينتهي العميل من كتابة الرسائل، ينتظر أن يقرأها الخادم كلها ويرسل رده. يمكنك تحديد طريقة البث من جهة العميل عن طريق وضع الكلمة الرئيسية stream قبل نوع الطلب.

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

RouteChat

يقبل هذا النوع مجموعة من RouteNotes يتم إرسالها أثناء التنقّل في مسار، مع تلقّي RouteNotes أخرى (مثل تلك الواردة من مستخدمين آخرين).

هذا هو بالضبط نوع حالة الاستخدام للبث الثنائي الاتجاه. يحتوي استدعاء الإجراء عن بُعد (RPC) ثنائي الاتجاه على كلا الجانبين لإرسال سلسلة من الرسائل باستخدام بث للقراءة والكتابة. يعمل كل من هذين النوعين بشكل مستقل، ما يتيح للعملاء والخوادم القراءة والكتابة بأي ترتيب يفضلونه.

على سبيل المثال، يمكن للخادم الانتظار إلى أن يتلقّى جميع رسائل العميل قبل كتابة ردوده، أو يمكنه بدلاً من ذلك قراءة رسالة ثم كتابة رسالة، أو أي مجموعة أخرى من عمليات القراءة والكتابة.

يتم الحفاظ على ترتيب الرسائل في كل بث. يمكنك تحديد هذا النوع من الطرق من خلال وضع الكلمة الرئيسية stream قبل كل من الطلب والاستجابة.

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

4. إنشاء رمز العميل والخادم

لقد قدّمنا لك الرمز الذي تم إنشاؤه من ملف .proto في الدليل الذي تم إنشاؤه.

إذا أردت معرفة كيفية إنشاء الرمز من ملف .proto بنفسك أو إجراء أي تغييرات على ملف .proto واختبارها، يُرجى الرجوع إلى هذه التعليمات.

يتضمّن الرمز البرمجي الذي تم إنشاؤه ما يلي:

  • تعريفات البنية لأنواع الرسائل Point وFeature وRectangle وRouteNote وRouteSummary
  • سمة الخدمة التي يجب تنفيذها: route_guide_server::RouteGuide.
  • نوع العميل الذي سنستخدمه لاستدعاء الخادم: route_guide_client::RouteGuideClient<T>.

بعد ذلك، سننفّذ الطرق من جهة الخادم، حتى يتمكّن الخادم من الردّ على الطلب الذي يرسله العميل.

5. تنفيذ الخدمة

لنلقِ نظرة أولاً على كيفية إنشاء خادم 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
};

يمكننا البدء بتحديد بنية لتمثيل خدمتنا. يمكننا إجراء ذلك على 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) للبث من جهة الخادم

لنبدأ بـ 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.

استدعاء إجراء عن بُعد للبث من جهة العميل

لنلقِ نظرة الآن على طريقة أكثر تعقيدًا: طريقة البث من جهة العميل 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() على الخادم مع تزويده بتفاصيل المنفذ لبدء وضع الانتظار المعطِّل إلى أن يتم إنهاء العملية.

6. إنشاء العميل

في هذا القسم، سنلقي نظرة على كيفية إنشاء برنامج عميل بلغة Rust لخدمة RouteGuide في src/client/client.rs.

أولاً، يجب إدراج الرمز الذي تم إنشاؤه في النطاق.

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 لقراءة ردود الخادم. نستخدم طريقة ListFeaturesStreammessage() لقراءة ردود الخادم بشكل متكرّر في عنصر Protocol Buffers للردود (في هذه الحالة Feature) إلى أن لا تعود هناك رسائل أخرى.

استدعاء إجراء عن بُعد للبث من جهة العميل

في هذا المثال، نحول متجهًا من النقاط إلى مصدر بيانات.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(())
}

7. للتجربة:

أولاً، لتشغيل Client وServer، لنضِفها كأهداف ثنائية إلى الحزمة. علينا تعديل ملف 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"

8. الخطوات التالية