۱. مقدمه
در این آزمایشگاه کد، شما از 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/2026.tar.gz \
| tar xvz --strip-components=4 \
grpc-codelabs-2026/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 های ما استفاده خواهند شد، تعریف کنیم. در فایل 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 را در تعریف سرویس خود تعریف کنید و انواع درخواست و پاسخ آنها را مشخص کنید. در این بخش از 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 در دایرکتوری 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 را به آن ارسال میکند. ما این کد را در کدی قرار دادهایم که فقط هنگام ارسال یک feature flag اجرا شود، به طوری که فقط زمانی که شما میخواهید، بازسازی شود. لازم نیست الان این کد را اجرا کنید، زیرا ما قبلاً کد را برای شما تولید کردهایم.
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 مراجعه کنید.
در مرحله بعد، متدهای سرویس را روی سرور پیادهسازی خواهیم کرد.
۵. پیادهسازی سرویس
ابتدا بیایید نگاهی به نحوه ایجاد یک سرور 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
بیایید با 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
حالا بیایید به چیزی کمی پیچیدهتر نگاه کنیم: متد استریمینگ سمت کلاینت 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
در نهایت، بیایید نگاهی به 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 برمیگرداند. اگرچه هر طرف همیشه پیامهای طرف دیگر را به ترتیبی که نوشته شدهاند دریافت میکند، اما هم کلاینت و هم سرور میتوانند به هر ترتیبی بخوانند و بنویسند - جریانها کاملاً مستقل عمل میکنند.
ما جریان خروجی را با استفاده از 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()روی سرور با جزئیات پورت خود فراخوانی کنید تا یک انتظار مسدودکننده تا زمان خاتمه فرآیند انجام شود.
۶. مشتری را ایجاد کنید
در این بخش، به ایجاد یک کلاینت 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های استریمینگ ناهمزمان و غیر مسدودکننده هستند و از سینتکس async/await و استریمهای Tokio در Rust استفاده میکنند.
RPC استریمینگ سمت سرور: PrintFeatures
در RPC های جریانی از سرور، کلاینت یک پیام درخواست واحد به سرور ارسال میکند و جریانی از پیامهای پاسخ را دریافت میکند. در اینجا در client.rs ما متد جریانی سمت سرور list_features() را فراخوانی میکنیم (که مربوط به اعلان rpc ListFeatures است که در نمونه اولیه ما یافت میشود). سرور نیز به نوبه خود، جریانی از پیامهای 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
در نهایت، بیایید نگاهی به RPC دوطرفه RouteChat() خود بیندازیم. در اینجا هم کلاینت و هم سرور دنباله ای از پیام ها را به عقب و جلو ارسال می کنند. ما یک وظیفه 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() اضافه میکنیم که از زمان اجرای async 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(())
}
۷. امتحانش کنید
برای اجرای کلاینت و سرور خود، ابتدا تأیید کنید که هر دو هدف دودویی در 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"
۸. قدم بعدی چیست؟
- مخزن رسمی gRPC-Rust را بررسی کنید.
- برای کسب اطلاعات بیشتر در مورد معماری gRPC به Core Concepts مراجعه کنید.
- مستندات gRPC-Rust را در gRPC.io مشاهده کنید.
۹. مشارکتکنندگان در این آزمایشگاه کد
- کتی ژائو
- اروند برایت
- ناتانیل فورد