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 tên hoặc địa chỉ bưu chính của nội dung nằm tại một toạ độ cụ thể trên bản đồ. Một ứng dụng hoàn chỉnh có thể sử dụng thiết kế máy chủ-máy khách này để liệt kê hoặc tóm tắt các địa điểm yêu thích dọc theo một tuyến đường.
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 máy khách-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 cho lớp học lập trình rồi chuyển đến thư mục đó:
mkdir grpc-rust-getting-started && cd 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-getting-started/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 dịch vụ
Bước đầu tiên là xác định dịch vụ gRPC của ứng dụng, 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:
- Một phương thức RPC có tên là
GetFeature
mà máy chủ triển khai và máy khách gọi. - Các loại thông báo
Point
vàFeature
là các cấu trúc dữ liệu được trao đổi giữa ứng dụng và máy chủ khi sử dụng phương thứcGetFeature
. Ứng dụng cung cấp toạ độ trên bản đồ dưới dạngPoint
trong yêu cầuGetFeature
gửi đến máy chủ, còn máy chủ sẽ trả lời bằng mộtFeature
tương ứng mô tả mọi thứ nằm ở toạ độ đó.
Phương thức RPC này và các loại thông báo của phương thức này sẽ được xác định trong tệp proto/route_guide.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.
Phương thức dịch vụ
Trước tiên, hãy xác định các phương thức dịch vụ rồi xác định các loại thông báo Point
và Feature
. 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.
Thêm phương thức rpc
GetFeature
vào bên trong định nghĩa RouteGuide
. Như đã giải thích trước đó, phương thức này sẽ tra cứu tên hoặc địa chỉ của một vị trí từ một tập hợp toạ độ nhất định, vì vậy, hãy để GetFeature
trả về một Feature
cho một Point
nhất định:
service RouteGuide {
// Definition of the service goes here
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
}
Đây là một phương thức RPC đơn phương: một RPC đơn giản, trong đó ứng dụng gửi một yêu cầu đến máy chủ và đợi phản hồi quay lại, giống như một lệnh gọi hàm cục bộ.
Loại thông báo
Trong tệp proto/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ố 1
và 2
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;
}
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.
Như với mọi dự án, chúng ta cần nghĩ đến những phần phụ thuộc cần thiết cho mã của mình. Đố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
.
Nếu bạn muốn tự tìm hiểu cách tạo mã từ tệp .proto
, 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
vàFeature
. - 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 phương thức GetFeature
ở phía máy chủ để khi ứng dụng gửi yêu cầu, máy chủ có thể trả lời.
5. Triển khai dịch vụ
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 RouteGuide
và Point
.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
pub use grpc_pb::{
route_guide_server::{RouteGuideServer, RouteGuide},
Point, Feature,
};
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.
Unary RPC
RouteGuideService
triển khai tất cả các phương thức dịch vụ của chúng tôi. Hàm get_feature
ở phía máy chủ là nơi thực hiện công việc chính: hàm này nhận một thông báo Point
từ máy khách và trả về thông tin vị trí tương ứng trong một thông báo Feature
từ danh sách các địa điểm đã biết. Dưới đây là cách triển khai hàm trong 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()))
}
}
Trong phương thức này, hãy điền thông tin thích hợp cho Point
đã cho vào một đối tượng Feature
, rồi trả về đối tượng đó.
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. Thay thế main()
bằng mã này.
#[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:
- 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
- Tạo một
RouteGuideService
có các tính năng được tải bằng cách gọi hàm trợ giúpload()
- 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. - Đăng ký việc triển khai dịch vụ của chúng tôi với máy chủ gRPC.
- 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
.
Như đã làm 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_code!
của gRPC và nhập loại RouteGuideClient
.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
use grpc_pb::{
route_guide_client::RouteGuideClient,
Point,
};
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.
Để 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 hàm này, khi tạo ứng dụng, chúng ta sẽ bao bọc kênh chung đã tạo ở trên bằng phần gốc mã đã tạo để triển khai các phương thức cụ thể được xác định trong dịch vụ .proto.
RPC đơn giản
Việc gọi RPC GetFeature
đơn giản gần như dễ dàng như gọi một phương thức cục bộ. Thêm nội dung này vào 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(())
Như bạn có thể thấy, chúng ta gọi phương thức trên phần bổ trợ mà chúng ta đã nhận được trước đó. Trong các tham số phương thức, chúng ta tạo và điền sẵn một đối tượng vùng đệm giao thức yêu cầu (trong trường hợp này là Point
). Nếu lệnh gọi không trả về lỗi, thì chúng ta có thể đọc thông tin phản hồi từ máy chủ qua giá trị trả về đầu tiên.
println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
response.name(),
response.location().latitude(),
response.location().longitude());
Nhìn chung, hàm main()
của ứng dụng sẽ có dạng 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);
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. 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 và thêm nội dung sau:
[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"
[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"
Sau đó, hãy thực thi các lệnh sau từ thư mục đang hoạt động của chúng ta:
- Chạy máy chủ trong một dòng lệnh:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server
- 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, trong đó dấu thời gian đã bị bỏ qua để đảm bảo sự rõ ràng:
*** SIMPLE RPC *** FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906
8. Bước tiếp theo
- Tìm hiểu cách gRPC hoạt động trong phần Giới thiệu về gRPC và Các khái niệm cốt lõi
- Xem Hướng dẫn cơ bản