Làm quen với gRPC-Rust – Truyền phát trực tuyến

1. Giới thiệu

Trong lớp học lập trình này, bạn sẽ sử dụng gRPC-Rust để tạo một máy khách và máy chủ tạo thành nền tảng của một ứng dụng lập bản đồ tuyến đường được viết bằng Rust.

Khi kết thúc hướng dẫn này, bạn sẽ có một ứng dụng kết nối với một máy chủ từ xa bằng gRPC để lấy thông tin về các đối tượng trên tuyến đường của ứng dụng, tạo bản tóm tắt về tuyến đường của ứng dụng và trao đổi thông tin về tuyến đường (chẳng hạn như thông tin cập nhật về tình trạng giao thông) với máy chủ và các ứng dụng khác.

Dịch vụ này được xác định trong một tệp Protocol Buffers. Tệp này sẽ được dùng để tạo mã chuẩn cho ứng dụng và máy chủ để chúng có thể giao tiếp với nhau, giúp bạn tiết kiệm thời gian và công sức khi triển khai chức năng đó.

Mã được tạo này không chỉ xử lý sự phức tạp của việc giao tiếp giữa máy chủ và ứng dụng mà còn xử lý quá trình chuyển đổi dữ liệu thành chuỗi và chuyển đổi chuỗi thành dữ liệu.

Kiến thức bạn sẽ học được

  • Cách sử dụng Protocol Buffers để xác định một API dịch vụ.
  • Cách tạo máy khách và máy chủ dựa trên gRPC từ một định nghĩa Protocol Buffers bằng cách sử dụng tính năng tạo mã tự động.
  • Hiểu rõ về giao tiếp truyền phát trực tiếp giữa máy khách và máy chủ bằng gRPC.

Lớp học lập trình này dành cho những nhà phát triển Rust mới làm quen với gRPC hoặc muốn tìm hiểu lại về gRPC, hoặc bất kỳ ai khác quan tâm đến việc xây dựng hệ thống phân tán. Bạn không cần có kinh nghiệm sử dụng gRPC.

2. Trước khi bắt đầu

Điều kiện tiên quyết

Đảm bảo bạn đã cài đặt những thứ sau:

Lấy mã

Để bạn không phải bắt đầu hoàn toàn từ đầu, lớp học lập trình này cung cấp một khung mã nguồn ứng dụng để bạn hoàn tất. Các bước sau đây sẽ hướng dẫn bạn cách hoàn tất ứng dụng, bao gồm cả việc sử dụng trình bổ trợ trình biên dịch vùng đệm giao thức để tạo mã gRPC chung.

Trước tiên, hãy tạo thư mục làm việc của lớp học lập trình và cd vào thư mục đó:

mkdir streaming-grpc-rust-getting-started && cd streaming-grpc-rust-getting-started

Tải và giải nén lớp học lập trình:

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

Ngoài ra, bạn có thể tải tệp .zip chỉ chứa thư mục codelab xuống rồi giải nén theo cách thủ công.

Bạn có thể xem mã nguồn hoàn chỉnh trên GitHub nếu muốn bỏ qua bước nhập nội dung triển khai.

3. Xác định thông báo và dịch vụ

Bước đầu tiên là xác định dịch vụ gRPC của ứng dụng, các phương thức RPC và các loại thông báo yêu cầu và phản hồi bằng cách sử dụng Protocol Buffers. Dịch vụ của bạn sẽ cung cấp:

  • Các phương thức RPC được gọi là ListFeatures, RecordRouteRouteChat mà máy chủ triển khai và ứng dụng gọi.
  • Các loại thông báo Point, Feature, Rectangle, RouteNoteRouteSummary là các cấu trúc dữ liệu được trao đổi giữa máy khách và máy chủ khi gọi các phương thức ở trên.

Tất cả các phương thức RPC này và các loại thông báo của phương thức sẽ được xác định trong tệp proto/routeguide.proto của mã nguồn được cung cấp.

