بدء استخدام gRPC-Rust

1. مقدمة

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

في نهاية هذا البرنامج التعليمي، سيكون لديك تطبيق عميل يتصل بخادم بعيد باستخدام gRPC للحصول على اسم أو عنوان بريدي للموقع الجغرافي الذي يقع عند إحداثيات معيّنة على الخريطة. قد يستخدم تطبيق متكامل تصميم العميل والخادم هذا لتعداد نقاط الاهتمام أو تلخيصها على طول مسار معيّن.

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

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

أهداف الدورة التعليمية

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

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

2. قبل البدء

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

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

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

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

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

أولاً، أنشئ دليل عمل الدرس التطبيقي وادخله:

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

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

3- تحديد الخدمة

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

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

سيتم تحديد طريقة RPC هذه وأنواع الرسائل الخاصة بها في ملف proto/route_guide.proto الخاص برمز المصدر المقدَّم.

يُشار إلى Protocol Buffers عادةً باسم protobufs. لمزيد من المعلومات عن مصطلحات gRPC، يُرجى الاطّلاع على المفاهيم الأساسية والبنية ودورة الحياة في gRPC.

طريقة الخدمة

لنحدّد أولاً طرق الخدمة، ثم نحدّد أنواع الرسائل Point وFeature. يحتوي ملف proto/routeguide.proto على بنية service باسم RouteGuide تحدّد طريقة واحدة أو أكثر توفّرها خدمة التطبيق.

أضِف الطريقة rpc داخل تعريف RouteGuide.GetFeature كما هو موضّح سابقًا، ستبحث هذه الطريقة عن اسم أو عنوان موقع جغرافي من مجموعة إحداثيات معيّنة، لذا اطلب من GetFeature عرض Feature لـ Point معيّن:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

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

أنواع الرسائل

في ملف 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! في gRPC واستيراد السمة 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 من الرمز الذي تم إنشاؤه.

استدعاء الإجراء عن بُعد الأحادي

تنفّذ الفئة 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. إنشاء RouteGuideService مع تحميل الميزات من خلال استدعاء الدالة المساعدة load()
  3. أنشِئ مثيلاً لخادم gRPC باستخدام RouteGuideServer::new() باستخدام الخدمة التي أنشأناها.
  4. تسجيل تنفيذ الخدمة في خادم gRPC
  5. اتّصِل بالخادم على serve() مع تفاصيل المنفذ لإجراء انتظار حظر إلى أن يتم إيقاف العملية.

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

في هذا القسم، سنلقي نظرة على كيفية إنشاء برنامج عميل بلغة 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.

Simple 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. الخطوات التالية