การเริ่มต้นใช้งาน gRPC-Rust

1. บทนำ

ในโค้ดแล็บนี้ คุณจะได้ใช้ gRPC-Rust เพื่อสร้างไคลเอ็นต์และเซิร์ฟเวอร์ซึ่งเป็นรากฐานของแอปพลิเคชันการแมปเส้นทางที่เขียนด้วย Rust

เมื่อจบบทแนะนำนี้ คุณจะมีไคลเอ็นต์ที่เชื่อมต่อกับเซิร์ฟเวอร์ระยะไกลโดยใช้ gRPC เพื่อรับชื่อหรือที่อยู่ไปรษณีย์ของสิ่งที่อยู่ ณ พิกัดที่เฉพาะเจาะจงบนแผนที่ แอปพลิเคชันที่สมบูรณ์อาจใช้การออกแบบไคลเอ็นต์-เซิร์ฟเวอร์นี้เพื่อแจงนับหรือสรุปจุดที่น่าสนใจตามเส้นทาง

บริการนี้กำหนดไว้ในไฟล์ Protocol Buffers ซึ่งจะใช้เพื่อสร้างโค้ดมาตรฐานสำหรับไคลเอ็นต์และเซิร์ฟเวอร์เพื่อให้สื่อสารกันได้ ซึ่งจะช่วยประหยัดเวลาและแรงในการติดตั้งใช้งานฟังก์ชันดังกล่าว

โค้ดที่สร้างขึ้นนี้ไม่เพียงแต่จัดการความซับซ้อนของการสื่อสารระหว่างเซิร์ฟเวอร์และไคลเอ็นต์เท่านั้น แต่ยังจัดการการซีเรียลไลซ์และการดีซีเรียลไลซ์ข้อมูลด้วย

สิ่งที่คุณจะได้เรียนรู้

  • วิธีใช้ Protocol Buffers เพื่อกำหนด API ของบริการ
  • วิธีสร้างไคลเอ็นต์และเซิร์ฟเวอร์ที่ใช้ gRPC จากคำจำกัดความของ Protocol Buffers โดยใช้การสร้างโค้ดอัตโนมัติ
  • ความเข้าใจเกี่ยวกับการสื่อสารระหว่างไคลเอ็นต์กับเซิร์ฟเวอร์ด้วย gRPC

Codelab นี้มีไว้สำหรับนักพัฒนา Rust ที่เพิ่งเริ่มใช้ gRPC หรือต้องการทบทวน gRPC หรือใครก็ตามที่สนใจสร้างระบบแบบกระจาย ไม่จำเป็นต้องมีประสบการณ์เกี่ยวกับ gRPC มาก่อน

2. ก่อนเริ่มต้น

ข้อกำหนดเบื้องต้น

ตรวจสอบว่าคุณได้ติดตั้งสิ่งต่อไปนี้แล้ว

รับโค้ด

Codelab นี้มีโครงสร้างของซอร์สโค้ดของแอปพลิเคชันเพื่อให้คุณทำต่อได้ คุณจึงไม่ต้องเริ่มต้นจากศูนย์ ขั้นตอนต่อไปนี้จะแสดงวิธีส่งแอปพลิเคชันให้เสร็จสมบูรณ์ ซึ่งรวมถึงการใช้ปลั๊กอินคอมไพเลอร์ Protocol Buffer เพื่อสร้างโค้ด gRPC ที่ซ้ำกัน

ก่อนอื่น ให้สร้างไดเรกทอรีการทำงานของ Codelab แล้วใช้คำสั่ง cd เพื่อเข้าไปในไดเรกทอรีดังกล่าว

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

ดาวน์โหลดและแตกไฟล์ 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-getting-started/start_here

หรือคุณจะดาวน์โหลดไฟล์ .zip ที่มีเฉพาะไดเรกทอรี Codelab แล้วแตกไฟล์ด้วยตนเองก็ได้

ซอร์สโค้ดที่เสร็จสมบูรณ์แล้วพร้อมใช้งานใน GitHub หากคุณไม่ต้องการพิมพ์การติดตั้งใช้งาน

3. กำหนดบริการ

ขั้นตอนแรกคือการกำหนดบริการ gRPC ของแอปพลิเคชัน เมธอด RPC รวมถึงประเภทข้อความคำขอและการตอบกลับโดยใช้ Protocol Buffers บริการของคุณจะให้ข้อมูลต่อไปนี้

  • เมธอด RPC ที่ชื่อ GetFeature ซึ่งเซิร์ฟเวอร์นำมาใช้และไคลเอ็นต์เรียกใช้
  • ประเภทข้อความ Point และ Feature ซึ่งเป็นโครงสร้างข้อมูลที่แลกเปลี่ยนระหว่างไคลเอ็นต์กับเซิร์ฟเวอร์เมื่อใช้เมธอด GetFeature ไคลเอ็นต์จะระบุพิกัดแผนที่เป็น Point ในคำขอ GetFeature ไปยังเซิร์ฟเวอร์ และเซิร์ฟเวอร์จะตอบกลับด้วย Feature ที่เกี่ยวข้องซึ่งอธิบายสิ่งที่อยู่ในพิกัดเหล่านั้น

วิธีการ RPC นี้และประเภทข้อความของวิธีการนี้จะกำหนดไว้ในไฟล์ proto/route_guide.proto ของซอร์สโค้ดที่ระบุ

Protocol Buffers เรียกกันโดยทั่วไปว่า protobuf ดูข้อมูลเพิ่มเติมเกี่ยวกับคำศัพท์ gRPC ได้ที่แนวคิดหลัก สถาปัตยกรรม และวงจรของ gRPC

วิธีการบริการ

ก่อนอื่นเรามากำหนดเมธอดบริการ แล้วกำหนดประเภทข้อความ Point และ Feature กัน ไฟล์ proto/routeguide.proto มีโครงสร้าง service ชื่อ RouteGuide ซึ่งกำหนดวิธีการอย่างน้อย 1 วิธีที่บริการของแอปพลิเคชันมีให้

เพิ่มrpc method GetFeature ภายในคำจำกัดความของ RouteGuide ดังที่อธิบายไว้ก่อนหน้านี้ วิธีนี้จะค้นหาชื่อหรือที่อยู่ของสถานที่จากชุดพิกัดที่กำหนด ดังนั้นให้ GetFeature แสดง Feature สำหรับ Point ที่กำหนด

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

นี่คือเมธอด RPC แบบเอกภาค ซึ่งเป็น RPC อย่างง่ายที่ไคลเอ็นต์ส่งคำขอไปยังเซิร์ฟเวอร์และรอให้เซิร์ฟเวอร์ส่งการตอบกลับกลับมา เหมือนกับการเรียกฟังก์ชันในเครื่อง

ประเภทข้อความ

ในproto/route_guide.protoไฟล์ของซอร์สโค้ด ให้กำหนดPointประเภทข้อความก่อน Point แสดงคู่พิกัดละติจูดและลองจิจูดบนแผนที่ สำหรับ Codelab นี้ ให้ใช้จำนวนเต็มสำหรับพิกัด

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

4. สร้างโค้ดไคลเอ็นต์และเซิร์ฟเวอร์

เราได้ให้โค้ดที่สร้างจากไฟล์ .proto ในไดเรกทอรีที่สร้างแล้วแก่คุณแล้ว

เช่นเดียวกับโปรเจ็กต์อื่นๆ เราต้องพิจารณาการขึ้นต่อกันที่จำเป็นสำหรับโค้ดของเรา สำหรับโปรเจ็กต์ Rust การอ้างอิงจะอยู่ใน Cargo.toml เราได้แสดงรายการทรัพยากร Dependency ที่จำเป็นไว้ในไฟล์ Cargo.toml แล้ว

หากต้องการดูวิธีสร้างโค้ดจากไฟล์ .proto ด้วยตนเอง โปรดดูวิธีการเหล่านี้

โค้ดที่สร้างขึ้นประกอบด้วย

  • คำจำกัดความของโครงสร้างสำหรับประเภทข้อความ Point และ Feature
  • ลักษณะบริการที่เราต้องใช้คือ route_guide_server::RouteGuide
  • ประเภทไคลเอ็นต์ที่เราจะใช้เพื่อเรียกเซิร์ฟเวอร์: route_guide_client::RouteGuideClient<T>

จากนั้นเราจะใช้เมธอด GetFeature ในฝั่งเซิร์ฟเวอร์เพื่อให้เมื่อไคลเอ็นต์ส่งคำขอ เซิร์ฟเวอร์จะตอบกลับได้

5. ติดตั้งใช้งานบริการ

ใน 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,
};

เราสามารถเริ่มต้นด้วยการกำหนดโครงสร้างเพื่อแสดงบริการของเรา โดยทำได้ใน src/server/server.rs ในตอนนี้

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

ตอนนี้เราต้องใช้ลักษณะ route_guide_server::RouteGuide จากโค้ดที่สร้างขึ้น

Unary RPC

RouteGuideService ใช้ทุกวิธีการบริการของเรา ฟังก์ชัน get_feature ในฝั่งเซิร์ฟเวอร์คือที่ที่ระบบทำงานหลัก โดยจะรับข้อความ Point จากไคลเอ็นต์และส่งกลับในข้อความ Feature ซึ่งเป็นข้อมูลตำแหน่งที่สอดคล้องกันจากรายการสถานที่ที่รู้จัก การใช้งานฟังก์ชันใน 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()))
    }
}

ในเมธอด ให้ป้อนข้อมูลที่เหมาะสมสำหรับ Point ที่ระบุลงในออบเจ็กต์ Feature แล้วส่งคืน

เมื่อใช้วิธีนี้แล้ว เราก็ต้องเริ่มเซิร์ฟเวอร์ 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() มีดังนี้

  1. ระบุพอร์ตที่เราต้องการใช้เพื่อรอรับคำขอของไคลเอ็นต์
  2. สร้าง RouteGuideService โดยโหลดฟีเจอร์ด้วยการเรียกใช้ฟังก์ชันตัวช่วย load()
  3. สร้างอินสแตนซ์ของเซิร์ฟเวอร์ gRPC โดยใช้ RouteGuideServer::new() โดยใช้บริการที่เราสร้างขึ้น
  4. ลงทะเบียนการติดตั้งใช้งานบริการกับเซิร์ฟเวอร์ gRPC
  5. เรียกใช้ serve() ในเซิร์ฟเวอร์พร้อมรายละเอียดพอร์ตเพื่อรอการบล็อกจนกว่าจะมีการสิ้นสุดกระบวนการ

6. สร้างไคลเอ็นต์

ในส่วนนี้ เราจะมาดูการสร้างไคลเอ็นต์ Rust สำหรับบริการ RouteGuide ใน src/client/client.rs กัน

เช่นเดียวกับใน src/server/server.rs เราสามารถนำโค้ดที่สร้างขึ้นมาไว้ในขอบเขตผ่านมาโคร include_generated_code! ของ gRPC และนำเข้าประเภท RouteGuideClient

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

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

วิธีการเรียกใช้บริการ

ใน gRPC-Rust, RPC จะทํางานในโหมดบล็อก/ซิงโครนัส ซึ่งหมายความว่าการเรียก RPC จะรอให้เซิร์ฟเวอร์ตอบกลับ และจะส่งคืนการตอบกลับหรือข้อผิดพลาด

หากต้องการเรียกใช้เมธอดบริการ เราต้องสร้างแชแนลเพื่อสื่อสารกับเซิร์ฟเวอร์ก่อน เราสร้างสิ่งนี้โดยการสร้างปลายทางก่อน จากนั้นเชื่อมต่อกับปลายทางนั้น และส่งช่องที่สร้างขึ้นเมื่อเชื่อมต่อกับ RouteGuideClient::new() ดังนี้

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

ในฟังก์ชันนี้ เมื่อสร้างไคลเอ็นต์ เราจะรวมแชแนลทั่วไปที่สร้างไว้ด้านบนเข้ากับโค้ด Stub ที่สร้างขึ้นซึ่งใช้เมธอดที่เฉพาะเจาะจงซึ่งกำหนดไว้ในบริการ .proto

RPC อย่างง่าย

การเรียกใช้ RPC อย่างง่าย GetFeature นั้นแทบจะตรงไปตรงมาเหมือนกับการเรียกใช้เมธอดในเครื่อง เพิ่มข้อความนี้ใน 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(())

ดังที่คุณเห็น เราเรียกใช้เมธอดใน Stub ที่เราได้รับก่อนหน้านี้ ในพารามิเตอร์ของเมธอด เราจะสร้างและป้อนข้อมูลออบเจ็กต์บัฟเฟอร์โปรโตคอลคำขอ (ในกรณีนี้คือ Point) หากการเรียกไม่แสดงข้อผิดพลาด เราจะอ่านข้อมูลการตอบกลับจากเซิร์ฟเวอร์ได้จากค่าที่ส่งคืนแรก

println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
    response.name(),
    response.location().latitude(),
    response.location().longitude());

โดยรวมแล้ว ฟังก์ชัน main() ของไคลเอ็นต์ควรมีลักษณะดังนี้

#[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. ลองเลย

ก่อนอื่น หากต้องการเรียกใช้ไคลเอ็นต์และเซิร์ฟเวอร์ ให้เพิ่มเป็นเป้าหมายไบนารีลงใน Crate เราจึงต้องแก้ไขCargo.tomlตามนั้นและเพิ่มข้อมูลต่อไปนี้

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

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

จากนั้นเรียกใช้คำสั่งต่อไปนี้จากไดเรกทอรีที่ทำงานอยู่

  1. เรียกใช้เซิร์ฟเวอร์ในเทอร์มินัลหนึ่ง
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server 
  1. เรียกใช้ไคลเอ็นต์จากเทอร์มินัลอื่นโดยใช้คำสั่งต่อไปนี้
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client

คุณจะเห็นเอาต์พุตลักษณะนี้ โดยระบบจะละเว้นการประทับเวลาเพื่อความชัดเจน

*** SIMPLE RPC ***

FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906

8. ขั้นตอนถัดไป