Vùng đệm giao thức thường được gọi là protobuf. Để biết thêm thông tin về thuật ngữ gRPC, hãy xem phần Các khái niệm, cấu trúc và vòng đời cốt lõi của gRPC.

Xác định các loại thông báo

Trước tiên, hãy xác định các thông báo sẽ được dùng bởi các RPC. Trong tệp routeguide/route_guide.proto của mã nguồn, trước tiên hãy xác định kiểu thông báo Point. Point biểu thị một cặp toạ độ vĩ độ và kinh độ trên bản đồ. Trong lớp học lập trình này, hãy sử dụng số nguyên cho toạ độ:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

Các số 12 là số nhận dạng duy nhất cho từng trường trong cấu trúc message.

Tiếp theo, hãy xác định loại thông báo Feature. Feature sử dụng trường string cho tên hoặc địa chỉ bưu chính của một thứ gì đó tại một vị trí do Point chỉ định:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Tiếp theo là thông báo Rectangle biểu thị một hình chữ nhật vĩ độ – kinh độ, được biểu thị dưới dạng hai điểm đối diện theo đường chéo "lo" và "hi".

message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;

  // The other corner of the rectangle.
  Point hi = 2;
}

Ngoài ra, RouteNote là một thông báo đại diện cho thông báo được gửi tại một thời điểm nhất định.

message RouteNote {
  // The location from which the message is sent.
  Point location = 1;

  // The message to be sent.
  string message = 2;
}

Chúng tôi cũng sẽ yêu cầu bạn gửi một thông báo RouteSummary. Thông báo này được nhận để phản hồi một RPC RecordRoute. RPC này sẽ được giải thích trong phần tiếp theo. Tệp này chứa số lượng điểm riêng lẻ nhận được, số lượng đối tượng được phát hiện và tổng khoảng cách đã đi được tính bằng tổng tích luỹ khoảng cách giữa mỗi điểm.

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;
}

Xác định các phương thức dịch vụ

Trước tiên, hãy xác định dịch vụ của chúng ta, sau đó xác định thông báo sau. Để xác định một dịch vụ, bạn chỉ định một dịch vụ có tên trong tệp .proto. Tệp proto/routeguide.proto có cấu trúc service tên là RouteGuide, xác định một hoặc nhiều phương thức do dịch vụ của ứng dụng cung cấp.

Xác định các phương thức RPC trong định nghĩa dịch vụ, chỉ định các loại yêu cầu và phản hồi của chúng. Trong phần này của lớp học lập trình, hãy xác định:

ListFeatures

Lấy các Feature có sẵn trong Rectangle đã cho. Kết quả được truyền trực tuyến thay vì trả về ngay lập tức (ví dụ: trong một thông báo phản hồi có trường lặp lại), vì hình chữ nhật có thể bao phủ một khu vực rộng lớn và chứa một số lượng lớn các đối tượng.

Một loại phù hợp cho RPC này là RPC truyền trực tuyến phía máy chủ: ứng dụng gửi yêu cầu đến máy chủ và nhận một luồng để đọc lại một chuỗi thông báo. Ứng dụng đọc từ luồng được trả về cho đến khi không còn thông báo nào nữa. Như bạn có thể thấy trong ví dụ của chúng tôi, bạn chỉ định một phương thức truyền phát trực tiếp phía máy chủ bằng cách đặt từ khoá stream trước loại phản hồi.

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

RecordRoute

Chấp nhận một luồng Point trên một tuyến đường đang được đi qua, trả về một RouteSummary khi quá trình đi qua hoàn tất.

RPC truyền trực tuyến phía máy khách có vẻ phù hợp trong trường hợp này: máy khách ghi một chuỗi thông báo và gửi chúng đến máy chủ, một lần nữa sử dụng một luồng được cung cấp. Sau khi hoàn tất việc ghi các thông báo, ứng dụng sẽ đợi máy chủ đọc tất cả các thông báo đó và trả về phản hồi. Bạn chỉ định một phương thức truyền phát trực tiếp phía máy khách bằng cách đặt từ khoá stream trước loại yêu cầu.

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

