开始
在工作空间的 Cargo.toml 里添加 "ch01/basics"
,添加之后内容如下所示:
[workspace]
members = [
"ch01/helloworld",
"ch01/error-handling",
"ch01/state",
"ch01/nested-routing",
"ch01/static-files",
"ch01/basics"
]
然后在文件夹 ch01 下创建 basics
项目,并和之前一样添加好依赖、编写好 main.rs 的内容。
(项目的 Cargo.toml)
[dependencies]
actix-web = "4.2.1"
(main.rs)
use actix_web::{HttpServer, App};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
这一次,我们将官方源码中的 static 下的内容直接复制到项目下。有两个 Html、一张图片和一个图标文件。Html 的内容也不复杂,welcome.html 显示了一行字和一个图片,404.html 中则是一个超链接,另一个是一行文字。
在官方例子的 Readme.md 中,列出了访问路径和会出现的结果。那么这些也将是我们之后需要做的。
- http://localhost:8080/async-body/bob
- http://localhost:8080/user/bob/ text/plain download
- http://localhost:8080/test (return status switch GET or POST or other)
- http://localhost:8080/favicon
- http://localhost:8080/welcome
- http://localhost:8080/notexit display 404 page
- http://localhost:8080/error Panic after request
添加中间件
中间件先从最熟悉的 Logger
开始添加。
在 Cargo.toml 中添加对 env_logger
和 log
的依赖:
env_logger = "0.9"
log = "0.4"
然后在主函数 HttpServer
初始化前,初始化好 env_logger
,并使用 log::info!()
打印一段消息,表示程序启动。
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
log::info!("starting HTTP server at http://localhost:8080");
HttpServer::new(...)
.bind(("127.0.0.1", 8080))?
.run()
.await
}
之后在 App
中,添加 Logger
中间件。
use actix_web::{HttpServer, App, middleware};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
log::info!("starting HTTP server at http://localhost:8080");
HttpServer::new(|| {
App::new()
// add here
.wrap(middleware::Logger::default())
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
接下来添加 Compress
中间件,暂时不清楚用处。值得注意的是 Compress
中间件需要优先注册,所以将其放在 Logger
前:
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// log...
HttpServer::new(|| {
App::new()
// add here
.wrap(middleware::Compress::default())
.wrap(middleware::Logger::default())
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
之后添加 SessionMiddleware
中间件,但是需要提前添加引用:
actix-session = { version = "0.7", features = ["cookie-session"] }
查看文档,注意到两条比较重要的信息,即 SessionMiddleware
主要的工作:
- 根据 Session 的状态和已经执行的操作,来指示 Session 存储后端为该 Session 附加上创建/更新/删除/检索状态。(按我的理解,就是可以对 Session 执行四种操作)
- 在客户端,设置/删除 Cookie,确保用户在发送不同的 Http 请求时, 始终关联同一个 Session。
SessionMiddleware
的添加比其他两个中间件复杂一点。首先需要创建一个数组 SESSION_SIGNING_KEY
,并通过这个数组生成一个 key
,然后在 SessionMiddleware
构建的时候传入。官方源码中,还设置了 cookie_sercue
,大致是用来确保 http 也能够传输:
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
use actix_web::{HttpServer, App, middleware};
static SESSION_SIGNING_KEY: &[u8] = &[0; 64];
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// log...
let key = actix_web::cookie::Key::from(SESSION_SIGNING_KEY);
HttpServer::new(move || {
App::new()
.wrap(middleware::Compress::default())
// add here
.wrap(
SessionMiddleware::builder(CookieSessionStore::default(), key.clone())
.cookie_secure(false)
.build()
)
.wrap(middleware::Logger::default())
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
路由配置
一共是 9 个路由,按官方源码来一个一个配置。(注意不是之前 Readme.md 中的顺序)
favicon
路由配置
favicon
函数对应的路径是 "/favicon"
,只做一件事,就是返回 favicon.ico 文件。在此之前需要先添加对 actix-file
的依赖:
actix-files = "0.6"
添加 favicon
函数,直接使用 actix_web::get
宏配置路径。注意到这时使用的 Result
为 actix_web::Result
:
use actix_files::NamedFile;
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
use actix_web::{HttpServer, App, middleware, Responder, get, Result};
#[get("/favicon")]
async fn favicon() -> Result<impl Responder> {
Ok(NamedFile::open("static/favicon.ico")?)
}
在主函数中,通过 service
函数,将 favicon
路由添加到 App
中:
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// ....
HttpServer::new(move || {
App::new()
// middle ware register
// add favicon here
.service(favicon)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
可以尝试运行并访问 http://localhost:8080/favicon,没出错应该能够看到齿轮图标。
welcome
路由
welcome
路由的路径也是直接通过 actix_web::get
宏来配置,指定的路径为 "/welcome"
;welcome
函数传入的参数类型为 HttpRequest
和 Session
,两种类型都实现了 FromRequest
trait。官方源码中,首先打印了这一次请求的内容:
use actix_session::{SessionMiddleware, storage::CookieSessionStore, Session};
use actix_web::{HttpServer, App, middleware, Responder, get, HttpRequest, HttpResponse, Result};
#[get("/welcome")]
async fn welcome(req: HttpRequest, session: Session) -> Result<HttpResponse> {
print!("{req:?}");
todo!()
}
然后,为 session
添加了一个计数器,在每次访问 "/welcome"
加一。设置计数器的值的方式是,先新建一个 counter
整型可变变量,初始值为 1,然后获取 session
中的 counter
,并提取到 count
变量中。如果存在则将 counter
的值设置为 count
加一,否则则会将 counter
的初始值设置给 session
中的counter
。这样的话,在第一次访问时,因为 session
中没有 counter
,所以在第一次访问之后,session
中新增了 counter
,并且值为 1。在之后访问时,就可以获取到 counter
的值。
#[get("/welcome")]
async fn welcome(req: HttpRequest, session: Session) -> Result<HttpResponse> {
print!("{req:?}");
// set counter for session
let mut counter = 1;
if let Some(count) = session.get::<i32>("counter")? {
counter = count + 1;
}
session.insert("counter", counter)?;
todo!()
}
接下来是写好返回值。返回的状态码为 200,内容类型为 plaintext,内容直接为 welcome.html
中的所有内容。
use actix_web::{
HttpServer, App, middleware, Responder, get, HttpRequest, HttpResponse, Result,
http::{
StatusCode, header::ContentType
}
};
#[get("/welcome")]
async fn welcome(req: HttpRequest, session: Session) -> Result<HttpResponse> {
print!("{req:?}");
// set counter for session
let mut counter = 1;
if let Some(count) = session.get::<i32>("counter")? {
counter = count + 1;
}
session.insert("counter", counter)?;
Ok(HttpResponse::build(StatusCode::OK)
.content_type(ContentType::plaintext())
.body(include_str!("../static/welcome.html")))
}
最后将 welcome
添加到 App
中。
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// ...
HttpServer::new(move || {
App::new()
// middle ware register
.service(favicon)
// add here
.service(welcome)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
可以尝试运行并访问 http://localhost:8080/welcome。没出错应该会看到 welcome.html 的源码。可以尝试将
.content_type(ContentType::plaintext())
改为.content_type(ContentType::html())
,不过图片会无法访问,所以无法显示。
路径 "/user/{name}"
的路由
路径对应的路由为 with_param
,表示访问时带有参数。应该是匹配 {name}
的内容,使用提取器来获取。提取器的位置在函数的传入参数中,这里使用到了 Path
提取器。
use actix_web::web;
async fn with_param(req: HttpRequest, path: web::Path<String>) -> HttpResponse {
print!("{req:?}");
todo!()
}
返回的状态码为 200,内容类型为 plaintext,返回的内容为一串字符串,不过字符串中会出现路径中 {name}
处的字符串。
async fn with_param(req: HttpRequest, path: web::Path<String>) -> HttpResponse {
print!("{req:?}");
HttpResponse::Ok()
.content_type(ContentType::plaintext())
.body(format!("Hello {}!", path))
}
最后在 App
的 service
配置路径和路由:
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// ....
HttpServer::new(move || {
App::new()
// middel ware
.service(favicon)
.service(welcome)
// add here
.service(web::resource("/user/{name}").route(web::get().to(with_param)))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
如果访问 http://localhost:8080/user/bob,可以看到网页上显示的字符串为 Hello bob!
,当然如果 bob
换成其他字符串也能够显示。
在传入的参数中,我没有像官方一样使用 web::Path<(String,)>
,指定元组,而是直接指定了 String
。当然也是能够运行的。另外,也可以在函数上直接使用 #[get("/user/{name}")]
配置路由的路径,这样一来,App
这边应该改为 App.service(with_param)
。
路径 "/async-body/{name}"
的路由
路径 "/async-body/{name}"
对应的路由函数为 response_body
。传入的参数为路径提取器,提取类型为 String
。
async fn response_body(path: web::Path<String>) -> HttpResponse {
todo!()
}
之后从提取器中获取提取的值,在返回 Http 响应时,返回了响应流。响应流通过 stream!
创建了迭代器,模拟实现异步操作。返回的内容为 Hello {name}!
,分单词符号异步传输。
首先需要添加 async-stream
依赖。
async-stream = "0.3"
然后编写 response_body
函数:
use std::convert::Infallible;
use async_stream::stream;
async fn response_body(path: web::Path<String>) -> HttpResponse {
let name = path.into_inner();
HttpResponse::Ok().streaming(stream! {
yield Ok::<_, Infallible>(web::Bytes::from("Hello "));
yield Ok::<_, Infallible>(web::Bytes::from(name));
yield Ok::<_, Infallible>(web::Bytes::from("!"));
})
}
yield Ok::<T,E>
中,_
代表自动识别类型,后面传入的是 web::Bytes
,所以这里也是这个类型。而 E
指定的类型为 Infallible
,查看文档,发现这个类型代表着 Result
不会返回错误。
编写好后,将 response_body
添加到 App
里。当然,路径可以通过 #[get()]
宏直接配置,也可以通过 web::resource().to()
或 web::resource().route(web::get().to())
配置。
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// ....
HttpServer::new(move || {
App::new()
// middle ware
.service(favicon)
.service(welcome)
.service(with_param)
// add here
.service(web::resource("/async-body/{name}").route(web::get().to(response_body)))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
运行并访问 http://localhost:8080/async-body/bob,应该会出现 Hello bob!
。
路径 /test
的路由
/test
的路由采用了匿名函数的形式,直接在配置时添加。在访问这个路径时,如果请求的方法为 GET
,则会返回 200;如果为 POST
,则会返回 405;其他的请求方法,则会返回 404。
use actix_web::{
HttpServer, App, middleware, Responder, get, HttpRequest, HttpResponse, Result,
http::{
StatusCode, header::ContentType, Method
}, web
};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// ....
HttpServer::new(move || {
App::new()
// middle ware
// other path and route
// path "/test"
.service(
web::resource("/test").to(|req: HttpRequest| match *req.method() {
Method::GET => HttpResponse::Ok(),
Method::POST => HttpResponse::MethodNotAllowed(),
_ => HttpResponse::NotFound(),
})
)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
可以使用一些工具来对 http://localhost:8080/test 进行测试。
路径 /error
的路由
和 /test
的路由一样是直接使用匿名函数来配置。函数内就构建了一个 InternalError
并返回。
use std::{convert::Infallible, io};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// ....
HttpServer::new(move || {
App::new()
// other configuration
// path "/error"
.service(
web::resource("/error").to(|| async {
actix_web::error::InternalError::new(
io::Error::new(io::ErrorKind::Other, "test"),
StatusCode::INTERNAL_SERVER_ERROR,
)
})
)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
编写完代码后,运行并访问 http://localhost:8080/error,可以看到 test
,和返回值中创建并传入的 io::Error
中的字符串一致。而控制台中,会得到一行状态码为 500 的输出。
路径 /static
的路由
在访问路径 /static
时,会直接以文件形式显示目录 static 下的所有内容。
use actix_files::{NamedFile, Files};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// ....
HttpServer::new(move || {
App::new()
// other configuration
// path "/static"
.service(Files::new("/static", "static").show_files_listing())
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
运行并访问 http://localhost:8080/static,应该可以看到 static
目录下的所有文件,这些文件都是可以访问的。
根路径的路由
在访问根路径时,会重定向到 static/welcome.html。
use actix_web::{
HttpServer, App, middleware, Responder, get, HttpRequest, HttpResponse, Result,
http::{
StatusCode, header::{ContentType, self}, Method
}, web
};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// ...
HttpServer::new(move || {
App::new()
// other configuration
// path "/"
.service(
web::resource("/").route(web::get().to(|req: HttpRequest| async move{
println!("{req:?}");
HttpResponse::Found()
.insert_header((header::LOCATION, "static/welcome.html"))
.finish()
}))
)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
在 insert_header
中,传入的 header::LOCATION
代表重定向。如果运行并访问 http://localhost:8080/,会发现链接变成了 http://localhost:8080/static/welcome.html,并且显示了 welcome.html 的网页内容。
默认路由配置
默认路由是在访问其他路径时,如果没有对应路由,则会运行默认路由。
默认路由对应的函数为 default_handler
。函数内,逻辑大概是,如果请求方法为 GET
,则返回 404.html 网页;否则返回 405 状态码:
use actix_web::{
HttpServer, App, middleware, Responder, get, HttpRequest, HttpResponse, Result,
http::{
StatusCode, header::{ContentType, self}, Method
}, web, Either
};
async fn default_handler(req_method: Method) -> Result<impl Responder> {
match req_method {
Method::GET => {
let file = NamedFile::open("static/404.html")?
.customize()
.with_status(StatusCode::NOT_FOUND);
Ok(Either::Left(file))
},
_ => Ok(Either::Right(HttpResponse::MethodNotAllowed().finish()))
}
}
Either
的作用大概是将两种不同的类型塞到同一个类型中。
编写好后访问没有配置过路由的路径,会显示 404 的网页。
总结
列一下这次出现的一些东西:
路由配置
- 直接使用宏配置,如
#[get()]
App::new().service(web::resouce().to())
,即调用App
的service
函数配置,路径使用web::resouce
配置,路由通过链式调用.to()
配置。App::new().service(web::resouce().route(web::{method}().to()))
,{method}
带指请求方法。- 静态文件路由,
Files::new("{网站路径}", {文件路径})
,单个文件调用index_file
,要显示列表调用show_files_listing
。 - 在路由中返回静态文件,
NamedFile::open
。
- 直接使用宏配置,如
Session的使用:
Cookie 的创建:首先创建/生成/读取一个 64 比特的字符串,即
&[u8]
数组,长度为 64。然后通过actix_web::cookie::Key::from
创建Key
。之后在App
中添加中间件SessionMiddleware
时,通过SessionMiddleware::builder
传入Key
。Cookie 的使用:使用时,在路由函数的传入参数中,指定一个类型为
Session
的变量,通过调用这个变量的get::<T>
来获取 cookie,也可以通过insert
插入一个 key/value 对。