1. Pengantar
Dalam codelab ini, Anda akan menggunakan gRPC-Rust untuk membuat klien dan server yang membentuk fondasi aplikasi pemetaan rute yang ditulis dalam Rust.
Di akhir tutorial, Anda akan memiliki klien yang terhubung ke server jarak jauh menggunakan gRPC untuk mendapatkan informasi tentang fitur di rute klien, membuat ringkasan rute klien, dan bertukar informasi rute seperti info terbaru lalu lintas dengan server dan klien lain.
Layanan ini ditentukan dalam file Protocol Buffers, yang akan digunakan untuk membuat kode boilerplate bagi klien dan server sehingga keduanya dapat berkomunikasi satu sama lain, sehingga menghemat waktu dan upaya Anda dalam menerapkan fungsi tersebut.
Kode yang dihasilkan ini tidak hanya menangani kompleksitas komunikasi antara server dan klien, tetapi juga serialisasi dan deserialisasi data.
Yang akan Anda pelajari
- Cara menggunakan Protocol Buffers untuk menentukan API layanan.
- Cara membangun klien dan server berbasis gRPC dari definisi Protocol Buffers menggunakan pembuatan kode otomatis.
- Pemahaman tentang komunikasi streaming klien-server dengan gRPC.
Codelab ini ditujukan bagi developer Rust yang baru menggunakan gRPC atau ingin mempelajari kembali gRPC, atau siapa pun yang tertarik untuk membangun sistem terdistribusi. Tidak diperlukan pengalaman gRPC sebelumnya.
2. Sebelum memulai
Prasyarat
Pastikan Anda telah menginstal berikut ini:
Mendapatkan kode
Agar Anda tidak perlu memulai dari awal, codelab ini menyediakan kerangka kode sumber aplikasi yang dapat Anda selesaikan. Langkah-langkah berikut akan menunjukkan cara menyelesaikan aplikasi, termasuk menggunakan plugin compiler buffer protokol untuk membuat kode gRPC boilerplate.
Pertama, buat direktori kerja codelab dan cd
ke dalamnya:
mkdir streaming-grpc-rust-getting-started && cd streaming-grpc-rust-getting-started
Download dan ekstrak codelab:
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
Atau, Anda dapat mendownload file .zip yang hanya berisi direktori codelab dan mengekstraknya secara manual.
Kode sumber yang sudah selesai tersedia di GitHub jika Anda ingin melewati pengetikan implementasi.
3. Menentukan pesan dan layanan
Langkah pertama Anda adalah menentukan layanan gRPC aplikasi, metode RPC, dan jenis pesan permintaan dan responsnya menggunakan Protocol Buffers. Layanan Anda akan menyediakan:
- Metode RPC yang disebut
ListFeatures
,RecordRoute
, danRouteChat
yang diimplementasikan server dan dipanggil klien. - Jenis pesan
Point
,Feature
,Rectangle
,RouteNote
, danRouteSummary
, yang merupakan struktur data yang dipertukarkan antara klien dan server saat memanggil metode di atas.
Metode RPC ini dan jenis pesannya akan ditentukan dalam file proto/routeguide.proto
kode sumber yang diberikan.
Protocol Buffers biasanya dikenal sebagai protobuf. Untuk mengetahui informasi selengkapnya tentang terminologi gRPC, lihat Konsep inti, arsitektur, dan siklus proses gRPC.
Menentukan Jenis pesan
Pertama, mari kita tentukan pesan yang akan digunakan oleh RPC kita. Dalam file routeguide/route_guide.proto
kode sumber, tentukan terlebih dahulu jenis pesan Point
. Point
mewakili pasangan koordinat lintang-bujur di peta. Untuk codelab ini, gunakan bilangan bulat untuk koordinat:
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
Angka 1
dan 2
adalah nomor ID unik untuk setiap kolom dalam struktur message
.
Selanjutnya, tentukan jenis pesan Feature
. Feature
menggunakan kolom string
untuk nama atau alamat pos sesuatu di lokasi yang ditentukan oleh Point
:
message Feature {
// The name or address of the feature.
string name = 1;
// The point where the feature is located.
Point location = 2;
}
Selanjutnya, pesan Rectangle
yang merepresentasikan persegi panjang lintang-bujur, yang direpresentasikan sebagai dua titik yang berlawanan secara diagonal "lo" dan "hi".
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
Juga pesan RouteNote
yang merepresentasikan pesan yang dikirim pada titik tertentu.
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
Kami juga akan memerlukan pesan RouteSummary
. Pesan ini diterima sebagai respons terhadap RPC RecordRoute
yang dijelaskan di bagian berikutnya. Objek ini berisi jumlah titik individual yang diterima, jumlah fitur yang terdeteksi, dan total jarak yang ditempuh sebagai jumlah kumulatif jarak antara setiap titik.
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;
}
Menentukan metode layanan
Pertama, tentukan layanan kita, lalu tentukan pesan kita nanti. Untuk menentukan layanan, Anda menentukan layanan bernama dalam file .proto
. File proto/routeguide.proto
memiliki struktur service
bernama RouteGuide
yang menentukan satu atau beberapa metode yang disediakan oleh layanan aplikasi.
Tentukan metode RPC di dalam definisi layanan Anda, dengan menentukan jenis permintaan dan responsnya. Di bagian codelab ini, mari kita tentukan:
ListFeatures
Mendapatkan Feature
yang tersedia dalam Rectangle
tertentu. Hasil di-streaming, bukan ditampilkan sekaligus (misalnya, dalam pesan respons dengan kolom berulang), karena persegi panjang dapat mencakup area yang luas dan berisi sejumlah besar fitur.
Jenis yang sesuai untuk RPC ini adalah RPC streaming sisi server: klien mengirim permintaan ke server dan mendapatkan aliran untuk membaca kembali urutan pesan. Klien membaca dari stream yang ditampilkan hingga tidak ada lagi pesan. Seperti yang dapat Anda lihat dalam contoh kami, Anda menentukan metode streaming sisi server dengan menempatkan kata kunci stream
sebelum jenis respons.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
Menerima aliran Point
di rute yang dilalui, menampilkan RouteSummary
saat traversal selesai.
RPC streaming sisi klien tampaknya sesuai dalam kasus ini: klien menulis urutan pesan dan mengirimkannya ke server, lagi-lagi menggunakan stream yang disediakan. Setelah klien selesai menulis pesan, klien akan menunggu server membaca semua pesan tersebut dan menampilkan responsnya. Anda menentukan metode streaming sisi klien dengan menempatkan kata kunci stream
sebelum jenis permintaan.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
Menerima aliran RouteNote
yang dikirim saat rute sedang dilalui, sambil menerima RouteNote
lainnya (misalnya, dari pengguna lain).
Inilah jenis kasus penggunaan yang tepat untuk streaming dua arah. RPC streaming dua arah membuat kedua sisi mengirim urutan pesan menggunakan stream baca-tulis. Kedua aliran beroperasi secara independen, sehingga klien dan server dapat membaca dan menulis dalam urutan apa pun yang mereka inginkan.
Misalnya, server dapat menunggu untuk menerima semua pesan klien sebelum menulis responsnya, atau dapat membaca pesan lalu menulis pesan secara bergantian, atau kombinasi baca dan tulis lainnya.
Urutan pesan di setiap aliran akan tetap terjaga. Anda menentukan jenis metode ini dengan menempatkan kata kunci stream
sebelum permintaan dan respons.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
4. Buat kode klien dan server
Kami telah memberikan kode yang dihasilkan dari file .proto
di direktori yang dihasilkan.
Jika Anda ingin mempelajari cara membuat kode dari file .proto
sendiri atau membuat perubahan pada file .proto
dan mengujinya, lihat petunjuk ini.
Kode yang dihasilkan berisi:
- Definisi struct untuk jenis pesan
Point
,Feature
,Rectangle
,RouteNote
, danRouteSummary
. - Trait layanan yang perlu kita terapkan:
route_guide_server::RouteGuide
. - Jenis klien yang akan kita gunakan untuk memanggil server:
route_guide_client::RouteGuideClient<T>
.
Selanjutnya, kita akan menerapkan metode di sisi server, sehingga saat klien mengirim permintaan, server dapat membalas dengan jawaban.
5. Menerapkan layanan
Pertama, mari kita lihat cara membuat server RouteGuide
. Ada dua bagian untuk membuat layanan RouteGuide
kami melakukan tugasnya:
- Mengimplementasikan antarmuka layanan yang dihasilkan dari definisi layanan kita: melakukan "pekerjaan" layanan kita yang sebenarnya.
- Menjalankan server gRPC untuk memproses permintaan dari klien dan mengirimkannya ke implementasi metode yang tepat.
Di src/server/server.rs
, kita dapat memasukkan kode yang dihasilkan ke dalam cakupan melalui makro include_generated_proto!
gRPC dan mengimpor trait RouteGuide
dan Point
.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
pub use grpc_pb::{
route_guide_server::{RouteGuideServer, RouteGuide},
Point, Feature, Rectangle, RouteNote, RouteSummary
};
Kita dapat memulai dengan menentukan struct untuk merepresentasikan layanan kita. Kita dapat melakukannya di src/server/server.rs
untuk saat ini:
#[derive(Debug)]
pub struct RouteGuideService {
features: Vec<Feature>,
}
Sekarang, kita perlu menerapkan trait route_guide_server::RouteGuide
dari kode yang dihasilkan.
Mengimplementasikan RouteGuide
Kita perlu menerapkan antarmuka RouteGuide
yang dihasilkan. Seperti inilah tampilan implementasinya. Ini sudah ada di template.
#[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> { ... } }
Mari kita pelajari setiap implementasi RPC secara mendetail.
RPC streaming sisi server
Mari kita mulai dengan ListFeatures
. Ini adalah RPC streaming sisi server, jadi kita perlu mengirim kembali beberapa Feature
ke klien.
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)))
}
Seperti yang dapat Anda lihat, kita mendapatkan objek permintaan (Rectangle
tempat klien kita ingin menemukan Features
). Kali ini, kita perlu menampilkan aliran nilai. Kita membuat channel dan memunculkan tugas asinkron baru tempat kita melakukan pencarian, mengirimkan fitur yang memenuhi batasan kita ke dalam channel. Setengah bagian Stream dari saluran ditampilkan ke pemanggil, yang di-wrap dalam tonic::Response
.
RPC streaming sisi klien
Sekarang mari kita lihat sesuatu yang sedikit lebih rumit: metode streaming sisi klien RecordRoute
, tempat kita mendapatkan aliran Points
dari klien dan menampilkan satu RouteSummary
dengan informasi tentang perjalanannya. API ini mendapatkan aliran data sebagai input, yang dapat digunakan server untuk membaca dan menulis pesan. Aplikasi ini dapat melakukan iterasi melalui pesan klien menggunakan metode next()
dan menampilkan satu responsnya.
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))
}
Dalam isi metode, kita menggunakan metode next()
stream untuk berulang kali membaca permintaan klien ke objek permintaan (dalam hal ini Point
) hingga tidak ada lagi pesan. Jika ini adalah None, streaming masih bagus dan dapat terus membaca.
RPC streaming dua arah
Terakhir, mari kita lihat RPC streaming dua arah 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)))
}
Kali ini kita mendapatkan stream yang, seperti dalam contoh streaming sisi klien, dapat digunakan untuk membaca dan menulis pesan. Namun, kali ini kita menampilkan nilai melalui stream metode saat klien masih menulis pesan ke stream pesannya. Sintaksis untuk membaca dan menulis di sini sangat mirip dengan metode streaming klien kami, kecuali server menampilkan RouteChatStream
. Meskipun setiap sisi akan selalu mendapatkan pesan dari sisi lain dalam urutan penulisannya, klien dan server dapat membaca dan menulis dalam urutan apa pun — aliran beroperasi sepenuhnya secara independen.
Kita membuat streaming output
menggunakan try_stream!
, yang menunjukkan bahwa streaming dapat menampilkan error.
Mulai server
Setelah menerapkan metode ini, kita juga perlu memulai server gRPC agar klien dapat menggunakan layanan kita. Isi 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(())
}
Berikut yang terjadi di main()
, langkah demi langkah:
- Tentukan port yang ingin kita gunakan untuk memproses permintaan klien
- Buat
RouteGuideService
dengan fitur yang dimuat - Buat instance server gRPC menggunakan
RouteGuideServer::new()
menggunakan layanan yang kita buat. - Daftarkan implementasi layanan kita dengan server gRPC.
- Panggil
serve()
di server dengan detail port kami untuk melakukan penantian pemblokiran hingga proses dihentikan.
6. Buat klien
Di bagian ini, kita akan melihat cara membuat klien Rust untuk layanan RouteGuide di src/client/client.rs
.
Pertama, bawa kode yang dihasilkan ke dalam cakupan.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
use grpc_pb::route_guide_client::RouteGuideClient;
use grpc_pb::{Point, Rectangle, RouteNote};
Panggil metode layanan
Sekarang, mari kita lihat cara memanggil metode layanan. Di gRPC-Rust, RPC beroperasi dalam mode pemblokiran/sinkron, yang berarti bahwa panggilan RPC menunggu server merespons, dan akan menampilkan respons atau error.
RPC streaming sisi server
Di sini, kita memanggil metode streaming sisi server ListFeatures
, yang menampilkan aliran objek Feature
geografis.
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(())
}
Kita meneruskan permintaan ke metode dan mendapatkan kembali instance ListFeaturesStream
. Klien dapat menggunakan stream ListFeaturesStream
untuk membaca respons server. Kita menggunakan metode ListFeaturesStream
's message()
untuk berulang kali membaca respons server ke objek buffer protokol respons (dalam hal ini Feature
) hingga tidak ada lagi pesan.
RPC streaming sisi klien
Di sini untuk record_route
, kita mengubah vektor titik menjadi aliran. Kemudian, kita meneruskan aliran ini ke record_route()
sebagai permintaan dan mendapatkan satu respons RouteSummary
setelah aliran diproses sepenuhnya oleh server.
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 streaming dua arah
Terakhir, mari kita lihat RPC streaming dua arah RouteChat()
. Kita meneruskan permintaan streaming metode yang kita tulis dan mendapatkan kembali streaming yang dapat kita baca pesannya. Kali ini kita menampilkan nilai melalui stream metode saat server masih menulis pesan ke stream pesannya.
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(())
}
Meskipun setiap sisi akan selalu mendapatkan pesan dari sisi lain dalam urutan penulisannya, klien dan server dapat membaca dan menulis dalam urutan apa pun — aliran beroperasi sepenuhnya secara independen.
Memanggil metode bantuan
Untuk memanggil metode layanan, kita harus membuat channel terlebih dahulu untuk berkomunikasi dengan server. Kita membuatnya dengan terlebih dahulu membuat endpoint, menghubungkan ke endpoint tersebut, dan meneruskan channel yang dibuat saat terhubung ke RouteGuideClient::new()
sebagai berikut:
#[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(())
}
Di main()
, jalankan metode yang baru saja kita buat.
#[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. Cobalah
Pertama, untuk menjalankan Klien dan Server, mari kita tambahkan sebagai target biner ke crate. Kita perlu mengedit Cargo.toml sesuai dengan itu:
[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"
[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"
Seperti project lainnya, kita juga perlu memikirkan dependensi yang diperlukan agar kode kita berfungsi. Untuk project Rust, dependensi akan ada di Cargo.toml
. Kita telah mencantumkan dependensi yang diperlukan dalam file Cargo.toml
.
Kemudian, jalankan perintah berikut dari direktori kerja kita:
- Jalankan server di satu terminal:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server
- Jalankan klien dari terminal lain:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client
Anda akan melihat output seperti ini:
*** 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. Langkah berikutnya
- Pelajari cara kerja gRPC di Pengantar gRPC dan Konsep inti.
- Pelajari tutorial Dasar-Dasar.