شروع کار با 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
.
در مرحله دوم، ما باید رشته اتصال کپی شده را با رمز عبور کاربری که قبلا ایجاد کرده بودیم تغییر دهیم و نام پایگاه داده را تغییر دهیم. برای این کار باید 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
ساخت با acol
زمینه دسترسی به مجموعه 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://[::1]:8080
به عنوان URL، را انتخاب کنید یک فایل .proto را وارد کنید گزینه و آپلود user.proto
فایلی که قبلا ایجاد کردیم
با انجام این کار، روش مربوطه پر می شود و می توانیم آنها را مطابق با آن آزمایش کنیم.
همچنین میتوانیم با بررسی مجموعه MongoDB خود تأیید کنیم که سرور gRPC ما کار میکند
نتیجه
این پست درباره چیستی gRPC، نقش آن در ساخت برنامههای مقیاسپذیر و نحوه شروع با ساخت یک سرویس مدیریت کاربر با Rust و MongoDB بحث میکند. فراتر از آنچه در بالا مورد بحث قرار گرفت، gRPC تکنیک های قوی در مورد احراز هویت، مدیریت خطا، عملکرد و غیره ارائه می دهد.
این منابع ممکن است مفید باشند: