1. مقدمة
في هذا الدرس العملي، ستستخدم gRPC-Rust لإنشاء عميل وخادم يشكلان أساس تطبيق رسم الخرائط المسارية المكتوب بلغة Rust.
بنهاية البرنامج التعليمي، سيكون لديك عميل يتصل بخادم بعيد باستخدام تطبيق Rust الرسمي لبروتوكول gRPC لجلب اسم أو عنوان بريدي لموقع عند إحداثيات محددة على الخريطة. قد يستخدم تطبيق متكامل تصميم العميل والخادم هذا لتعداد نقاط الاهتمام أو تلخيصها على طول مسار معيّن.
يتم تحديد الخدمة في ملف بتنسيق Protocol Buffers، وسيتم استخدام هذا الملف لإنشاء رمز نص نموذجي للعميل والخادم حتى يتمكّنا من التواصل مع بعضهما البعض، ما يوفّر عليك الوقت والجهد في تنفيذ هذه الوظيفة.
لا يهتم هذا الرمز الذي تم إنشاؤه بتعقيدات الاتصال بين الخادم والعميل فحسب، بل أيضًا بتسلسل البيانات وإلغاء تسلسلها.
ماذا ستتعلّم؟
- كيفية استخدام "مخازن البروتوكولات المؤقتة" (Protocol Buffers) لتحديد واجهة برمجة تطبيقات الخدمة
- كيفية إنشاء عميل قائم على gRPC من تعريف بروتوكول Buffer باستخدام توليد التعليمات البرمجية الآلي.
- فهم عملية التواصل بين العميل والخادم باستخدام gRPC
يهدف هذا الدرس العملي إلى مطوري لغة Rust الجدد على gRPC أو الذين يسعون إلى تنشيط معلوماتهم حول gRPC، أو أي شخص آخر مهتم ببناء أنظمة موزعة. لا يُشترط توفّر خبرة سابقة في gRPC.
2. قبل البدء
المتطلبات الأساسية
تأكد من تثبيت ما يلي:
- مجلس التعاون الخليجي. اتبع التعليمات هنا.
- تعليمات تثبيت Git هنا.
- Rust، الإصدار 1.88.0 اتّبِع تعليمات التثبيت هنا.
الحصول على الشفرة
كي لا تضطر إلى البدء من الصفر تمامًا، يوفّر لك هذا الدرس التطبيقي حول الترميز بنية أساسية للرمز المصدر الخاص بالتطبيق لتتمكّن من إكماله. ستوضّح لك الخطوات التالية كيفية إكمال التطبيق، بما في ذلك استخدام مكوّنات برنامج تجميع مخازن البروتوكولات المؤقتة لإنشاء رمز gRPC النموذجي.
أولاً، أنشئ دليل عمل الدرس التطبيقي وادخله:
mkdir grpc-rust-getting-started && cd grpc-rust-getting-started
نزِّل الدرس التطبيقي حول الترميز واستخرِجه:
curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/2026.tar.gz \
| tar xvz --strip-components=4 \
grpc-codelabs-2026/codelabs/grpc-rust-getting-started/start_here
بدلاً من ذلك، يمكنك تنزيل ملف .zip الذي يحتوي على دليل الدرس العملي فقط وفك ضغطه يدويًا.
يتوفّر الرمز المصدر المكتمل على GitHub إذا كنت تريد تخطّي كتابة عملية التنفيذ.
3. تحديد الخدمة
تتمثّل خطوتك الأولى في تحديد خدمة gRPC للتطبيق وطريقة استدعاء إجراء عن بُعد (RPC) وأنواع رسائل الطلبات والردود باستخدام مخازن البروتوكولات المؤقتة. ستوفّر خدمتك ما يلي:
- طريقة استدعاء إجراء عن بُعد تُسمّى
GetFeatureينفّذها الخادم ويستدعيها العميل. - أنواع الرسائل
PointوFeatureهي هياكل البيانات المتبادلة بين العميل والخادم عند استخدام طريقةGetFeature. يقدّم العميل إحداثيات الخريطة كـPointفي طلبGetFeatureإلى الخادم، ويردّ الخادم بـFeatureمطابق يصف أي شيء يقع في تلك الإحداثيات.
سيتم تحديد طريقة "استدعاء الإجراء عن بُعد" هذه وأنواع الرسائل الخاصة بها في ملف proto/routeguide.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/routeguide.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 في الدليل generated/، بما في ذلك جميع الإضافات التي قمت بها أعلاه. ومع ذلك، نود أن نأخذ لحظة لشرح كيفية عمل عملية توليد الكود.
يصف ملفنا .proto جميع الهياكل والوظائف التي يستخدمها العميل أو الخادم. نستخدم برنامج بناء Cargo (build.rs) جنبًا إلى جنب مع حزمة grpc-protobuf-build لتوليد هذا الكود تلقائيًا.
في Cargo.toml نضيف grpc-protobuf-build تحت [build-dependencies].
في build.rs، نقوم بتهيئة grpc_protobuf_build::CodeGen لتجميع proto/routeguide.proto في الدليل generated/. النقاط الرئيسية موجودة هنا:
grpc_protobuf_build::CodeGen::new()
.include("proto")
.input("routeguide.proto")
.output_dir("generated")
.compile()
.unwrap();
يستدعي هذا توليد الكود الخاص بالحزمة grpc_protobuf_build، ويمرر إليها routeguide.proto. لقد قمنا بتغليف هذا ببعض التعليمات البرمجية ليتم تشغيلها فقط عند تمرير علامة الميزة، بحيث يتم إعادة إنشائها فقط عندما تريد ذلك. لست مضطرًا لتشغيل هذا الآن، لأننا قمنا بالفعل بإنشاء الكود لك.
cargo build --bin routeguide-server --features regenerate_proto
عند تشغيل الأمر cargo build، يقوم build.rs بتجميع تعريفات بروتوكول المخزن المؤقت في الدليل generate/، بما في ذلك:
- تعريفات البنية لأنواع الرسائل
PointوFeature. - إحدى سمات خدمة Tonic التي سنحتاج إلى تطبيقها على الخادم:
route_guide_server::RouteGuide. - نوع عميل gRPC-Rust الذي سنستخدمه للاتصال بالخادم:
route_guide_client::RouteGuideClient<T>.
يمكنك الرجوع إلى protoc-gen-rust-grpc guide لمزيد من المعلومات.
بعد ذلك، سنقوم بتنفيذ أساليب الخدمة على الخادم.
5. تنفيذ الخدمة
في src/server/server.rs، يمكننا إدخال الكود المُنشأ في النطاق من خلال ماكرو include_generated_proto! الخاص بـ gRPC واستيراد السمة RouteGuide و Point.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
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 من الكود الذي تم إنشاؤه.
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()))
}
}
بمجرد تطبيق هذه الطريقة، نحتاج أيضًا إلى تشغيل خادم 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()، خطوة بخطوة:
- حدد المنفذ الذي نريد استخدامه للاستماع إلى طلبات العميل
- أنشئ
RouteGuideServiceمع تحميل الميزات عن طريق استدعاء الدالة المساعدةload() - أنشئ نسخة من خادم gRPC باستخدام
RouteGuideServer::new()باستخدام الخدمة التي أنشأناها. - قم بتسجيل تطبيق الخدمة الخاص بنا مع خادم gRPC.
- قم باستدعاء
serve()على الخادم باستخدام تفاصيل المنفذ الخاصة بنا للقيام بانتظار حظر حتى يتم إنهاء العملية.
6. إنشاء العميل
في هذا القسم، سنلقي نظرة على إنشاء عميل Rust لخدمة RouteGuide الخاصة بنا في src/client/client.rs.
كما فعلنا في src/server/server.rs، يمكننا إدخال الكود المُنشأ في النطاق من خلال ماكرو include_generated_proto! الخاص بـ gRPC واستيراد النوع RouteGuideClient.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
use grpc_pb::{
route_guide_client::RouteGuideClient,
Point,
};
طُرق خدمات الاستدعاء
في gRPC-Rust، تكون RPCs غير متزامنة وغير مانعة، باستخدام صيغة Rust async/await لانتظار الاستجابات من الخادم.
لاستدعاء وظائف الخدمة، نقوم أولاً بإنشاء Channel باستخدام Channel::builder()، مع تحديد عنوان الخادم (dns:///[::1]:10000) وبيانات اعتماد الاتصال (LocalChannelCredentials). ثم نمرر القناة إلى RouteGuideClient::new() لإنشاء عميلنا.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a new gRPC channel
let channel = Channel::builder(
"dns:///[::1]:10000",
Arc::new(LocalChannelCredentials::new()),
)
.build();
// Create a new client
let client = RouteGuideClient::new(channel);
}
في هذه الوظيفة، RouteGuideClient::new() يربط القناة العامة بـ client stub الذي تم إنشاؤه والذي ينفذ الأساليب المحددة في تعريف الخدمة .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(point)
.await
.expect("RPC error");
في gRPC-Rust، نقوم بتمرير رسالة protobuf Point مباشرة إلى client.get_feature(point). يؤدي انتظار المستقبل المُعاد إلى الحصول على الاستجابة Feature مباشرة، دون الحاجة إلى استدعاءات طرق إضافية.
بعد ذلك، اطبع الحقول من الاستجابة:
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>> {
// 1. Create a new gRPC channel
let channel = Channel::builder(
"dns:///[::1]:10000",
Arc::new(LocalChannelCredentials::new()),
)
.build();
// 2. Instantiate the generated RouteGuideClient
let client = RouteGuideClient::new(channel);
println!("*** SIMPLE RPC ***");
let point = proto!(Point {
latitude: 409_146_138,
longitude: -746_188_906,
});
let response = client
.get_feature(point)
.await
.expect("RPC error");
println!(
"Response = Name = \"{}\", Latitude = {}, Longitude = {}",
response.name(),
response.location().latitude(),
response.location().longitude()
);
Ok(())
}
7. للتجربة:
لتشغيل العميل والخادم، تحقق أولاً من تعريف كلا الهدفين الثنائيين في Cargo.toml:
[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"
[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"
ثم قم بتنفيذ الأوامر التالية من دليل العمل الخاص بنا:
- قم بتشغيل الخادم في نافذة طرفية واحدة:
cargo run --bin routeguide-server
- شغِّل العميل من وحدة طرفية أخرى:
cargo run --bin routeguide-client
ستظهر لك نتيجة مشابهة لما يلي، مع حذف الطوابع الزمنية لتوضيح الصورة:
*** SIMPLE RPC ***
Response = Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Latitude = 409146138, Longitude = -746188906
8. الخطوات التالية
- تابِع إلى برنامج التدريب العملي بدء استخدام gRPC-Rust (البث).
- استكشِف مستودع gRPC-Rust الرسمي.
- يمكنك الاطّلاع على مزيد من المعلومات عن بنية gRPC في مقالة المفاهيم الأساسية.
- يمكنك الاطّلاع على مستندات gRPC-Rust على gRPC.io.
9. المساهمون في هذا الدرس التطبيقي حول الترميز
- Cathy Zhao
- لوسيو فرانكو
- Arvind Bright
- ناثانيال فورد