rust web框架actix入门
鸣谢¶
感谢译者的辛苦翻译, 可以帮译者在git上点一个星星
hello-world.rs¶
use actix_web::{Error,middleware, web, App, HttpRequest, HttpServer, Responder, HttpResponse,guard, dev::HttpResponseBuilder};
use serde::{Deserialize,Serialize};
use actix_web::http::{HeaderName, HeaderValue, StatusCode};
use cookie::Cookie;
use actix_files::{Files, NamedFile};
use std::sync::Mutex;
use futures::future::{ready, Ready};
use std::fs::File;
use futures::StreamExt;
async fn index(req: HttpRequest) -> &'static str { // return 的参数会转为 Responder
println!("REQ: {:?}", req); // 打印请求的相关信息
"Hello world!"
}
async fn cookies() -> impl Responder{
/*
cookie 练习
*/
let mut res = HttpResponse::Ok();
res.cookie(Cookie::build("say", "hello").finish()); // 设置一个 cookie
res.finish()
}
async fn header() -> impl Responder {
/*
header 练习
*/
let mut res = HttpResponse::Ok();
res.header("language", "python") // 添加一个请求头(无论有没有相同的请求头, 都会新增一个响应头, 同样的 key 可能会有多个)
.set_header("language", "javascript") // 设置一个请求头(设置一个响应头(如果没有该key则会新建一个, 如果有则会覆盖))
.set_header("say", "hello1") // 设置一个响应头(如果没有该key则会新建一个, 如果有则会覆盖)
.header("say", "hello2"); // 添加一个响应头
res.finish()
}
#[derive(Deserialize,Serialize,Debug)] // 为结构体实现序列化和反序列化的功能
struct UserInfo{
user_id:i32,
mobile:String
}
async fn get_user1(req:HttpRequest, params: web::Path<(UserInfo)>,) -> impl Responder{
// Path 可以从请求中提取 路径中的参数
println!("get_user1_REQ: {:?}", req);
let p = params.0; // 接收到的外部params参数 可以通过下标0 取出
println!("收到了用户: {:?} 请求", p);
let mut res = HttpResponse::Ok();
res.json(p)
}
async fn get_user2(req:HttpRequest) -> impl Responder{
println!("获得Path 参数相关 req.match_info(): {:?}", req.match_info());
let user_id: i32 = req.match_info().query("user_id").parse().unwrap(); // 使用 query
let mobile: String = req.match_info().get("mobile").unwrap().parse().unwrap(); // 使用 get
HttpResponse::Ok().json(UserInfo{user_id:user_id,mobile:mobile})
}
async fn get_user3(info:web::Query<UserInfo>) -> impl Responder{
println!("仅仅在请求查询字符串中有 'UserInfo' 相关字段时才会被调用info: {:?}", info);
HttpResponse::Ok().json(info.into_inner())
}
async fn get_user4(req:HttpRequest,info:web::Json<UserInfo>) -> impl Responder{
// 从body中接收了一个 json 数据 info
HttpResponse::Ok().json(info.into_inner())
}
async fn get_user5(req:HttpRequest,info:web::Form<UserInfo>) -> impl Responder{
// 从body中接收了一个 表单数据 数据 info
// 仅当 content type 类型是 *x-www-form-urlencoded* 是 handler处理函数才会被调用
// 且请求中的内容能够被反序列化到一个 "FormData" 结构体中去
HttpResponse::Ok().json(info.into_inner())
}
// async fn my_body(info:String) -> impl Responder{ // payload 转为字符串
async fn my_body(mut info:web::Payload) -> impl Responder{
// TODO 我这里提取不出来 可能是版本不对
// info.next().await
HttpResponse::Ok()
}
#[derive(Debug)]
struct Status{
count:Mutex<i32> // <- Mutex 必须安全的在线程之间可变
}
async fn get_states_less(data:web::Data<Status>) -> String{
/*
由 app 传入的数据
*/
let mut count = data.count.lock().unwrap(); // 等待获取锁
*count -= 1; // 获取到锁之后 减少1
println!("接收到了一个 state, 减少1后: {:?}", count);
format!("count: {}", count)
}
async fn get_states_inc(data:web::Data<Status>) -> String{
/*
由 app 传入的数据
*/
let mut count = data.count.lock().unwrap(); // 等待获取锁
*count += 1; // 获取到锁之后 增加1
println!("接收到了一个 state, 增加1后: {:?}", count);
format!("count: {}", count)
}
// 自定义 struct
#[derive(Serialize)]
struct Student{name: String}
// 为自定义struct 实现 Responder
impl Responder for Student{
type Error = Error;
type Future = Ready<Result<HttpResponse, Error>>;
fn respond_to(self, _req: &HttpRequest) -> Self::Future {
let body = serde_json::to_string(&self).unwrap();
// 创建响应并设置Content-Type
ready(Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body)))
}
}
async fn my_response()-> impl Responder{ // 返回自定义类型
Student{name:"小明".to_string()}
}
async fn my_err1(file_name:web::Path<String>) -> std::io::Result<NamedFile>{
// 如果一个处理函数在一个Result中返回Error(指普通的Rust trait std::error:Error),此Result也实现了ResponseError trait的话, actix-web将使用相应的actix_web::http::StatusCode 响应码作为一个Http response响应渲染.默认情况下会生成内部服务错误
// impl<T: Responder, E: Into<Error>> Responder for Result<T, E>{} // 这里为普通的 Result 实现了一些 trait, 可以点进 Responder 看
println!("{:?}", format!("static/{}", file_name));
let ret = NamedFile::open(format!("static/{}", file_name));
ret
}
// 导入自定义错误需要的包
use derive_more::{Display, Error};
use actix_web::dev::BodyEncoding;
// 定义结构体(需要实现相关 trait)
#[derive(Debug,Display,Error)]
#[display(fmt = "my error: {}", msg)]
struct ErrResp{msg:&'static str}
// 为结构体使用 ResponseError 的默认实现即可 (ResponseError 的一个默认实现 error_response() 会渲染500)
impl actix_web::error::ResponseError for ErrResp{}
async fn my_err2() -> Result<HttpResponse,ErrResp>{
Err(ErrResp{msg:"自定义的错误默认实现了 500"}) // 返回自定义的错误
}
// 使用 enum 重写 ResponseError 产生更多样的错误进行返回
#[derive(Debug,Display,Error)]
enum ErrEnum{
#[display(fmt = "ServerError: {}",msg)]
ServerError{msg:&'static str},
#[display(fmt = "Forbidden: {}",msg)]
Forbidden{msg:&'static str},
#[display(fmt = "ResError: {}",msg)]
ResError{msg:&'static str}
}
// 重写
impl actix_web::error::ResponseError for ErrEnum{
fn status_code(&self) -> StatusCode {
match *self{
ErrEnum::ServerError{..} => StatusCode::from_u16(500).unwrap(), // 自定义状态码 100 - 599
ErrEnum::Forbidden{..} => StatusCode::FORBIDDEN, // 使用预设的状态码
ErrEnum::ResError{..} => StatusCode::from_u16(599).unwrap(),
}
}
fn error_response(&self) -> HttpResponse {
let mut resp = HttpResponseBuilder::new(self.status_code());
resp.set_header(
actix_web::http::header::CONTENT_TYPE,
actix_web::http::header::HeaderValue::from_static("text/plain; charset=utf-8"),
);
resp.body(self.to_string()) // impl<T: fmt::Display + ?Sized> ToString for T
}
}
async fn my_err3(flag:web::Path<i32>)-> Result<HttpResponse,ErrEnum>{
let flag = flag.into_inner();
match flag {
0 => Ok(HttpResponse::Ok().body("成功")),
1 => Err(ErrEnum::Forbidden{msg:"没有权限"}),
_ => Err(ErrEnum::ServerError{msg:"服务器内部错误"})
}
}
// 统一错误处理使用 map_err, 隐藏真正的错误到给用户展示
async fn my_err4()-> Result<HttpResponse,ErrEnum>{
// 假设中间发生了一些错误
let e:Result<HttpResponse,&str> = Err("2233"); // 模拟一个错误
e.map_err(|e| {
println!("发生一个错误: {:?}", e);
// 处理错误
ErrEnum::ServerError { msg: "出错了嗷" }
})?;
Ok(HttpResponse::Ok().finish())
}
async fn res(flag:web::Path<i32>) -> actix_web::Result<HttpResponse>{
println!("进入 url_for1 视图, 接收到参数: {:?}", flag);
Ok(
HttpResponse::Ok().body(flag.into_inner().to_string())
)
// let url = req.url_for("foo", &["1", "2", "3"])?;
// println!("url: {:?}", url);
// Ok(
// HttpResponse::Ok()
// .set_header(actix_web::http::header::LOCATION, url.as_str())
// .finish()
// )
}
async fn url_for(req:HttpRequest) -> actix_web::Result<HttpResponse>{
let url = req.url_for("res_name", &["666"]).unwrap();
println!("即将跳转: {:?}", url);
// 使用 Found 跳转
Ok(HttpResponse::Found()
.header(actix_web::http::header::LOCATION, url.as_str()) // 跳转
.finish())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
// 关于路径 前缀没有包含斜杠,那么会默认自动的插入一个斜杠
// 请求处理器
// 默认情况下 actix-web 为 &‘static str , String 等提供了 Responder 的标准实现. 完整的实现清单可以参考 Responder documentation
// 请求处理发生在两个阶段.首先处理对象被调用,并返回一个实现了 Responder trait的任何对象.然后在返回的对象上调用 respond_to() 方法,将其 自身转换成一个 HttpResponse 或者 Error
// 请求处理器可以自定义返回类型, 相应的, 自定义返回的类型需要实现 Responder trait
// 创建一个共享可变状态, 下面使用 move 移动到app中
let counter = web::Data::new(Status { count: Mutex::new(0) });
HttpServer::new(move || {
App::new()
// enable logger
.wrap(middleware::Logger::default()) // 添加一个 log 中间件, 用来记录请求
// .wrap(middleware::NormalizePath::default()) // TODO 这里我并没有发现有嘛用,启用了之后下面的资源都404了, 路径正规化并重定向: 比如请求的 ///res//// 很多斜杠的资源,在适配的时候会替换成 /res/
// 状态 state
// .data(Status { count: 0 })// 这里注册一个 普通状态, 状态通过 data 向视图中传递,State也能被中间件访问, 当然也可以单独给 service中的 resource 单独使用,
.app_data(counter.clone())// 共享可变状态 状态通过 .app_data 向视图中传递 , 为了防止所有权转移需要 clone
.service( // service 用来注册一个服务
web::resource("/index.html").route(web::get().to(|| actix_web::web::HttpResponse::Ok().json("hello index world"))))// 这里 return 的 &str 实现了Responder trait, 所以会内部调用相关 方法
// .service(web::resource("/").to(index)) // 一个普通的响应体
.service(web::resource("/my_response") // 返回自定义类型(为自定义类型实现 Responder trait, 即可)
.route(web::get().to(my_response)))
// 请求头和cookie
.service(web::resource("/cookie")
.route(web::get().to(cookies)))
.service(web::resource("/header") // 设置请求头相关
.route(web::get().to(header)))
// 视图中的共享可变 状态 state
.service(web::resource("/get_states_less") // 在视图中获取上面注册的状态 修改减1
.route(web::get().to(get_states_less)))
.service(web::resource("/get_states_inc") // 在视图中获取上面注册的状态 修改加1
.route(web::get().to(get_states_inc)))
// 路由分发以及守卫 默认的情况下路由不包含任何的 guards, 因此所有的请求都可以被默认的处理器(HttpNotFound)处理.
// web::get() 和 web::post() 返回一个 Route也就是路由
// 路由上配置的所有guard,进来的请求都必须满足才能通过
// 如果在检查期间注册在路由上的任意一个保护(guard)参数配置返回了false 那么当前路由就会被跳过,然后继续通过有序的路由集合进行匹配.
.service(web::resource("/guard") // 布置一个路由守卫, 其内可以实现一些方法更详细的文档: https://docs.rs/actix-web/3.0.2/actix_web/guard/index.html#functions
// .guard(guard::fn_guard(|req| // 给整个resource 进行校验,如果请求头没有包含user_info就返回了false 返回404
// req.headers.contains_key("user_info"))
// )
.route(web::route() // 给整个resource进行校验, 进行自定义返回
.guard(guard::fn_guard(
|req| !req.headers.contains_key("user_info") // 如果返回 true 则进入 to视图中,这里使用了 ! 返回了 false
))
.to(|| { // 如果校验为 true 则进入到此分支,返回自定义内容, 如果返回false则跳过当前路由, 继续匹配下面的路由
println!("resource 校验失败");
HttpResponse::Forbidden()
}))
.route(web::get() // 单独给 某个 路由, 请求方式添加守卫
.guard(guard::fn_guard(|req| {
println!("子路由 通过了校验");
true
}))
.to(|| {
println!("进入到了视图");
web::HttpResponse::Ok().finish()
})))
// Route::guard() - 注册一个新的守卫, 每个路由都能注册多个guards.
// Route::method() - 请求方法过滤守卫, 每个路由都能注册多个guards.
// Route::to() - 为某个路由注册一个处理函数. 仅能注册一个处理器函数. 通常处理器函数在最后的配置操作时注册.
// Route::to_async() - 注册一个异步处理函数. 仅能注册一个处理器函数. 通常处理器函数在最后的配置操作时注册.
// 提取器
// http://127.0.0.1:8080/user1/2233/13673429931
.service(web::resource("/user1/{user_id}/{mobile}") // 使用 path 提取器 序列化与反序列化 获取路径参数相关, 并返回application/json数据
.route(web::get().to(get_user1)))
// http://127.0.0.1:8080/user2/2233/13673429931
.service(web::resource("/user2/{user_id}/{mobile}") // 使用 match_info().get 或者 match_info().query 方法提取参数
.route(web::get().to(get_user2)))
// http://127.0.0.1:8080/user3?user_id=1718&mobile=13673429931
.service(web::resource("/user3") // 查询字符串 Query 类型为请求参数提供了提取功能. 底层是使用了 serde_urlencoded 包功能
.route(web::get().to(get_user3)))
// JSON格式; {"user_id": 123,"mobile": "13673429931" }
.service(web::resource("user4") // post 请求体中的 JSON 数据, 可以使用Json提取器允将请求 body 中的 JSON 信息反序列化到一个结构体中
.route(web::post().to(get_user4)))
// 表单格式: user_id = 2233, mobile = "13673429931"
.service(web::resource("user5") // post 请求体中的 表单From 数据, 使用 web::From<T> 提取, 注意请求的类型必须是 application/x-www-form-urlencoded
.route(web::post().to(get_user5)))
// 提取整个请求体使用 web::Payload, 也可以使用 String 将整个payload 转为 string类型
.service(web::resource("body")
.route(web::post().to(my_body)))
// Data - 如果你需要访问应用程序状态的话.
// HttpRequest - 如果你需要访问请求, HttpRequest本身就是一个提取器,它返回Self.
// String - 你可以转换一个请求的playload成一个String. 可以参考文档中的example
// bytes::Bytes - 你可以转换一个请求的playload到Bytes中. 可以参考文档中的example
// Playload - 可以访问请求中的playload. example
// 异常处理
// http://127.0.0.1:8080/my_err1/1.txt
.service(web::resource("/my_err1/{file_name}") // 基本错误处理: Actix-web提供了一些常见的非actix error的 ResponseError 实现
.route(web::get().to(my_err1)))
.service(web::resource("my_err2") // 自定义错误响应示例, 为 struct 使用 ResponseError 的默认实现即可
.route(web::get().to(my_err2)))
// http://127.0.0.1:8080/my_err3/0
.service(web::resource("my_err3/{flag}") // 重写 ResponseError 让自定义错误 产生更多样的错误进行返回
.route(web::get().to(my_err3)))
.service(web::resource("my_err4") // 统一错误处理使用 map_err, 隐藏真正的错误到给用户展示
.route(web::get().to(my_err4)))
// 资源模式语法
// 在路由中使用模式可以使用斜杠开头. 如果模式不是以斜杠开头, 在匹配的时候会在前面加一个隐式的斜杠 {foo}/bar/baz 和 /{foo}/bar/baz 是等价的
// {foo}/bar/baz 中, {}标识符意味着“直到下一个斜杠符前,可以接受任何的字符”, 并将其用作 HttpRequest.match_info()对象中的名称
// 模式中的替换标记与正则表达式 [^{}/]+ 相匹配
// 匹配的信息是一个 Params 对象,代表基于路由模式从URL中动态提取出来的部分 可以作为 request.match_info() 中的信息来使用
// 对 foo/{baz}/{bar} 匹配 定义一个字段(foo)和两个标记(baz,and bar)
// foo/1/2 -> Params {'baz': '1', 'bar':'2'} 可以匹配
// foo/abc/def -> Params {'baz': 'abc', 'bar':'def'} 可以匹配
// foo/1/2/ !!! 路径根本无法匹配, 多了一个斜杠 而我们定义的是 foo/{baz}/{bar}
// 替换标记也可以使用正则表达式 扩展的替换标记语法. 在大括号内,替换标记名后必须跟一个冒号, 然后才是正则表达式 注意反斜杠 \ 属于转义字符 需要使用 \\, 或者使用 r#
// 正则匹配 foo/{bar}/{tail:.*} 使用正则匹配后如下
// foo/1/2/ -> Params: { 'bar': '1', 'tail':'2' }
// foo/abc/def/a/b/c -> Params: { 'bar':u'abc', 'tail':'def/a/b/c' }
// 匹配任意数字
.service(web::resource(r#"/number/{num:\d+}"#)
.route(web::get().to(|num:web::Path<i32>|{
println!("使用正则语法匹配到了一些数值: {:?}",num);
HttpResponse::Ok().body(format!("{:?}",num))
})))
// scope 范围匹配 使用 scope 来建立一个 "空间"
.service(
web::scope("/space1").service(
web::scope("/say").service(
web::resource("/hi") // 访问路径 http://127.0.0.1:8080/space1/say/hi
.route(web::get().to(||HttpResponse::Ok().body("get_hi")))
.route(web::post().to(||HttpResponse::Ok().body("post_hi")))
)
)
)
.service(
web::scope("/space2/{flag}") // 访问路径 http://127.0.0.1:8080/{space2}/hello
.service(web::resource("/hello").to(|req:web::HttpRequest|{
println!("req.match_info: {:?}",req.match_info()); // 使用 match_info 获取一下呢
HttpResponse::Ok().body("hello")})
)
)
// 匹配信息
// 所有表示路径匹配段的值都可以在 HttpRequest::match_info() 中获取
.service(web::resource("/match/{flag}/eat")
.route(web::get().to(|flag:web::Path<String>,req:HttpRequest|{
println!("匹配的flag: {}", flag);
println!("匹配的一些信息: {:?}", req.match_info());
HttpResponse::Ok().body("eat")
})))
// 生成资源URL 并进行重定向
.service(web::resource("/url_for")
.route(web::get().to(url_for)))
.service(web::resource("/res/{flag}")
.name("res_name") // 这里设置资源名, 下面就可以使用了
.route(web::get().to(res)))
// 直接重定向 暂时没找到现有的api, 好像没有, 还是需要手动设置请求体 以及状态码和跳转的url 比如上面 HttpResponse::TemporaryRedirect()和 HttpResponse::PermanentRedirect() 都返回了 ResponseBuild
// .service(web::resource("into_baidu")
// .route(web::get().to()))
// Requests 内容解码 如果请求头中包含一个 Content-Encoding 头, 请求的payload会根据header中的值来解压缩. 不支持多重解码器:Content-Encoding:br, gzip
// 支持的解码器: Brotli, Chunked, Compress, Gzip, Deflate, Identity, Trailers, EncodingExt
// Response 响应解码
// Actix-web 能使用 Compress middleware中间件, 默认 ContentEncoding::Auto被启用 根据请求头中的 Accept-Encoding 来决定压缩 payloads.
// 可以手动指定想要压缩的协议 支持: Brotli, gzip, Deflate, Identity
// 在视图中,可以对单独的函数进行 指定压缩方式: ContentEncoding::Identity 可用来禁用压缩
// .wrap(middleware::Compress::default()) // 中间件 开启默认压缩 (ContentEncoding::Auto)
.wrap(middleware::Compress::new(actix_web::http::ContentEncoding::Br)) // 中间件 添加一个 br 压缩中间件
.service(web::resource("zip")
.route(web::get().to(||{
let s = (0..4096).map(|x|"a").collect::<String>();
HttpResponse::Ok()
// .encoding(actix_web::http::ContentEncoding::Gzip) // 指定压缩方式
.encoding(actix_web::http::ContentEncoding::Identity) // 禁用压缩
.body(s)
})))
// 文件服务
.service(Files::new("/images", "./static/images/") // 渲染一个文件列表(注意此service 的注册顺序需要在 / 的上面, 不然会从绑定的根资源路径去找)
.show_files_listing())
.service(Files::new("/", "./static/root/") // 绑定 根路径, 并且绑定一个访问 根资源的 index 文件
.index_file("index.html"))
// 重写 NotFound 404
.default_service(
web::route().to(||HttpResponse::NotFound().body("内容不见了哦"))
)
})
.bind("127.0.0.1:8080")?
.workers(20) // 启动 20 个 worker线程
// .keep_alive(None) // 全局关闭 keep-alive, 传递 Option<usize,None> 或者 usize 都是可以的因为实现了 impl From<usize> for KeepAlive 和 impl From<Option<usize>> for KeepAlive
.keep_alive(70) // 全局开启keep-alive, keep alive 在 HTTP/1.0是默认 关闭 的, 在 HTTP/1.1 和 HTTP/2.0 是默认开启的
// 在视图中 链接类型可以使用 HttpResponseBuilder::connection_type() 方法来改变: HttpResponse::Ok().connection_type(http::ConnectionType::Close).force_close().finish()
// .disable_signals() // 禁用信号处理
.shutdown_timeout(30) // 在接收到停机信号后,worker线程有一定的时间来完成请求. 超过时间后的所有worker都会被强制drop掉. 默认的shutdown 超时时间设置为30秒
// 常用信号
// SIGINT - 强制关闭worker kill -2
// SIGTERM - 优雅关闭worker kill -15
// SIGQUIT - 强制关闭worker kill -3
.run()
.await
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::dev::Service;
use actix_web::{http, test, web, App, Error};
#[actix_rt::test]
async fn test_index() -> Result<(), Error> {
let app = App::new().route("/", web::get().to(index));
let mut app = test::init_service(app).await;
let req = test::TestRequest::get().uri("/").to_request();
let resp = app.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let response_body = match resp.response().body().as_ref() {
Some(actix_web::body::Body::Bytes(bytes)) => bytes,
_ => panic!("Response error"),
};
assert_eq!(response_body, r##"Hello world!"##);
Ok(())
}
}
Cargo.toml¶
[package]
name = "hello-world"
version = "2.0.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
edition = "2018"
[dependencies]
actix-web = "3"
env_logger = "0.7"
serde = "1.0.116" # 序列化用的
serde_json = "1.0"
cookie = "0.14.2" # cookie先关
futures = "0.3.5"
actix-files = "0.3.0" # actix 文件相关
derive_more = "0.99.10"
[dev-dependencies]
actix-rt = "1"
[features]
#secure-cookies = ["actix-http/secure-cookies"]