Rust中的GRPC入门
#rust #mongodb #grpc #backend

微服务体系结构是构建可扩展和健壮应用程序的首选方法之一。它涉及将大型应用程序分解为明确定义的较小组件,执行特定的任务并使用应用程序编程接口(API)集进行通信。

通信是微服务的重要组成部分;它在让服务在较大的应用程序上下文中相互交流时起着重要的作用。协议微服务用于彼此通信的一些示例包括HTTP, grpc ,消息经纪人等。

在本文中,我们将通过使用GRPC,MongoDB和Rust构建用户管理服务来探索什么是GRPC以及如何开始。

什么是GRPC?

GRPC是一个现代的通信框架,可以在任何环境中运行并有助于有效连接服务。它于2015年推出,并由云本机计算平台(CNCF)统治。除了有效地连接分布式系统,移动应用程序,后端的前端等方面的服务外,它还支持健康检查,负载平衡,跟踪和身份验证。

GRPC为开发人员提供了一个新的视角,可以为复杂的应用程序构建媒介,因为它可以为多种语言生成客户端和服务器绑定。以下是其一些好处:

服务定义

GRPC使用协议缓冲区作为其接口描述语言,类似于JSON,并提供了身份验证,取消,超时等功能。

轻巧和表现

GRPC定义比JSON定义小30%,比传统的REST API快5到7倍。

多个平台支持

GRPC是语言不可知论,并且具有为客户端和服务器支持的语言的自动代码生成。

可伸缩

从开发人员的环境到生产,GRPC旨在扩展数百万秒的秒请求。

入门

现在,我们了解GRPC在构建可扩展应用程序中的重要性,让我们使用GRPC,MongoDB和Rust构建用户管理服务。可以找到项目源代码here

先决条件

要完全掌握本教程中提出的概念,需要以下内容:

  • 对锈的基本理解
  • 对协议缓冲区的基本理解
  • 协议缓冲区compiler已安装
  • MongoDB account托管数据库。 Signup 是完全免费的
  • Postman或任何GRPC测试应用程序

项目和依赖关系设置

要开始,我们需要导航到所需的目录并在我们的终端中运行下面的命令:

cargo new grpc_rust && cd grpc_rust

此命令创建一个名为grpc_rust的Rust Project并导航到项目目录。

接下来,我们通过修改Cargo.toml文件的[dependencies]部分来安装所需的依赖项,如下所示:

//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"]}是用于序列化和应对生锈数据结构的框架。

dotenv = "0.15.0"是用于管理环境变量的库。

tonic = "0.9.2"是grpc的生锈。

prost = "0.11.9"是Rust中的协议缓冲区实现,并从proto2proto3文件中生成简单的,惯用的生锈代码。

futures = "0.3"是一个与mongodb驱动程序进行异步编程的库

[dependencies.mongodb]是连接到MongoDB的驱动程序。它还指定了必需的版本和功能类型(异步API)。

[build-dependencies]tonic-build = "0.9.2"指定为依赖关系。它将.proto文件编译到锈谱中。

定义用户管理协议缓冲区和编译

要开始,我们需要定义一个协议缓冲区,以表示用户管理服务中涉及的所有操作和响应。为此,首先,我们需要在根目录中创建一个proto文件夹,在此文件夹中,创建一个user.proto文件,然后添加下面的smippet:

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声明为包装名称
  • 创建一个service来创建,读取,编辑和删除(crud)用户及其相应的响应作为message

其次,我们需要创建一个构建文件,该文件指示tonic-build = "0.9.2"依赖项将我们的user.proto文件编译为锈代码。为此,我们需要在根目录中创建一个build.rs文件,然后添加下面的摘要:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/user.proto")?;
    Ok(())
}

最后,我们需要使用我们前面在终端中运行以下命令来编译user.proto文件:

cargo build

在我们的应用程序中使用GRPC生成的代码

完成构建过程,我们可以在应用程序中使用生成的代码。

数据库设置和集成

首先,我们需要在MongoDB上设置一个数据库和一个集合,如下所示:

database setup

我们还需要单击连接按钮并将驱动程序更改为Rust

其次,我们必须使用我们之前创建的用户密码修改复制的连接字符串并更改数据库名称。为此,我们需要在根目录中创建一个.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 :使用col字段创建一个DBMongo结构,以访问mongodb收藏
  • 行:24-122 :创建一个实现块,该块将方法添加到MongoRepo struct中,以使用其相应的CRUD操作初始化数据库。

将数据库逻辑与GRPC生成的代码

集成

使用我们的数据库逻辑设置,我们可以使用这些方法来创建应用程序处理程序。为此,我们需要在同一src文件夹中创建一个service.rs文件,然后添加下面的摘要:

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使用tonic::include_proto!("user")将GRPC生成的代码范围范围
  • 线:14-15 :创建一个User结构来表示我们的应用程序模型
  • 行:17-125 :通过创建必需的方法并返回GRPC生成的适当响应,从GRPC生成的代码中实现UserService特征。

创建服务器

完成此操作,我们可以通过修改main.rs文件来创建应用程序GRPC服务器,如下所示:

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_connectionservice添加为模块
  • 使用Server::builder()方法创建服务器,并将UserServiceServer添加为服务。

完成此操作,我们可以通过在终端中运行以下命令来测试我们的应用程序。

cargo run

与Postman进行测试

在我们的服务器启动和运行时,我们可以通过创建新的 grpc请求

来测试我们的应用程序。

Create new

Select gRPC Request

输入grpc://[::1]:8080作为URL,选择导入一个.proto文件选项并上传我们之前创建的user.proto文件。

URL and upload proto file

完成此操作,相应的方法将被填充,我们可以相应地测试它们。

getAllUsers
getAUser

我们还可以通过检查MongoDB Collection

来验证我们的GRPC服务器的工作原理

Collection

结论

这篇文章讨论了GRPC是什么,其在构建可扩展应用程序中的作用以及如何通过使用Rust和MongoDB构建用户管理服务开始。除了上面讨论的内容之外,GRPC还提供了围绕身份验证,错误处理,性能等的强大技术

这些资源可能会有所帮助: