برنامه نویسی

شروع کار با gRPC در Rust

معماری میکروسرویس ها یکی از روش های ارجح برای ساخت برنامه های کاربردی مقیاس پذیر و قوی است. این شامل تقسیم برنامه های بزرگ به اجزای کوچکتر است که به خوبی تعریف شده اند، یک کار خاص را انجام می دهند و از مجموعه هایی از رابط برنامه نویسی برنامه (API) برای ارتباط آنها استفاده می کنند.

ارتباطات یک بخش اساسی از میکروسرویس ها است. نقش مهمی در اجازه دادن به سرویس‌ها با یکدیگر در زمینه بزرگتر برنامه کاربردی دارد. چند نمونه از پروتکل های میکروسرویس برای برقراری ارتباط با یکدیگر شامل HTTP، gRPC، کارگزاران پیام و غیره

در این مقاله، بررسی خواهیم کرد که gRPC چیست و چگونه می توان با ساخت یک سرویس مدیریت کاربر با استفاده از gRPC، MongoDB و Rust شروع به کار کرد.

gRPC چیست؟

gRPC یک چارچوب ارتباطی مدرن است که می تواند در هر محیطی اجرا شود و به اتصال موثر خدمات کمک می کند. در سال 2015 معرفی شد و توسط پلتفرم محاسبات بومی ابری (CNCF) اداره می شود. فراتر از اتصال موثر خدمات در یک سیستم توزیع شده، برنامه های کاربردی تلفن همراه، جلو به باطن و غیره، از بررسی سلامت، تعادل بار، ردیابی و احراز هویت پشتیبانی می کند.

gRPC دیدگاه جدیدی را به توسعه دهندگانی که برنامه های کاربردی متوسط ​​تا پیچیده می سازند، ارائه می دهد، زیرا می تواند پیوندهای مشتری و سرور را برای چندین زبان ایجاد کند. در زیر به برخی از مزایای آن اشاره می شود:

تعریف خدمات

gRPC از Protocol Buffers به ​​عنوان زبان توصیف رابط خود، مشابه JSON استفاده می کند و ویژگی هایی مانند احراز هویت، لغو، مهلت زمانی و غیره را ارائه می دهد.

سبک و کارآمد

تعاریف gRPC 30 درصد کوچکتر از تعاریف JSON هستند و 5 تا 7 برابر سریعتر از REST API سنتی هستند.

پشتیبانی از پلتفرم های متعدد

gRPC زبان آگنوستیک است و تولید کد خودکار برای زبان‌های پشتیبانی شده توسط کلاینت و سرور دارد.

مقیاس پذیر

از محیط توسعه‌دهنده تا تولید، gRPC برای مقیاس درخواست میلیون‌ها ثانیه طراحی شده است.

اکنون که اهمیت gRPC در ساخت برنامه های مقیاس پذیر را درک کردیم، بیایید یک سرویس مدیریت کاربر با gRPC، MongoDB و Rust بسازیم. کد منبع پروژه را می توانید در اینجا بیابید.

پیش نیازها

برای درک کامل مفاهیم ارائه شده در این آموزش، موارد زیر مورد نیاز است:

  • درک اولیه از Rust
  • درک اولیه پروتکل بافر
  • کامپایلر Protocol Buffer نصب شده است
  • یک حساب MongoDB برای میزبانی پایگاه داده. ثبت نام کاملا رایگان است.
  • پستچی یا هر برنامه آزمایشی gRPC

راه اندازی پروژه و وابستگی ها

برای شروع، باید به دایرکتوری مورد نظر رفته و دستور زیر را در ترمینال خود اجرا کنیم:

cargo new grpc_rust && cd grpc_rust
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این دستور یک پروژه Rust به نام ایجاد می کند grpc_rust و وارد فهرست پروژه می شود.

در مرحله بعد، وابستگی‌های مورد نیاز را با اصلاح کردن نصب می‌کنیم [dependencies] بخش از Cargo.toml فایل مطابق شکل زیر:

//other code section goes here

[dependencies]
tokio = {version = "1", features = ["macros", "rt-multi-thread"]}
serde = {versiom = "1", features = ["derive"]}
dotenv = "0.15.0"
tonic = "0.9.2"
prost = "0.11.9"
futures = "0.3"

[dependencies.mongodb]
version = "2.2.0"

[build-dependencies]
tonic-build = "0.9.2"
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

tokio = {version = "1", features = ["macros", "rt-multi-thread"]} یک زمان اجرا است که برنامه نویسی ناهمزمان را در Rust فعال می کند.

serde = {versiom = "1", features = ["derive"]} چارچوبی برای سریال سازی و سریال زدایی از ساختارهای داده Rust است.

dotenv = "0.15.0" یک کتابخانه برای مدیریت متغیرهای محیطی است.

tonic = "0.9.2" اجرای Rust از gRPC است.

prost = "0.11.9" یک پیاده سازی Protocol Buffers در Rust است و کد Rust ساده و اصطلاحی را از آن تولید می کند proto2 و proto3 فایل ها.

futures = "0.3" یک کتابخانه برای انجام برنامه نویسی ناهمزمان با درایور MongoDB است

[dependencies.mongodb] یک درایور برای اتصال به MongoDB است. همچنین نسخه مورد نیاز و نوع ویژگی (Asynchronous API) را مشخص می کند.

[build-dependencies] مشخص می کند tonic-build = "0.9.2" به عنوان یک وابستگی کامپایل می کند .proto فایل ها به کد Rust.

تعریف بافر و کامپایل پروتکل مدیریت کاربر

برای شروع، باید یک بافر پروتکل تعریف کنیم تا تمام عملیات و پاسخ‌های مربوط به سرویس مدیریت کاربر را نشان دهد. برای انجام این کار، ابتدا باید a را ایجاد کنیم proto پوشه در پوشه ریشه، و در این پوشه، a ایجاد کنید user.proto فایل و قطعه زیر را اضافه کنید:

syntax = "proto3";
package user;

service UserService {
    rpc GetUser (UserRequest) returns (UserResponse);
    rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
    rpc UpdateUser (UpdateUserRequest) returns (UpdateUserResponse);
    rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse);
    rpc GetAllUsers (Empty) returns (GetAllUsersResponse);
}

message UserRequest {
    string id = 1;
}

message UserResponse {
    string id = 1;
    string name = 2;
    string location = 3;
    string title = 4;
}

message CreateUserRequest {
    string name = 2;
    string location = 3;
    string title = 4;
}

message CreateUserResponse {
    string data = 1;
}

message UpdateUserRequest {
    string _id = 1;
    string name = 2;
    string location = 3;
    string title = 4;
}

message UpdateUserResponse {
    string data = 1;
}

message DeleteUserRequest {
    string id = 1;
}

message DeleteUserResponse {
    string data = 1;
}

message Empty {}

