前言
本后端项目用到的技术栈主要包括:
- Actix Web框架;
- Log 日志库;
- Serde 序列化;
- SnowFlake Id生成;
- dotenv 获取环境配置;
- MongoDB 存取;
- lazy_static 全局静态初始化;
- ELO 算法;
- 使用 Pre-Commit 在 Git Commit 前进行校验;
- 使用 Github Action 进行 CI;
- 使用中间镜像对代码进行编译并创建部署镜像;
- ……
阅读了本文,你应该也能够学会上面这些库的用法;
那么废话不多说,直接开始!
代码实现
代码目录结构
整个项目的目录结构如下(已去掉无关文件):
代码语言:javascript复制$ tree
.
├── .env
├── .github
│ └── workflows
│ └── ci.yaml
├── .pre-commit-config.yaml
├── Cargo.toml
├── Dockerfile
├── Makefile
├── build-image.sh
└── src
├── algorithm
│ ├── elo_rating.rs
│ ├── k_factor.rs
│ └── mod.rs
├── config
│ └── mod.rs
├── controller
│ ├── face_info_controller.rs
│ ├── file_controller.rs
│ └── mod.rs
├── dao
│ ├── face_info_dao.rs
│ ├── file_resource_dao.rs
│ ├── mod.rs
│ └── rating_log_dao.rs
├── entity
│ ├── face_info.rs
│ ├── file_resource.rs
│ ├── mod.rs
│ └── rating_log.rs
├── logger
│ └── mod.rs
├── main.rs
├── resource
│ ├── id_generator.rs
│ ├── mod.rs
│ └── mongo.rs
├── service
│ ├── face_info_service.rs
│ ├── file_resource_service.rs
│ └── mod.rs
└── utils
├── md5.rs
└── mod.rs
下面来说明:
.github
目录:Github Actions 相关配置;src
目录:项目源代码目录;.pre-commit-config.yaml
:Pre-Commit 配置;.env
:项目环境变量配置;Cargo.toml
:Cargo 项目配置;Makefile
:项目编译脚本;Dockerfile
:项目Docker镜像配置;build-image.sh
:打包镜像脚本;
对于 src 目录下的各个子目录,见名知意,基本上很好理解了!
服务入口
Cargo 项目约定程序的入口都是:src/main.rs
下;
我们从 main 函数来看做了些什么:
src/main.rs
代码语言:javascript复制#[macro_use]
extern crate log;
use actix_web::{middleware, App, HttpServer};
use dotenv::dotenv;
use mongodb::bson::doc;
use crate::controller::{face_info_controller, file_controller};
use crate::resource::mongo;
mod algorithm;
mod config;
mod controller;
mod dao;
mod entity;
mod logger;
mod resource;
mod service;
mod utils;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv().ok();
logger::init();
resource::check_resources().await;
service::init_file_service().await;
HttpServer::new(|| {
App::new()
.wrap(middleware::Logger::default())
.service(face_info_controller::get_face_info_randomly)
.service(face_info_controller::get_face_info_by_id)
.service(face_info_controller::add_face_info)
.service(face_info_controller::vote_face_info)
.service(file_controller::create_file_resource_by_stream)
.service(file_controller::create_file_resource)
.service(file_controller::download_local_file)
})
.bind(("0.0.0.0", 8080))?
.run()
.await
}
在入口文件中,首先启用了一些库的宏(Macro),并声明了 Actix-Web 框架的 main 函数;
在 main 函数中,做了一般后端服务都会做的事情:
- 获取环境配置;
- 初始化项目日志;
- 初始化资源:数据库、Id生成器等;
- 注册并启动服务;
下面我们分别来看
配置与日志
获取环境配置
我们可以通过 dotenv
库解析位于项目下、以及系统环境变量中的配置;
只需要下面一句话即可:
代码语言:javascript复制dotenv().ok();
配置文件如下:
.env
代码语言:javascript复制MONGODB_URI=mongodb://admin:123456@localhost:27017/?retryWrites=true&w=majority
LOG_LEVEL=INFO
SNOWFLAKE_MACHINE_ID=1
SNOWFLAKE_NODE_ID=1
主要是配置了 MongoDB 的连接地址、日志级别、SnowFlake 的配置;
上面的语句会将这些配置解析;
初始化Logger
main 函数中的这条语句初始化了 Logger:
代码语言:javascript复制logger::init();
这个是 logger 模块封装的一个函数:
logger/mod.rs
代码语言:javascript复制use std::env;
use crate::config::LOG_LEVEL;
use log::{Level, LevelFilter, Metadata, Record};
struct Logger;
pub fn init() {
static LOGGER: Logger = Logger;
log::set_logger(&LOGGER).unwrap();
let log_level: String = env::var(LOG_LEVEL).unwrap_or_else(|_| String::from("INFO"));
log::set_max_level(match log_level.as_str() {
"ERROR" => LevelFilter::Error,
"WARN" => LevelFilter::Warn,
"INFO" => LevelFilter::Info,
"DEBUG" => LevelFilter::Debug,
"TRACE" => LevelFilter::Trace,
_ => LevelFilter::Info,
});
}
impl log::Log for Logger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let color = match record.level() {
Level::Error => 31, // Red
Level::Warn => 93, // BrightYellow
Level::Info => 34, // Blue
Level::Debug => 32, // Green
Level::Trace => 90, // BrightBlack
};
println!(
"u{1B}[{}m[{:>5}]:{} - {}u{1B}[0m",
color,
record.level(),
record.target(),
record.args(),
);
}
fn flush(&self) {}
}
上面的代码首先定义了一个全局日志类型 Logger;
并在 init 函数中初始化了全局静态变量:LOGGER,并使用 log::set_logger
进行了设置;
同时,我们我们从环境变量中获取 LOG_LEVEL
日志级别配置(如果未设置,则默认为 INFO
级别),随后进行了设置;
我们为我们的 Logger 实现了log::Log
Trait,这也是为什么我们能将该类型的变量设置为Logger的原因!
在 log::Log
Trait 的实现中,我们简单定义了日志的输出格式以及输出颜色;
可以看到有了很多第三方库的支持,rust 还是非常好用的!
初始化资源
接下来我们调用:
代码语言:javascript复制resource::check_resources().await;
service::init_file_service().await;
来等待资源初始化完成;
下面初始化文件服务的逻辑非常简单,只是创建了一个临时文件:
代码语言:javascript复制pub async fn init_file_service() {
init_local_directory().await;
}
pub async fn init_local_directory() {
fs::create_dir_all(SAVE_DIR).unwrap()
}
我们重点来看 check_resources()
函数,在其中初始化并校验了 MongoDB 连接以及 SnowFlake Id生成器;
资源相关的初始化都是在 resource 模块中完成的;
resource 模块的入口 mod.rs 中定义了资源的校验函数:
resource/mod.rs
代码语言:javascript复制use crate::doc;
pub mod id_generator;
pub mod mongo;
pub async fn check_resources() {
check_mongo().await;
check_id_generator().await;
}
async fn check_mongo() {
mongo::MONGO_CLIENT
.get()
.await
.database("admin")
.run_command(doc! {"ping": 1}, None)
.await
.unwrap();
info!("Mongo connected successfully.");
}
async fn check_id_generator() {
info!("Id generate success: {}.", id_generator::get_id().await)
}
MongoDB 通过 Ping 校验了数据库连接,而 SnowFlake 通过创建了一个 Id 校验了正确性;
那么这些资源是在哪里初始化的呢?
主要是通过 lazy_static
在首次使用的时候初始化的!
lazy_static
的一个特性是:在首次使用这个变量的时候,才会进行静态初始化;
下面分别来看:
src/resource/mongo.rs
代码语言:javascript复制use std::env;
use async_once::AsyncOnce;
use lazy_static::lazy_static;
use mongodb::Client;
use crate::config::MONGODB_URI;
lazy_static! {
pub static ref MONGO_CLIENT: AsyncOnce<Client> = AsyncOnce::new(async {
let uri = env::var(MONGODB_URI).expect("You must set the MONGODB_URI environment var!");
Client::with_uri_str(&uri).await.unwrap()
});
}
上面的代码在 lazy_static!
宏中,异步初始化了 MongoDB 的连接:
首先,从环境变量中获取配置 MONGODB_URI
,随后进行了初始化,并保存至变量:MONGO_CLIENT
中;
src/resource/id_generator.rs
代码语言:javascript复制use std::env;
use std::sync::Mutex;
use lazy_static::lazy_static;
use snowflake::SnowflakeIdBucket;
use crate::config;
lazy_static! {
static ref ID_GENERATOR_BUCKET: Mutex<SnowflakeIdBucket> = Mutex::new({
let machine_id: i32 = env::var(config::SNOWFLAKE_MACHINE_ID)
.expect("You must set the SNOWFLAKE_MACHINE_ID environment var!")
.parse::<i32>()
.unwrap();
let node_id: i32 = env::var(config::SNOWFLAKE_NODE_ID)
.expect("You must set the SNOWFLAKE_NODE_ID environment var!")
.parse::<i32>()
.unwrap();
SnowflakeIdBucket::new(machine_id, node_id)
});
}
pub async fn get_id() -> String {
ID_GENERATOR_BUCKET.lock().unwrap().get_id().to_string()
}
#[actix_rt::test]
async fn generate_id_test() {
use dotenv::dotenv;
dotenv().ok();
println!("{}", get_id().await)
}
与上面的初始化类似,这里从环境变量中获取:SNOWFLAKE_MACHINE_ID
和 SNOWFLAKE_NODE_ID
,随后使用 SnowflakeIdBucket::new
进行了初始化;
同时,和 MongoDB 不同的是,这里需要使用 Mutex
进行封装,因为极有可能多个出现多个线程并发获取Id;
而 MongoDB 的 Client 已经是:Arc<ClientInner>
类型了!
我们也封装了 get_id 函数,直接供外部调用,而无需暴露 ID_GENERATOR_BUCKET
变量!
最下面是一个单测,用于测试我们的 Id 生成器;
至此,我们的资源初始化完成;
完整文章,请传送至:https://rustcc.cn/article?id=bafc76f0-1d85-40b1-a551-bb635e3fd37d