简介
本专题将带你使用 axum 和 gRPC 构建一个分布式的博客系统数据结构与Protobuf
本章对我们项目的数据结构和proto进行定义实现分类服务
本章我们实现分类服务,即 `category-srv`实现文章服务
本章将带你实现文章的 gPRC 服务。实现前台web服务
本章将通过使用 axum 调用分类和文章的 gRPC 服务,来实现博客前台Web服务实现管理员服务
本章我们将实现管理员服务实现后台管理web服务
本章将使用 axum 调用 gRPC 服务来实现后台管理的 web 服务安全与鉴权
本章将讨论使用jwt进行鉴权服务扩容、注册、发现和编排
本章将讨论服务管理相关的话题配置中心服务
本章讨论配置中心的实现总结
本专题试图通过一个分布式博客的案例来探讨使用 rust 实现 gRPC 微服务架构的可行性
实现前台web服务
本章将通过使用 axum 调用分类和文章的 gRPC 服务,来实现博客前台Web服务。
创建项目将加入到 workspace 中:
cargo new blog-frontend
添加依赖
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
状态共享
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
:模板引擎
获取文章列表
let query_category_id = match params.category_id.clone() {
Some(cid) => {
if cid > 0 {
Some(cid)
} else {
None
}
}
None => None,
};
let mut tpc = state.topic.clone();
let resp = tpc
.list_topic(tonic::Request::new(ListTopicRequest {
page: params.page.clone(),
category_id: query_category_id,
keyword: params.keyword.clone(),
is_del: Some(false),
dateline_range: None,
}))
.await
.map_err(|err| err.to_string())?;
let reply = resp.into_inner();
let mut topic_list: Vec<blog_types::Topic> = Vec::with_capacity(reply.topics.capacity());
for reply_topic in reply.topics {
let mut t: blog_types::Topic = reply_topic.into();
// 查找分类
for cate in &cate_list {
if cate.id == t.category_id {
t.category_name = cate.name.clone();
break;
}
}
topic_list.push(t);
}
let paginate = blog_types::Paginate {
page: reply.page,
page_size: reply.page_size,
page_totoal: reply.page_totoal,
record_total: reply.record_total,
data: topic_list,
};
文章列表需要接收多个可选参数,我们对其进行定义,并通过 axum 的 Query
进行获取:
为了方便在模板中组合成url参数,我们还定义了对应的结构体,并对其实现 From<QueryParams>
,方便从获取到的参数转换成模板中所需要的参数:
#[derive(Deserialize, Serialize)]
pub struct QueryParamsForUrl {
pub category_id: i32,
pub keyword: String,
pub page: i32,
}
impl From<QueryParams> for QueryParamsForUrl {
fn from(p: QueryParams) -> Self {
Self {
category_id: match p.category_id {
Some(cid) => cid,
None => 0,
},
keyword: match p.keyword {
Some(kw) => kw,
None => "".to_string(),
},
page: p.page.unwrap_or(0),
}
}
}
请点击查看 handler::index()
的完整代码。
博客文章详情 handler::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/实现前台服务