异步化一个Actix网络应用程序并将其转移到Actix 1.0的方法

98 阅读7分钟

上一篇文章中,我展示了一个使用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_thenmap_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_asyncwith_async_config 的使用,它们是用于基于未来的处理程序的方法。

就这样了--现在我们的处理程序已经完全无阻塞了。)

接下来,我们将把整个应用程序迁移到Actix 1.0。

迁移到Actix 1.0

我们将使用Actix迁移指南来看看我们需要改变什么来迁移到1.0。第一步,也是最重要的一步,当然是更新依赖关系,看看有什么问题。

actix-web = "1.0.0"

对我们的应用程序来说,最大的不同是新的资源注册API,它使用了新的service() 方法,该方法与web::scopeweb::resource 一起被用来构造路由。

另外,actix_web::serverHttpServer 取代,当调用.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,JsonData 被移到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有关的事情。)

资源