简介
本专题将带你使用 axum 和 gRPC 构建一个分布式的博客系统数据结构与Protobuf
本章对我们项目的数据结构和proto进行定义实现分类服务
本章我们实现分类服务,即 `category-srv`实现文章服务
本章将带你实现文章的 gPRC 服务。实现前台web服务
本章将通过使用 axum 调用分类和文章的 gRPC 服务,来实现博客前台Web服务实现管理员服务
本章我们将实现管理员服务实现后台管理web服务
本章将使用 axum 调用 gRPC 服务来实现后台管理的 web 服务安全与鉴权
本章将讨论使用jwt进行鉴权服务扩容、注册、发现和编排
本章将讨论服务管理相关的话题配置中心服务
本章讨论配置中心的实现总结
本专题试图通过一个分布式博客的案例来探讨使用 rust 实现 gRPC 微服务架构的可行性
实现前台web服务
创建项目将加入到 workspace 中:
cargo new blog-frontend
添加依赖
[dependencies]
tokio = {version = "1", features = ["full"]}
prost = "0.11"
prost-types = "0.11"
tonic = "0.8"
axum = "0.5"
tera = "1"
serde = { version="1", features = ["derive"] }
chrono = "0.4"
blog-proto = {path="../blog-proto"}
blog-types = {path="../blog-types"}
由于采用了微服务,所以并没有直接的数据库操作。
blog-types
web 所需的数据结构
由于 web 需要的数据结构和 pb 生成的并不一定相同,比如需要序列化和反序列化、比如可能根据需要增加/删除某些结构体、字段等。
基于此,我们需要定义 web 所需要的数据结构,并提供 From/Into
等方法,方便和 pb 生成的数据结构进行转换。
代码相对简单,请直接在 git 上查看。
博客首页 handler::index
的实现
获取分类列表
let mut cate = state.cate.clone();
let resp = cate
.list_category(tonic::Request::new(ListCategoryRequest {
name: None,
is_del: Some(false),
}))
.await
.map_err(|err| err.to_string())?;
let reply = resp.into_inner();
let mut cate_list: Vec<blog_types::Category> = Vec::with_capacity(reply.categories.len());
for reply_cate in reply.categories {
cate_list.push(reply_cate.into());
}
我们通过调用分类服务的 list_category
方法来获取分类列表。为了共享 gRPC 客户端连接,我们将各种 gRPC 客户端连接通过 AppState 进行共享。
AppState
状态共享
在 model.rs
中,我们定义了 AppState
以实现handler间的状态共享:
pub struct AppState {
pub cate: CategoryServiceClient<tonic::transport::Channel>,
pub topic: TopicServiceClient<tonic::transport::Channel>,
pub tera: Tera,
}
impl AppState {
pub fn new(
cate: CategoryServiceClient<tonic::transport::Channel>,
topic: TopicServiceClient<tonic::transport::Channel>,
tera: Tera,
) -> Self {
Self { cate, topic, tera }
}
}
cate
:连接到分类服务category-srv
的 gRPC 客户端topic
:连接到文章服务topic-srv
的 gRPC 客户端tera
:模板引擎
文章列表需要接收多个可选参数,我们对其进行定义,并通过 axum 的 Query
进行获取:
#[derive(Deserialize, Serialize)]
pub struct QueryParams {
pub page: Option<i32>,
pub category_id: Option<i32>,
pub keyword: Option<String>,
}
请点击查看 handler::index()
的完整代码。
博客文章详情 handler::detail
的实现
相对于 index()
,detail()
的实现就简单多了。
pub async fn detail(
Extension(state): Extension<Arc<AppState>>,
Path(id): Path<i64>,
) -> Result<Html<String>, String> {
let mut ctx = Context::new();
// 获取分类列表
let mut cate = state.cate.clone();
let resp = cate
.list_category(tonic::Request::new(ListCategoryRequest {
name: None,
is_del: Some(false),
}))
.await
.map_err(|err| err.to_string())?;
let reply = resp.into_inner();
let mut cate_list: Vec<blog_types::Category> = Vec::with_capacity(reply.categories.len());
for reply_cate in reply.categories {
cate_list.push(reply_cate.into());
}
ctx.insert("cate_list", &cate_list);
// 获取文章详情
let mut tpc = state.topic.clone();
let resp = tpc
.get_topic(tonic::Request::new(GetTopicRequest {
id,
inc_hit: Some(true),
is_del: Some(false),
}))
.await
.map_err(|err| err.to_string())?;
let reply = resp.into_inner();
let mut t: blog_types::Topic = match reply.topic {
Some(topic) => topic.into(),
None => {
return Err("不存在的文章".to_string());
}
};
// 查找分类
for cate in &cate_list {
if cate.id == t.category_id {
t.category_name = cate.name.clone();
break;
}
}
ctx.insert("topic", &t);
let out = state
.tera
.render("detail.html", &ctx)
.map_err(|err| err.to_string())?;
Ok(Html(out))
}
启动前台WEB服务
#[tokio::main]
async fn main() {
let addr = "0.0.0.0:39527";
let cate = CategoryServiceClient::connect("http://127.0.0.1:19527")
.await
.unwrap();
let topic = TopicServiceClient::connect("http://127.0.0.1:29527")
.await
.unwrap();
let tera = Tera::new("blog-frontend/templates/**/*.html").unwrap();
let app = Router::new()
.route("/", get(handler::index))
.route("/detail/:id", get(handler::detail))
.layer(Extension(Arc::new(model::AppState::new(cate, topic, tera))));
axum::Server::bind(&addr.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
- 首先初始化
AppState
所需要的 gRPC 客户端连接和模板引擎 - 定义路由
本章代码位于04/实现前台服务