RouteChat

Chấp nhận luồng RouteNote được gửi trong khi một tuyến đường đang được đi qua, đồng thời nhận các RouteNote khác (ví dụ: từ những người dùng khác).

Đây chính xác là loại trường hợp sử dụng cho truyền phát trực tiếp hai chiều. RPC truyền trực tuyến hai chiều có cả hai bên gửi một chuỗi thông báo bằng cách sử dụng luồng đọc-ghi. Hai luồng này hoạt động độc lập, vì vậy, các máy khách và máy chủ có thể đọc và ghi theo bất kỳ thứ tự nào mà chúng muốn.

Ví dụ: máy chủ có thể chờ nhận tất cả tin nhắn của ứng dụng trước khi ghi phản hồi, hoặc có thể lần lượt đọc một tin nhắn rồi ghi một tin nhắn, hoặc một số tổ hợp khác giữa đọc và ghi.

Thứ tự của các thông báo trong mỗi luồng được giữ nguyên. Bạn chỉ định loại phương thức này bằng cách đặt từ khoá stream trước cả yêu cầu và phản hồi.

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

4. Tạo mã máy khách và máy chủ

Chúng tôi đã cung cấp cho bạn mã được tạo từ tệp .proto trong thư mục được tạo.

Nếu bạn muốn tự tìm hiểu cách tạo mã từ tệp .proto hoặc thực hiện bất kỳ thay đổi nào đối với tệp .proto và kiểm thử các thay đổi đó, hãy tham khảo các hướng dẫn này.

Mã được tạo chứa:

  • Định nghĩa cấu trúc cho các loại thông báo Point, Feature, Rectangle, RouteNoteRouteSummary.
  • Một đặc điểm dịch vụ mà chúng ta cần triển khai: route_guide_server::RouteGuide.
  • Loại ứng dụng mà chúng ta sẽ dùng để gọi máy chủ: route_guide_client::RouteGuideClient<T>.

Tiếp theo, chúng ta sẽ triển khai các phương thức ở phía máy chủ để khi máy khách gửi yêu cầu, máy chủ có thể trả lời.

5. Triển khai dịch vụ

Trước tiên, hãy xem cách chúng ta tạo một máy chủ RouteGuide. Có hai phần để dịch vụ RouteGuide thực hiện công việc của mình:

  • Triển khai giao diện dịch vụ được tạo từ định nghĩa dịch vụ của chúng ta: thực hiện "công việc" thực tế của dịch vụ.
  • Chạy một máy chủ gRPC để theo dõi các yêu cầu từ ứng dụng và gửi các yêu cầu đó đến phương thức triển khai phù hợp.

Trong src/server/server.rs, chúng ta có thể đưa mã được tạo vào phạm vi thông qua macro include_generated_proto! của gRPC và nhập đặc điểm RouteGuidePoint.

mod grpc_pb {
    grpc::include_generated_proto!("generated", "routeguide");
}

pub use grpc_pb::{
    route_guide_server::{RouteGuideServer, RouteGuide},
    Point, Feature, Rectangle, RouteNote, RouteSummary
};

Chúng ta có thể bắt đầu bằng cách xác định một cấu trúc để biểu thị dịch vụ của mình. Hiện tại, chúng ta có thể thực hiện việc này trên src/server/server.rs:

#[derive(Debug)]
pub struct RouteGuideService {
    features: Vec<Feature>,
}

Bây giờ, chúng ta cần triển khai đặc điểm route_guide_server::RouteGuide từ mã đã tạo.

Triển khai RouteGuide

Chúng ta cần triển khai giao diện RouteGuide đã tạo. Đây là cách triển khai. Nội dung này đã có trong mẫu.

#[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> {
        ...
    }
}

Hãy cùng tìm hiểu chi tiết về từng cách triển khai RPC.

RPC truyền trực tuyến phía máy chủ

Hãy bắt đầu với ListFeatures. Đây là một RPC truyền trực tuyến phía máy chủ, vì vậy chúng ta cần gửi lại nhiều Feature cho ứng dụng.

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)))
    }

Như bạn thấy, chúng ta nhận được một đối tượng yêu cầu (Rectangle mà ứng dụng muốn tìm Features). Lần này, chúng ta cần trả về một luồng giá trị. Chúng ta sẽ tạo một kênh và tạo ra một tác vụ không đồng bộ mới, trong đó chúng ta thực hiện một thao tác tra cứu, gửi các đối tượng đáp ứng các ràng buộc của chúng ta vào kênh. Nửa Stream của kênh được trả về cho phương thức gọi, được bao bọc trong một tonic::Response.

RPC truyền phát trực tiếp phía máy khách

Bây giờ, hãy xem xét một phương thức phức tạp hơn một chút: phương thức truyền trực tuyến phía máy khách RecordRoute, trong đó chúng ta nhận được một luồng Points từ máy khách và trả về một RouteSummary duy nhất có thông tin về chuyến đi của họ. Thao tác này nhận một luồng làm dữ liệu đầu vào mà máy chủ có thể dùng để vừa đọc vừa ghi thông báo. Nó có thể lặp lại các thông báo của ứng dụng bằng phương thức next() và trả về một phản hồi duy nhất.

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))
    }

Trong phần nội dung phương thức, chúng ta sử dụng phương thức next() của luồng để liên tục đọc các yêu cầu của ứng dụng khách vào một đối tượng yêu cầu (trong trường hợp này là Point) cho đến khi không còn thông báo nào nữa. Nếu giá trị này là None, thì luồng vẫn hoạt động tốt và có thể tiếp tục đọc.

RPC truyền trực tuyến hai chiều

Cuối cùng, hãy xem RPC truyền phát trực tiếp hai chiều 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)))
    }

Lần này, chúng ta sẽ nhận được một luồng mà, như trong ví dụ về truyền phát trực tiếp phía máy khách, có thể dùng để đọc và ghi tin nhắn. Tuy nhiên, lần này chúng ta sẽ trả về các giá trị thông qua luồng của phương thức trong khi máy khách vẫn đang ghi thông báo vào luồng thông báo của họ. Cú pháp để đọc và ghi ở đây rất giống với phương thức truyền phát trực tiếp phía máy khách của chúng tôi, ngoại trừ việc máy chủ trả về một RouteChatStream. Mặc dù mỗi bên sẽ luôn nhận được tin nhắn của bên kia theo thứ tự chúng được viết, nhưng cả máy khách và máy chủ đều có thể đọc và ghi theo bất kỳ thứ tự nào – các luồng hoạt động hoàn toàn độc lập.

Chúng ta tạo luồng output bằng try_stream!, cho biết rằng luồng có thể trả về lỗi.

Khởi động máy chủ

Sau khi triển khai phương thức này, chúng ta cũng cần khởi động một máy chủ gRPC để máy khách có thể thực sự sử dụng dịch vụ của chúng ta. Điền thông tin vào 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(())
}

Sau đây là những gì diễn ra trong main(), từng bước:

  1. Chỉ định cổng mà chúng ta muốn sử dụng để theo dõi các yêu cầu của máy khách
  2. Tạo RouteGuideService có các tính năng được tải vào
  3. Tạo một phiên bản của máy chủ gRPC bằng RouteGuideServer::new() bằng dịch vụ mà chúng ta đã tạo.
  4. Đăng ký việc triển khai dịch vụ của chúng tôi với máy chủ gRPC.
  5. Gọi serve() trên máy chủ bằng thông tin chi tiết về cổng của chúng tôi để thực hiện thao tác chờ chặn cho đến khi quy trình bị huỷ.

6. Tạo ứng dụng

Trong phần này, chúng ta sẽ xem xét việc tạo một ứng dụng Rust cho dịch vụ RouteGuide trong src/client/client.rs.

