1. مقدمة
في هذا الدرس التطبيقي حول الترميز، ستستخدم gRPC-Rust لإنشاء عميل وخادم يشكّلان الأساس لتطبيق يحدّد المسارات مكتوب بلغة Rust.
في نهاية هذا الدليل التعليمي، سيكون لديك عميل يتصل بخادم بعيد باستخدام gRPC للحصول على معلومات حول الميزات على مسار العميل، وإنشاء ملخّص لمسار العميل، وتبادل معلومات المسار، مثل آخر الأخبار عن حركة المرور، مع الخادم والعملاء الآخرين.
يتم تحديد الخدمة في ملف بتنسيق Protocol Buffers، وسيتم استخدام هذا الملف لإنشاء رمز نص نموذجي للعميل والخادم حتى يتمكّنا من التواصل مع بعضهما البعض، ما يوفّر عليك الوقت والجهد في تنفيذ هذه الوظيفة.
لا يهتم هذا الرمز الذي تم إنشاؤه بتعقيدات الاتصال بين الخادم والعميل فحسب، بل أيضًا بتسلسل البيانات وإلغاء تسلسلها.
ماذا ستتعلّم؟
- كيفية استخدام "مخازن البروتوكولات المؤقتة" (Protocol Buffers) لتحديد واجهة برمجة تطبيقات الخدمة
- كيفية إنشاء عميل وخادم يستندان إلى gRPC استنادًا إلى "مخازن البروتوكولات المؤقتة" من خلال إنشاء الرموز البرمجية آليًا
- فهم عملية التواصل بين العميل والخادم باستخدام gRPC
هذا الدرس التطبيقي حول الترميز موجّه لمطوّري Rust المبتدئين في gRPC أو الذين يريدون تجديد معلوماتهم في المجال، أو أي شخص آخر مهتم بتطوير أنظمة موزّعة. لا يُشترط توفّر خبرة سابقة في gRPC.
2. قبل البدء
المتطلبات الأساسية
تأكَّد من تثبيت ما يلي:
- GCC. اتّبِع التعليمات الواردة هنا.
- Git: تعليمات التثبيت هنا
- Rust، الإصدار 1.88.0 اتّبِع تعليمات التثبيت هنا.
الحصول على الشفرة
كي لا تضطر إلى البدء من الصفر تمامًا، يوفّر لك هذا الدرس التطبيقي حول الترميز بنية أساسية للرمز المصدر الخاص بالتطبيق لتتمكّن من إكماله. ستوضّح لك الخطوات التالية كيفية إكمال التطبيق، بما في ذلك استخدام مكوّنات برنامج تجميع مخازن البروتوكولات المؤقتة لإنشاء رمز 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/2026.tar.gz \
| tar xvz --strip-components=4 \
grpc-codelabs-2026/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. في ملف 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;
}
بعد ذلك، تظهر رسالة 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
يحصل على Features المتاحة ضمن Rectangle المحدّدة. يتم بث النتائج بدلاً من عرضها دفعة واحدة (مثل رسالة رد تتضمّن حقلًا متكرّرًا)، لأنّ المستطيل قد يغطّي مساحة كبيرة ويحتوي على عدد كبير من العناصر.
النوع المناسب لهذا الإجراء عن بُعد هو إجراء عن بُعد للبث من جهة الخادم: يرسل العميل طلبًا إلى الخادم ويتلقّى بثًا لقراءة سلسلة من الرسائل. يقرأ العميل من البث الذي تم إرجاعه إلى أن لا تتبقى أي رسائل. كما هو موضّح في مثالنا، يمكنك تحديد طريقة البث من جهة الخادم من خلال وضع الكلمة الرئيسية stream قبل نوع الاستجابة.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
تقبل هذه السمة مجموعة من Points على مسار يتم اجتيازه، وتعرض RouteSummary عند اكتمال عملية الاجتياز.
يبدو أنّ طلب إجراء مكالمة RPC ببث من جهة العميل مناسب في هذه الحالة: يكتب العميل سلسلة من الرسائل ويرسلها إلى الخادم، وذلك باستخدام بث يتم توفيره مرة أخرى. بعد أن ينتهي العميل من كتابة الرسائل، ينتظر أن يقرأها الخادم كلها ويرسل رده. يمكنك تحديد طريقة البث من جهة العميل عن طريق وضع الكلمة المفتاحية stream قبل نوع الطلب.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
يقبل هذا النوع مجموعة من RouteNotes يتم إرسالها أثناء التنقّل في مسار، مع تلقّي RouteNotes أخرى (مثل تلك الواردة من مستخدمين آخرين).
هذا هو بالضبط نوع حالة الاستخدام للبث الثنائي الاتجاه. يتم في استدعاء الإجراء عن بُعد (RPC) ثنائي الاتجاه إرسال تسلسل من الرسائل من كلا الجانبين باستخدام بث للقراءة والكتابة. يعمل كل من هذين النوعين بشكل مستقل، لذا يمكن للعملاء والخوادم القراءة والكتابة بأي ترتيب يريدونه.
على سبيل المثال، يمكن للخادم الانتظار إلى أن يتلقّى جميع رسائل العميل قبل كتابة ردوده، أو يمكنه بدلاً من ذلك قراءة رسالة ثم كتابة رسالة، أو أي مجموعة أخرى من عمليات القراءة والكتابة.
يتم الحفاظ على ترتيب الرسائل في كل تدفق. يمكنك تحديد هذا النوع من الطرق من خلال وضع الكلمة الرئيسية stream قبل كل من الطلب والاستجابة.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
4. قم بإنشاء كود العميل والخادم
لقد قدّمنا لك الرمز البرمجي الذي تم إنشاؤه من الملف .proto في الدليل generated/، بما في ذلك جميع الإضافات التي أجريتها أعلاه. ومع ذلك، نودّ أن نوضّح لك آلية عمل ميزة إنشاء الرموز.
يصف ملف .proto جميع البُنى والدوال التي يستخدمها العميل أو الخادم. نستخدم نصًا برمجيًا لإنشاء Cargo (build.rs) مع حزمة grpc-protobuf-build لإنشاء هذا الرمز تلقائيًا.
في Cargo.toml، أضفنا grpc-protobuf-build بالفعل كعنصر تابع في عملية الإنشاء.
في 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 تعريفات Protocol Buffers في الدليل generate/، بما في ذلك:
- تعريفات البنية لأنواع الرسائل
PointوFeature - سمة خدمة Tonic التي سنحتاج إلى تنفيذها للخادم:
route_guide_server::RouteGuide. - نوع عميل gRPC-Rust الذي سنستخدمه لاستدعاء الخادم:
route_guide_client::RouteGuideClient<T>.
يمكنك الرجوع إلى دليل protoc-gen-rust-grpc للحصول على مزيد من المعلومات.
بعد ذلك، سننفّذ طرق الخدمة على الخادم.
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
لنبدأ بـ ListFeatures. هذا هو استدعاء إجراء عن بُعد (RPC) لبث البيانات من جهة الخادم (سيرسل العميل رسالة واحدة، وسيردّ الخادم برسائل متعددة)، لذا علينا إرسال عدة Features إلى العميل.
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
والآن دعونا ننظر إلى شيء أكثر تعقيدًا بعض الشيء: طريقة البث من جهة العميل 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) ثنائي الاتجاه للبث: RouteChat
أخيرًا، لنلقِ نظرة على RouteChat() RPC لعمليات البث الثنائي الاتجاه.
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. على الرغم من أنّ كل طرف سيتلقّى دائمًا رسائل الطرف الآخر بالترتيب الذي تمت كتابتها به، يمكن لكل من العميل والخادم القراءة والكتابة بأي ترتيب، لأنّ عمليات البث تعمل بشكل مستقل تمامًا.
ننشئ دفق الإخراج باستخدام 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():
- حدد المنفذ الذي نريد استخدامه للاستماع إلى طلبات العميل
- إنشاء
RouteGuideServiceمع تحميل الميزات - أنشِئ مثيلاً لخادم gRPC باستخدام
RouteGuideServer::new()باستخدام الخدمة التي أنشأناها. - قم بتسجيل تطبيق الخدمة الخاص بنا مع خادم gRPC.
- قم باستدعاء
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) المتدفقة غير متزامنة ولا تؤدي إلى الحظر، وذلك باستخدام بنية async/await في Rust وعمليات البث في Tokio.
استدعاء إجراء عن بُعد (RPC) للبث من جهة الخادم: PrintFeatures
في طلبات استدعاء الإجراء عن بُعد (RPC) التي تتضمّن بثًا من الخادم، يرسل العميل رسالة طلب واحدة إلى الخادم، ويتلقّى بثًا من رسائل الرد. في ما يلي المكان الذي نستدعي فيه طريقة البث من جهة الخادم list_features() في client.rs (التي تتوافق مع تعريف ListFeatures rpc الذي تم العثور عليه في ملف proto). سيرسل الخادم بدوره مجموعة من رسائل Feature:
async fn print_features(client: &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(rectangle).await;
while let Some(feature) = stream.recv().await {
println!(
"FEATURE: Name = \"{}\", Lat = {}, Lon = {}",
feature.name(),
feature.location().latitude(),
feature.location().longitude()
);
}
let status = stream.status().await;
assert!(status.is_ok(), "{:?}", status);
Ok(())
}
استدعاء إجراء عن بُعد (RPC) لبث البيانات من جهة العميل: RecordRoute
عندما نستخدم البث من جهة العميل، سيقوم العميل بفتح قناة اتصال بالخادم وإرسال سلسلة من الرسائل. سيتلقّى رسالة ردّ واحدة عند انتهاء البث.
في هذا المثال، نبدأ طلب البحث باستخدام client.record_route().await، ونرسل عدة إحداثيات Point تم إنشاؤها واحدة تلو الأخرى عبر البث باستخدام stream.send(point).await، ثم نغلق البث باستخدام stream.close_and_recv().await لتلقّي رسالة RouteSummary من الخادم الفردي.
async fn run_record_route(client: &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 mut stream = client.record_route().await;
for point in &points {
if stream.send(point).await.is_err() {
break;
}
}
match stream.close_and_recv().await {
Ok(response) => {
println!(
"SUMMARY: Feature Count = {}, Distance = {}",
response.feature_count(),
response.distance()
);
}
Err(e) => println!("something went wrong: {e:?}"),
}
Ok(())
}
استدعاء إجراء عن بُعد (RPC) ثنائي الاتجاه: RouteChat
أخيرًا، لنلقِ نظرة على RouteChat() RPC لعمليات البث الثنائي الاتجاه. في هذه الحالة، سيتبادل كلّ من العميل والخادم سلسلة من الرسائل. نقوم بإنشاء مهمة tokio لإرسال الرسائل باستمرار إلى الخادم باستخدام tx.send(note).await.is_err(). في الوقت نفسه، يستمع rx.recv().await إلى رسائل الرد من الخادم، ويطبعها عند تلقّيها.
async fn run_route_chat(client: &RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
let (mut tx, mut rx) = client.route_chat().await;
let start = time::Instant::now();
tokio::spawn(async move {
let mut interval = time::interval(Duration::from_millis(50));
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_millis() as i32,
longitude: -746188906,
}),
message: format!("at {elapsed:?}"),
});
if tx.send(note).await.is_err() {
return;
}
}
tx.close();
});
while let Some(note) = rx.recv().await {
println!(
"Note: Latitude = {}, Longitude = {}, Message = \"{}\"",
note.location().latitude(),
note.location().longitude(),
note.message()
);
}
let status = rx.status().await;
assert!(status.is_ok(), "{:?}", status);
Ok(())
}
على الرغم من أنّ كل طرف سيتلقّى دائمًا رسائل الطرف الآخر بالترتيب الذي تمت كتابتها به، يمكن لكل من العميل والخادم القراءة والكتابة بأي ترتيب، لأنّ عمليات البث تعمل بشكل مستقل تمامًا.
إنشاء عميل وتمريره
لاستدعاء طرق الخدمة، علينا أولاً إنشاء قناة للتواصل مع الخادم. نقوم بإنشاء هذا عن طريق إنشاء نقطة نهاية أولاً، والاتصال بتلك النقطة، وتمرير القناة التي تم إنشاؤها عند الاتصال بـ RouteGuideClient::new() على النحو التالي:
// Create channel to connect to server
let channel = Channel::builder(
"dns:///[::1]:10000",
Arc::new(LocalChannelCredentials::new()),
)
.build();
// Create a new client
let client = RouteGuideClient::new(channel);
بعد إنشاء هذا العميل، يمكننا استدعاء الطرق التي كتبناها أعلاه، مع تمرير العميل إليها. نضيف كل هذا الرمز إلى main()، الذي يستخدم وقت التشغيل غير المتزامن في Tokio. في ما يلي الرمز الكامل:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create channel to connect to server
let channel = Channel::builder(
"dns:///[::1]:10000",
Arc::new(LocalChannelCredentials::new()),
)
.build();
// Create a new client
let client = RouteGuideClient::new(channel);
println!("\n*** SERVER STREAMING ***");
print_features(&client).await?;
println!("\n*** CLIENT STREAMING ***");
run_record_route(&client).await?;
println!("\n*** BIDIRECTIONAL STREAMING ***");
run_route_chat(&client).await?;
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
ستظهر لك نتيجة مشابهة لما يلي:
*** 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. الخطوات التالية
- استكشِف مستودع gRPC-Rust الرسمي.
- يمكنك الاطّلاع على مزيد من المعلومات عن بنية gRPC في مقالة المفاهيم الأساسية.
- يمكنك الاطّلاع على مستندات gRPC-Rust على gRPC.io.
9. المساهمون في هذا الدرس التطبيقي حول الترميز
- Cathy Zhao
- Arvind Bright
- ناثانيال فورد