HTTP 服务器
server.md
commit - 4d8d53cea59bca095ca5c02ef81f0b1791736855 - 2020.09.12
HttpServer 类型负责为 HTTP 请求提供服务。
HttpServer
接受应用程序工厂作为参数,并且应用程序工厂必须具有 Send
+ Sync
约束。在“多线程”一节有更多信息。
- 要绑定到特定的套接字(socket)地址,必须使用
bind()
方法,并且可以多次调用它。 - 要绑定 ssl 套接字(socket),应使用
bind_openssl()
方法或者bind_rustls()
方法。 - 要运行 HTTP 服务器,请使用
HttpServer::run()
方法。
use actix_web::{web, App, HttpResponse, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new().route("/", web::get().to(|| HttpResponse::Ok()))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
run()
方法返回 Server
类型的实例,Server
类型的方法可用于管理 HTTP 服务器:
pause()
- 暂停接受传入的连接resume()
- 重新开始接受传入的连接stop()
- 停止处理传入的连接,停止所有工作线程,并退出
下面的示例展示了如何在独立的线程中启动 HTTP 服务器。
use actix_web::{web, App, HttpResponse, HttpServer, rt::System};
use std::sync::mpsc;
use std::thread;
#[actix_web::main]
async fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let sys = System::new("http-server");
let srv = HttpServer::new(|| {
App::new().route("/", web::get().to(|| HttpResponse::Ok()))
})
.bind("127.0.0.1:8080")?
.shutdown_timeout(60) // <- Set shutdown timeout to 60 seconds
.run();
let _ = tx.send(srv);
sys.run()
});
let srv = rx.recv().unwrap();
// 暂停接受传入的连接
srv.pause().await;
// 重新开始接受传入的连接
srv.resume().await;
// 停止服务器
srv.stop(true).await;
}
多线程
HttpServer
会自动启动多个 HTTP 工作线程(worker),默认情况下,线程的数量等于系统中逻辑 CPU 的数量。线程的数量可以用 HttpServer::workers()
方法重写。
use actix_web::{web, App, HttpResponse, HttpServer};
#[actix_web::main]
async fn main() {
HttpServer::new(|| {
App::new().route("/", web::get().to(|| HttpResponse::Ok()))
})
.workers(4); // <- Start 4 workers
}
一旦创建了线程(worker),每个线程(worker)都会收到一个独立的 App(应用程序)实例,每个 App 实例都可以处理请求。应用程序状态(state)在线程之间不能共享,但 handler
可以自由地操作其状态副本,而无需担心并发性问题。
应用程序状态(state)不需要具有
Send
或者Sync
约束,但应用程序工厂必须具有Send
+Sync
约束。
要在工作线程之间共享状态,要使用 Arc(原子引用计数器)
。一旦引入共享和同步,应格外小心。在许多情况下,由于锁定共享状态以对其进行修改,会无意中引入性能成本。
在某些情况下,使用更有效的锁定策略可以降低这些性能成本。例如,使用读/写锁而不是互斥器(mutexes)来实现非互斥锁,但最具性能的实现,往往是根本不发生锁定的实现。
由于每个工作线程都是按顺序处理其请求的,因此当 handler
阻塞当前线程时,将导致当前工作线程停止处理新请求:
fn my_handler() -> impl Responder {
std::thread::sleep(Duration::from_secs(5)); // <-- Bad practice! Will cause the current worker thread to hang!
"response"
}
因上述原因,任何长时间的、非 cpu 限制的操作(例如,I/O、数据库操作等)都应该使用 future
函数或异步函数。异步 handler
由工作线程并发执行,因此不会发生阻塞:
async fn my_handler() -> impl Responder {
tokio::time::delay_for(Duration::from_secs(5)).await; // <-- Ok. Worker thread will handle other requests here
"response"
}
同样的限制也适用于提取器(extractor)。当 handler
函数接收到实现了 FromRequest
的参数,并且该实现阻塞当前线程时,工作线程也将在运行时发生阻塞。因此,在实现提取器(extractor)时必须特别注意,而且在需要时也应该异步地实现它们。
SSL
ssl 服务器有两个实现:rustls
和 openssl
。rustls
集成在 Rust 程序设计语言新开发的 TLS 库 rustls
,openssl
用于开源的 TLS 业界标准库 openssl
。
[dependencies]
actix-web = { version = "{{< actix-version "actix-web" >}}", features = ["openssl"] }
openssl = { version="0.10" }
use actix_web::{get, App, HttpRequest, HttpServer, Responder};
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
#[get("/")]
async fn index(_req: HttpRequest) -> impl Responder {
"Welcome!"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// load ssl keys
// to create a self-signed temporary cert for testing:
// `openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
builder
.set_private_key_file("key.pem", SslFiletype::PEM)
.unwrap();
builder.set_certificate_chain_file("cert.pem").unwrap();
HttpServer::new(|| App::new().service(index))
.bind_openssl("127.0.0.1:8080", builder)?
.run()
.await
}
注意:HTTP/2.0 需要 tls alpn 协议支持。目前,仅有
openssl
支持alpn
协议。完整实例请查看 examples/openssl。
使用命令创建 key.pem 和 cert.pem,注意填写你自己的主题:
$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
-days 365 -sha256 -subj "/C=CN/ST=Fujian/L=Xiamen/O=TVlinux/OU=Org/CN=muro.lxd"
移除密码,然后将 nopass.pem 复制到 key.pem:
$ openssl rsa -in key.pem -out nopass.pem
Keep-Alive
actix 可以在 keep-alive 连接上等待请求。
keep-alive 连接行为由服务器设置定义:
75
、Some(75)
、KeepAlive::Timeout(75)
- 开启 keep-alive 保活时间为 75 秒。None
或者KeepAlive::Disabled
- 关闭 keep-alive。KeepAlive::Tcp(75)
- 使用SO_KEEPALIVE
套接字选项。
use actix_web::{web, App, HttpResponse, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let one = HttpServer::new(|| {
App::new().route("/", web::get().to(|| HttpResponse::Ok()))
})
.keep_alive(75); // <- Set keep-alive to 75 seconds
// let _two = HttpServer::new(|| {
// App::new().route("/", web::get().to(|| HttpResponse::Ok()))
// })
// .keep_alive(); // <- Use `SO_KEEPALIVE` socket option.
let _three = HttpServer::new(|| {
App::new().route("/", web::get().to(|| HttpResponse::Ok()))
})
.keep_alive(None); // <- Disable keep-alive
one.bind("127.0.0.1:8080")?.run().await
}
如果选择了上面的第一个选项,则根据响应的 connection-type 计算 keep alive 状态。默认情况下,HttpResponse::connection_type
是未定义的,keep-alive 状态由请求的 HTTP 版本定义。
对于 HTTP/1.0,keep-alive 默认关闭;对于 HTTP/1.1 和 HTTP/2.0,keep-alive 默认开启。
可以使用 HttpResponseBuilder::connection_type()
方法更改 connection-type。
use actix_web::{http, HttpRequest, HttpResponse};
async fn index(req: HttpRequest) -> HttpResponse {
HttpResponse::Ok()
.connection_type(http::ConnectionType::Close) // <- Close connection
.force_close() // <- Alternative method
.finish()
}
正常关闭
HttpServer
支持正常关闭。在收到停止信号后,工作线程分配一定的时间来完成服务请求。超时后,任何仍然存活的工作线程都将被强制丢弃。默认情况下,关机超时设置为 30 秒,可以使用 HttpServer::shutdown_timeout()
方法更改此参数。
你可以通过服务器地址向服务器发送停止消息,并指明是否要正常关闭。Server
类型有 start()
方法,可以返回服务器地址。
HttpServer
会处理多个操作系统信号。其中 CTRL-C 可用于所有操作系统,其它信号可用于 unix 系统。
- SIGINT - 强制关闭工作线程
- SIGTERM - 正常关闭工作线程
- SIGQUIT - 强制关闭工作线程
使用
HttpServer::disable_signals()
方法可以禁用信号处理。