Trước tiên, hãy đưa mã đã tạo vào phạm vi.

mod grpc_pb {
    grpc::include_generated_proto!("generated", "routeguide");
}

use grpc_pb::route_guide_client::RouteGuideClient;
use grpc_pb::{Point, Rectangle, RouteNote};

Gọi các phương thức dịch vụ

Bây giờ, hãy xem cách chúng ta gọi các phương thức dịch vụ. Trong gRPC-Rust, các RPC hoạt động ở chế độ chặn/đồng bộ, tức là lệnh gọi RPC sẽ đợi máy chủ phản hồi và sẽ trả về phản hồi hoặc lỗi.

RPC truyền trực tuyến phía máy chủ

Đây là nơi chúng ta gọi phương thức truyền trực tuyến phía máy chủ ListFeatures, phương thức này trả về một luồng các đối tượng địa lý 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(())
}

Chúng ta truyền một yêu cầu cho phương thức này và nhận lại một thực thể ListFeaturesStream. Ứng dụng có thể dùng luồng ListFeaturesStream để đọc các phản hồi của máy chủ. Chúng ta sử dụng phương thức message() của ListFeaturesStream để đọc đi đọc lại các phản hồi của máy chủ thành một đối tượng bộ đệm giao thức phản hồi (trong trường hợp này là Feature) cho đến khi không còn thông báo nào nữa.

RPC truyền phát trực tiếp phía máy khách

Ở đây, đối với record_route, chúng ta chuyển một vectơ điểm thành một luồng. Sau đó, chúng ta sẽ truyền luồng này vào record_route() dưới dạng một yêu cầu và nhận được một phản hồi RouteSummary duy nhất sau khi máy chủ xử lý xong luồng.

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 truyền trực tuyến hai chiều

Cuối cùng, hãy xem RPC truyền phát trực tiếp hai chiều RouteChat(). Chúng ta truyền cho phương thức một yêu cầu truyền phát mà chúng ta ghi vào và nhận lại một luồng mà chúng ta có thể đọc tin nhắn từ đó. Lần này, chúng ta sẽ trả về các giá trị thông qua luồng của phương thức trong khi máy chủ vẫn đang ghi thông báo vào luồng thông báo của chúng.

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(())
}

Mặc dù mỗi bên sẽ luôn nhận được tin nhắn của bên kia theo thứ tự chúng được viết, nhưng cả máy khách và máy chủ đều có thể đọc và ghi theo bất kỳ thứ tự nào – các luồng hoạt động hoàn toàn độc lập.

Gọi các phương thức trợ giúp

Để gọi các phương thức dịch vụ, trước tiên, chúng ta cần tạo một kênh để giao tiếp với máy chủ. Chúng ta tạo mục này bằng cách tạo một điểm cuối, kết nối với điểm cuối đó và truyền kênh được tạo khi kết nối với RouteGuideClient::new() như sau:

#[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(())
}

Trong main(), hãy thực thi các phương thức mà chúng ta vừa tạo.

#[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. Dùng thử

Trước tiên, để chạy Client và Server, hãy thêm chúng làm mục tiêu nhị phân vào crate của chúng ta. Chúng ta cần chỉnh sửa Cargo.toml cho phù hợp:

[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"

[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"

Cũng như mọi dự án, chúng ta cũng cần nghĩ đến những phần phụ thuộc cần thiết để mã của chúng ta hoạt động. Đối với các dự án Rust, các phần phụ thuộc sẽ nằm trong Cargo.toml. Chúng tôi đã liệt kê các phần phụ thuộc cần thiết trong tệp Cargo.toml.

Sau đó, hãy thực thi các lệnh sau từ thư mục làm việc của chúng tôi:

  1. Chạy máy chủ trong một dòng lệnh:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server 
  1. Chạy ứng dụng từ một thiết bị đầu cuối khác:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client

Bạn sẽ thấy kết quả như sau:

*** 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. Bước tiếp theo