在上一篇文章中,我展示了一个使用Actix 0.7和同步处理程序的Rust网络应用实例。
这一次,我们将把这个简单的应用从Actix 0.7转移到Actix 1.x,并且我们将完全异步化它。我建议你阅读上述的帖子,或者至少浏览一下代码示例,以便跟随本帖和我们要做的改变。
第一步是将旧的Actix 0.7应用程序中的处理程序异步化,然后我们将整个应用程序移到Actix 1.0。这样做的目的是首先看看使应用程序完全无阻塞有多容易(或多难)。
开始吧!
转移到异步处理程序
好了,本节将展示我们如何从actix的同步处理请求转移到异步处理。之前实现的问题是,虽然actix默认是异步处理传入的请求,但我们使用的是reqwest库的同步版本,所以我们在每个请求上都有阻塞的东西。
async 在这个例子中,我们将通过使用reqwest 的模块来改变这种情况。
不幸的是,要想一步一步地做到这一点并保持所有的编译是相当困难的,因为我们将通过Futures而不是Results。然而,需要改变的代码并不多,我们将看看两个处理程序(一个是POST ,一个是GET ),看看从处理程序到外部HTTP调用的区别。
我们将首先从HTTP调用开始,然后再回到处理程序。我们需要做的第一件事是使用reqwest 的异步版本。
use reqwest::r#async::{Client, Response};
use futures::Future;
我们需要r#async ,因为async 现在是rust中的一个保留词。
好吧,让我们从获取活动的GET 请求开始。这是我们拥有的最简单的情况,我们需要将其转换为返回一个impl Future ,而不是Result ,同时仍然像以前一样传播错误。
fn get(path: &str, jwt: &str) -> impl Future<Item = Response, Error = reqwest::Error> {
let client = Client::new();
client
.get(path)
.header("Authorization", format!("Bearer {}", jwt))
.send()
.and_then(|res: Response| futures::future::ok(res))
.map_err(|err| {
println!("Error during get request: {}", err);
err
})
}
pub fn get_activities(jwt: &str) -> impl Future<Item = ActivitiesResponse, Error = reqwest::Error> {
let activities_path = format!("{}/activities", BASE_URL);
get(&activities_path, jwt).and_then(|mut res: Response| res.json::<ActivitiesResponse>())
}
正如你所看到的,这里没有什么变化--Result<ActivitiesResponse, reqwest::Error> 现在是impl Future<Item = ActivitiesResponse,, Error = request::Error> ,我们需要使用and_then 和map_err 来返回正确的类型。
在reqwest ,只需使用async 客户端,将响应包在一个futures::future::ok ,并映射错误,就足以完成工作了。
好了,这还不算太糟。接下来让我们看一下POST ,在这里我们也要处理一个有效载荷。
fn post(
path: &str,
body: &HashMap<&str, &str>,
jwt: &str,
) -> impl Future<Item = Response, Error = reqwest::Error> {
let client = Client::new();
client
.post(path)
.json(&body)
.header("Authorization", format!("Bearer {}", jwt))
.send()
.and_then(|res: Response| futures::future::ok(res))
.map_err(|err| {
println!("Error during post request: {}", err);
err
})
}
pub fn create_activity(
activity: &ActivityRequest,
jwt: &str,
) -> impl Future<Item = ActivityResponse, Error = Error> {
let mut body: HashMap<&str, &str> = HashMap::new();
body.insert("name", &activity.name);
body.insert("color", &activity.color);
body.insert("integration", &activity.integration);
let path = format!("{}/activities", BASE_URL);
post(&path, &body, jwt)
.and_then(|mut res: Response| res.json::<ActivityResponse>())
.map_err(|e| format_err!("error creating activity, reason: {}", e))
}
变化实际上与GET 的例子相同--在这种情况下,我们将错误转换为failure::Error ,但除此之外都是一样的。准备有效载荷并将其添加到请求中的代码也没有改变,所以我们在这里是安全的。
我们可以对所有的处理程序和reqwest 包装函数重复这个练习--所有的过程都是一样的。
在改变了所有的获取函数后,实际获取数据在每种情况下都返回一个Future 。接下来,我们要对调用我们上面更新的方法的actix处理程序进行异步化。这个过程与HTTP调用的过程基本相同。同样,我们将从获取活动的处理程序开始。
pub fn get_activities(
state: State<AppState>,
) -> impl Future<Item = HttpResponse, Error = AnalyzerError> {
let jwt = &state.jwt;
let log = state.log.clone();
external::get_activities(jwt)
.map_err(move |e| {
error!(log, "Get Activities ExternalServiceError: {}", e);
AnalyzerError::ExternalServiceError
})
.and_then(|data| json_ok(&data))
}
同样,我们简单地将返回类型从Result 改为impl Future 。然而,这一次,我们没有使用Json 方法来转换并从函数中返回一个数据对象,而是使用json_ok 辅助方法返回我们自己的HttpResponse 。
我们使用这个json_ok 函数将我们的响应包装成一个JSON有效载荷。它还将内容类型和正文设置为一个200 OK HTTP响应。
fn json_ok<T: ?Sized>(data: &T) -> Result<HttpResponse, AnalyzerError>
where
T: serde::ser::Serialize,
{
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(serde_json::to_string(&data).unwrap())
.into())
}
好了,正如我们在上面所做的HTTP调用方法一样,我们也要看一下POST 处理程序。剧透一下:和其他的变化没有什么区别。
pub fn create_activity(
(req, activity): (HttpRequest<AppState>, Json<ActivityRequest>),
) -> impl Future<Item = HttpResponse, Error = AnalyzerError> {
let jwt = &req.state().jwt;
let log = req.state().log.clone();
info!(log, "creating activity {:?}", activity);
external::create_activity(&activity, jwt)
.map_err(move |e| {
error!(log, "Create Activity ExternalServiceError {}", e);
AnalyzerError::ExternalServiceError
})
.and_then(|data| json_ok(&data))
}
好了,现在剩下的最后一件事是使用main.rs 中正确的路由函数来连接async 处理程序。
.scope("/rest/v1", |v1_scope| {
v1_scope.nested("/activities", |activities_scope| {
activities_scope
.resource("", |r| {
r.method(http::Method::GET)
.with_async(handlers::get_activities);
r.method(http::Method::POST).with_async_config(
handlers::create_activity,
|cfg| {
(cfg.0).1.error_handler(handlers::json_error_handler);
},
)
})
.resource("/{activity_id}", |r| {
r.method(http::Method::GET)
.with_async(handlers::get_activity);
r.method(http::Method::DELETE)
.with_async(handlers::delete_activity);
r.method(http::Method::PATCH).with_async_config(
handlers::edit_activity,
|cfg| {
(cfg.0).1.error_handler(handlers::json_error_handler);
},
);
})
})
})
这里的区别只是对with_async 和with_async_config 的使用,它们是用于基于未来的处理程序的方法。
就这样了--现在我们的处理程序已经完全无阻塞了。)
接下来,我们将把整个应用程序迁移到Actix 1.0。
迁移到Actix 1.0
我们将使用Actix迁移指南来看看我们需要改变什么来迁移到1.0。第一步,也是最重要的一步,当然是更新依赖关系,看看有什么问题。
actix-web = "1.0.0"
对我们的应用程序来说,最大的不同是新的资源注册API,它使用了新的service() 方法,该方法与web::scope 和web::resource 一起被用来构造路由。
另外,actix_web::server 被HttpServer 取代,当调用.run() 时,它返回一个Result 。
新的Actix 1.0兼容路由配置看起来像这样。
match HttpServer::new(move || {
App::new()
.data(AppState {
jwt: jwt.to_string(),
log: log.clone(),
})
.service(web::scope("/rest/v1").service(
web::scope("/activities").service(web::resource("")
.route(web::get().to_async(handlers::get_activities))
.route(web::post().to_async(handlers::create_activity)),
)
.service(web::resource("/{activity_id}")
.route(web::get().to_async(handlers::get_activity))
.route(web::delete().to_async(handlers::delete_activity))
.route(web::patch().to_async(handlers::edit_activity)),
)))
.service(web::resource("/health").route(web::get().to(handlers::health)))
})
.bind("0.0.0.0:8080")
.unwrap()
.run()
{
Ok(_) => info!(runner_log, "Server Stopped!"),
Err(e) => error!(runner_log, "Error running the server: {}", e),
};
另一个变化是如何将AppState 传递给App 。现在是通过使用.data() 函数而不是with_state() 。除此之外,自定义错误处理现在更简单了,似乎必须使用with_async_config 这样单独配置自定义错误处理的整个概念已经消失了。很好。
这意味着,我们的处理程序配置实际上更短,更容易阅读。
下一个重大变化是,现在所有的处理函数都必须使用提取器,所以只有.to() 和.to_async() ,不再有.f() 等。
然而,这意味着我们所有的处理函数都需要改变,因为它们都使用HttpRequest<AppState> ,并从那里手动提取数据。
所以
pub fn get_activities(
state: State<AppState>,
) -> impl Future<Item = HttpResponse, Error = AnalyzerError> {
let jwt = &state.jwt;
let log = state.log.clone();
...
变成
pub fn get_activities(
data: Data<AppState>,
) -> impl Future<Item = HttpResponse, Error = AnalyzerError> {
let jwt = &data.jwt;
let log = data.log.clone();
...
而
pub fn get_activity(
(req, activity_id): (HttpRequest<AppState>, Path<String>),
) -> impl Future<Item = HttpResponse, Error = AnalyzerError> {
let jwt = &req.state().jwt;
let log = req.state().log.clone();
...
变成
pub fn get_activity(
data: Data<AppState>,
activity_id: Path<String>,
) -> impl Future<Item = HttpResponse, Error = AnalyzerError> {
let jwt = &data.jwt;
let log = data.log.clone();
...
而最后
pub fn create_activity(
(req, activity): (HttpRequest<AppState>, Json<ActivityRequest>),
) -> impl Future<Item = HttpResponse, Error = AnalyzerError> {
let jwt = &req.state().jwt;
let log = req.state().log.clone();
...
变成
pub fn create_activity(
data: Data<AppState>,
activity: Json<ActivityRequest>,
) -> impl Future<Item = HttpResponse, Error = AnalyzerError> {
let jwt = &data.jwt;
let log = data.log.clone();
...
我想你已经明白了... :)
另一个变化是,Path,Json 和Data 被移到actix_web::web 包中,所以我们需要使用。
use actix_web::web::{Data, Json, Path};
我们也可以删除我们的json_error_handler 方法,因为我们不需要再以这种方式配置自定义错误处理程序。
完美--我们现在有了一个无阻塞的Actix 1.0网络应用程序,从一个部分阻塞的Actix 0.7程序开始。这正是我们所要做的。)
完整的例子代码可以在这里找到
结论
Actix 1.0是一个重要的里程碑,据我所知,他们让这个框架变得更加平易近人,并且在人体工程学方面也有了很大的改进。
将我的简单应用转移到async和1.0上比我想象的要容易得多。坦率地说,我不得不质疑我最初为了保持简单而不做异步的做法,因为无论是Actix 0.7还是1.0,实际上都没有涉及更多的复杂性。
我希望这是个有用的指南,我期待着在未来做更多与Actix有关的事情。)