message GetAllUsersResponse {
    repeated UserResponse users = 1;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قطعه بالا کارهای زیر را انجام می دهد:

  • کاربرد را مشخص می کند proto3 نحو
  • اعلام می کند user به عنوان نام بسته
  • a را ایجاد می کند service برای ایجاد، خواندن، ویرایش و حذف (CRUD) یک کاربر و پاسخ های مربوط به آنها به عنوان messageس

در مرحله دوم، ما باید یک فایل ساخت که دستور می دهد ایجاد کنیم tonic-build = "0.9.2" وابستگی به کامپایل ما user.proto در یک کد Rust فایل کنید. برای این کار باید a ایجاد کنیم build.rs فایل را در دایرکتوری ریشه قرار دهید و قطعه زیر را اضافه کنید:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/user.proto")?;
    Ok(())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در نهایت، ما باید آن را کامپایل کنیم user.proto فایل با استفاده از build.rs دستورالعملی که قبلاً با اجرای دستور زیر در ترمینال خود مشخص کردیم:

cargo build
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با استفاده از کد تولید شده از gRPC در برنامه ما

با انجام فرآیند ساخت، می توانیم از کد تولید شده در برنامه خود استفاده کنیم.

راه اندازی و یکپارچه سازی پایگاه داده

ابتدا باید یک پایگاه داده و مجموعه ای را در MongoDB راه اندازی کنیم که در زیر نشان داده شده است:

همچنین باید رشته اتصال پایگاه داده خود را با کلیک کردن بر روی آن دریافت کنیم اتصال دکمه و تغییر درایور به Rust.

1684856551 60 شروع کار با gRPC در Rust

در مرحله دوم، ما باید رشته اتصال کپی شده را با رمز عبور کاربری که قبلا ایجاد کرده بودیم تغییر دهیم و نام پایگاه داده را تغییر دهیم. برای این کار باید a ایجاد کنیم .env فایل را در دایرکتوری ریشه قرار دهید و قطعه کپی شده را اضافه کنید:

MONGOURI=mongodb+srv://<YOUR USERNAME HERE>:<YOUR PASSWORD HERE>@cluster0.e5akf.mongodb.net/<DATABASE NAME>?retryWrites=true&w=majority
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نمونه ای از یک رشته اتصال به درستی پر شده در زیر:

MONGOURI=mongodb+srv://malomz:malomzPassword@cluster0.e5akf.mongodb.net/rustDB?retryWrites=true&w=majority
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در نهایت، ما باید به مسیر بروید src پوشه، ایجاد یک mongo_connection.rs فایل را برای پیاده سازی منطق پایگاه داده ما و اضافه کردن قطعه زیر:

use std::{env, io::Error};

use dotenv::dotenv;
use futures::TryStreamExt;
use mongodb::bson::doc;
use mongodb::bson::oid::ObjectId;
use mongodb::results::{DeleteResult, InsertOneResult, UpdateResult};
use mongodb::{Client, Collection};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct User {
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    pub id: Option<ObjectId>,
    pub name: String,
    pub location: String,
    pub title: String,
}

pub struct DBMongo {
    col: Collection<User>,
}

impl DBMongo {
    pub async fn init() -> Self {
        dotenv().ok();
        let uri = match env::var("MONGOURI") {
            Ok(v) => v.to_string(),
            Err(_) => format!("Error loading env variable"),
        };
        let client = Client::with_uri_str(uri)
            .await
            .expect("error connecting to database");
        let col = client.database("rustDB").collection("User");
        DBMongo { col }
    }

    pub async fn create_user(new_user: User) -> Result<InsertOneResult, Error> {
        let db = DBMongo::init().await;
        let new_doc = User {
            id: None,
            name: new_user.name,
            location: new_user.location,
            title: new_user.title,
        };
        let user = db
            .col
            .insert_one(new_doc, None)
            .await
            .ok()
            .expect("Error creating user");
        Ok(user)
    }

    pub async fn get_user(id: String) -> Result<User, Error> {
        let db = DBMongo::init().await;
        let obj_id = ObjectId::parse_str(id).unwrap();
        let filter = doc! {"_id": obj_id};
        let user_detail = db
            .col
            .find_one(filter, None)
            .await
            .ok()
            .expect("Error getting user's detail");
        Ok(user_detail.unwrap())
    }

    pub async fn update_user(id: String, new_user: User) -> Result<UpdateResult, Error> {
        let db = DBMongo::init().await;
        let obj_id = ObjectId::parse_str(id).unwrap();
        let filter = doc! {"_id": obj_id};
        let new_doc = doc! {
            "$set":
                {
                    "id": new_user.id,
                    "name": new_user.name,
                    "location": new_user.location,
                    "title": new_user.title
                },
        };
        let updated_doc = db
            .col
            .update_one(filter, new_doc, None)
            .await
            .ok()
            .expect("Error updating user");
        Ok(updated_doc)
    }

    pub async fn delete_user(id: String) -> Result<DeleteResult, Error> {
        let db = DBMongo::init().await;
        let obj_id = ObjectId::parse_str(id).unwrap();
        let filter = doc! {"_id": obj_id};
        let user_detail = db
            .col
            .delete_one(filter, None)
            .await
            .ok()
            .expect("Error deleting user");
        Ok(user_detail)
    }

    pub async fn get_all_users() -> Result<Vec<User>, Error> {
        let db = DBMongo::init().await;
        let mut cursors = db
            .col
            .find(None, None)
            .await
            .ok()
            .expect("Error getting list of users");
        let mut users: Vec<User> = Vec::new();
        while let Some(user) = cursors
            .try_next()
            .await
            .ok()
            .expect("Error mapping through cursor")
        {
            users.push(user)
        }
        Ok(users)
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قطعه بالا کارهای زیر را انجام می دهد:

  • خط: 1 – 9: وابستگی های مورد نیاز را وارد می کند
  • خط: 11 – 18: ایجاد یک User ساختار با خواص مورد نیاز ما همچنین ویژگی های فیلد را به آن اضافه کردیم id ویژگی را تغییر نام دهید و در صورت خالی بودن فیلد را نادیده بگیرید.
  • خط: 20 – 22: ایجاد یک DBMongo ساخت با a col زمینه دسترسی به مجموعه MongoDB
  • خط: 24 – 122: یک بلوک پیاده سازی ایجاد می کند که متدهایی را به آن اضافه می کند MongoRepo struct برای مقداردهی اولیه پایگاه داده با عملیات CRUD مربوطه آن.

ادغام منطق پایگاه داده با کد تولید شده توسط gRPC

با راه‌اندازی منطق پایگاه داده‌مان، می‌توانیم از روش‌هایی برای ایجاد کنترل‌کننده‌های برنامه خود استفاده کنیم. برای این کار باید a ایجاد کنیم service.rs فایل داخل همان src پوشه را وارد کنید و قطعه زیر را اضافه کنید:

use mongodb::bson::oid::ObjectId;
use tonic::{Request, Response, Status};
use user::{
    user_service_server::UserService, CreateUserRequest, CreateUserResponse, DeleteUserRequest,
    DeleteUserResponse, Empty, GetAllUsersResponse, UpdateUserRequest, UpdateUserResponse,
};
use crate::mongo_connection::{self, DBMongo};
use self::user::{UserRequest, UserResponse};

pub mod user {
    tonic::include_proto!("user");
}

#[derive(Debug, Default)]
pub struct User {}

#[tonic::async_trait]
impl UserService for User {
    async fn create_user(
        &self,
        request: Request<CreateUserRequest>,
    ) -> Result<Response<CreateUserResponse>, Status> {
        let req = request.into_inner();
        let new_user = mongo_connection::User {
            id: None,
            name: req.name,
            location: req.location,
            title: req.title,
        };
        let db = DBMongo::create_user(new_user).await;
        match db {
            Ok(resp) => {
                let user = CreateUserResponse {
                    data: resp.inserted_id.to_string(),
                };
                Ok(Response::new(user))
            }
            Err(error) => Err(Status::aborted(format!("{}", error))),
        }
    }

    async fn get_user(
        &self,
        request: Request<UserRequest>,
    ) -> Result<Response<UserResponse>, Status> {
        let req = request.into_inner();
        let db = DBMongo::get_user(req.id).await;
        match db {
            Ok(resp) => {
                let user = UserResponse {
                    id: resp.id.unwrap().to_string(),
                    name: resp.name,
                    location: resp.location,
                    title: resp.title,
                };
                Ok(Response::new(user))
            }
            Err(error) => Err(Status::aborted(format!("{}", error))),
        }
    }

    async fn update_user(
        &self,
        request: Request<UpdateUserRequest>,
    ) -> Result<Response<UpdateUserResponse>, Status> {
        let req = request.into_inner();
        let new_user = mongo_connection::User {
            id: Some(ObjectId::parse_str(req.id.clone()).unwrap()),
            name: req.name,
            location: req.location,
            title: req.title,
        };
        let db = DBMongo::update_user(req.id.clone(), new_user).await;
        match db {
            Ok(_) => {
                let user = UpdateUserResponse {
                    data: String::from("User details updated successfully!"),
                };
                Ok(Response::new(user))
            }
            Err(error) => Err(Status::aborted(format!("{}", error))),
        }
    }

    async fn delete_user(
        &self,
        request: Request<DeleteUserRequest>,
    ) -> Result<Response<DeleteUserResponse>, Status> {
        let req = request.into_inner();
        let db = DBMongo::delete_user(req.id).await;
        match db {
            Ok(_) => {
                let user = DeleteUserResponse {
                    data: String::from("User details deleted successfully!"),
                };
                Ok(Response::new(user))
            }
            Err(error) => Err(Status::aborted(format!("{}", error))),
        }
    }

    async fn get_all_users(
        &self,
        _: Request<Empty>,
    ) -> Result<Response<GetAllUsersResponse>, Status> {
        let db = DBMongo::get_all_users().await;
        match db {
            Ok(resp) => {
                let mut user_list: Vec<UserResponse> = Vec::new();
                for data in resp {
                    let mapped_user = UserResponse {
                        id: data.id.unwrap().to_string(),
                        name: data.name,
                        location: data.location,
                        title: data.title,
                    };
                    user_list.push(mapped_user);
                }
                let user = GetAllUsersResponse { users: user_list };
                Ok(Response::new(user))
            }
            Err(error) => Err(Status::aborted(format!("{}", error))),
        }
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قطعه بالا کارهای زیر را انجام می دهد:

  • خط: 1 – 8: وابستگی های مورد نیاز را وارد می کند (از جمله gRPC تولید شده)
  • خط: 10 – 12: اعلام می کند user struct برای وارد کردن کدهای تولید شده توسط gRPC ما با استفاده از tonic::include_proto!("user")
  • خط: 14 – 15: ایجاد یک User struct برای نشان دادن مدل برنامه ما
  • خط: 17 – 125: اجرا می کند UserService صفات از کد تولید شده توسط gRPC برای User با ایجاد روش‌های مورد نیاز و برگرداندن پاسخ‌های مناسب همانطور که توسط gRPC ایجاد می‌شود

ایجاد سرور

با انجام این کار، می توانیم سرور gRPC برنامه را با تغییر دادن ایجاد کنیم main.rs فایل مطابق شکل زیر:

use std::net::SocketAddr;

use service::{user::user_service_server::UserServiceServer, User};
use tonic::transport::Server;

mod mongo_connection;
mod service;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let address: SocketAddr = "[::1]:8080".parse().unwrap();
    let user = User::default();

    Server::builder()
        .add_service(UserServiceServer::new(user))
        .serve(address)
        .await?;
    Ok(())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قطعه بالا کارهای زیر را انجام می دهد:

  • وابستگی های مورد نیاز را وارد کرده و اضافه می کند mongo_connection و service به عنوان یک ماژول
  • با استفاده از یک سرور ایجاد می کند Server::builder() روش و اضافه می کند UserServiceServer به عنوان یک خدمت

با انجام این کار، می توانیم برنامه خود را با اجرای دستور زیر در ترمینال خود آزمایش کنیم.

cargo run
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تست با پستچی

با راه‌اندازی سرور ما، می‌توانیم برنامه خود را با ایجاد یک برنامه جدید آزمایش کنیم درخواست gRPC.

ایجاد جدید

درخواست gRPC را انتخاب کنید

ورودی grpc://[::1]:8080 به عنوان URL، را انتخاب کنید یک فایل .proto را وارد کنید گزینه و آپلود user.proto فایلی که قبلا ایجاد کردیم

آدرس و فایل پروتو آپلود کنید

با انجام این کار، روش مربوطه پر می شود و می توانیم آنها را مطابق با آن آزمایش کنیم.

getAllUsers
getAUser

همچنین می‌توانیم با بررسی مجموعه MongoDB خود تأیید کنیم که سرور gRPC ما کار می‌کند

مجموعه

نتیجه

این پست درباره چیستی gRPC، نقش آن در ساخت برنامه‌های مقیاس‌پذیر و نحوه شروع با ساخت یک سرویس مدیریت کاربر با Rust و MongoDB بحث می‌کند. فراتر از آنچه در بالا مورد بحث قرار گرفت، gRPC تکنیک های قوی در مورد احراز هویت، مدیریت خطا، عملکرد و غیره ارائه می دهد.

این منابع ممکن است مفید باشند:

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

همچنین ببینید
بستن
دکمه بازگشت به بالا