作者:禅与计算机程序设计艺术
1.简介
1994年,互联网泡沫破裂,一批优秀的程序员、工程师纷纷加入到web开发领域。而其中的Rust语言却备受瞩目,它是一种现代系统编程语言,专注于安全和并发。因此,Rust在当下成为最流行的编程语言之一,很多框架也开始使用Rust重构,这使得Rust得到了越来越多人的青睐。
2017年1月,Google发布了它的Serverless计算产品,旨在实现按需自动扩容的能力,主要由FaaS(Functions as a Service)实现。为了达成此目标,需要构建一个高性能、易扩展、可伸缩的HTTP服务器。因此,在这一背景下,Rust语言又一次变得值得学习。
本文首先会带领读者了解并发Web服务器的概念、特性及其应用场景。然后,通过学习常用Rust库如Tokio、Hyper等,基于TCP/IP协议栈,实现了一个简单的并发Web服务器,并结合具体的代码讲解如何编写并发Web服务器的程序。本文将介绍如下知识点:
2. 并发Web服务器的概念、特性及其应用场景
2.1 概念和特性
Web服务器,通常指作为网络服务端的计算机软件,其主要职责就是接受客户端的请求,响应并返回相应的内容。传统的Web服务器是一个单进程、单线程的应用程序,也就是串行处理请求。随着服务器压力的增加,这种单进程、单线程的方式无法满足需求,出现了多进程、多线程的多进程模型。然而,这种多进程、多线程模型同样存在资源竞争的问题,无法有效利用多核CPU资源。另一方面,对于每个客户端的请求都需要进行新的进程或线程的创建和销毁,导致服务器的系统开销大增。
并发Web服务器的提出,就是为了解决传统Web服务器的效率低下和资源利用率低下的问题。并发Web服务器可以同时处理多个请求,每个请求都运行在不同的线程中,从而充分利用多核CPU资源,提升服务器的吞吐量。另外,还可以通过异步IO模型来优化服务器的性能,减少请求等待的时间。
2.2 应用场景
并发Web服务器应用场景广泛,以下列举一些典型应用场景:
-
大规模并发访问: 大型网站、社交媒体网站、电商平台等,都是并发Web服务器的典型应用场景。由于访问量巨大,服务器应当能够同时响应大量的用户请求,以保证网站的正常访问。例如,京东、淘宝、网易新闻、微博等网站均使用了并发Web服务器。
-
高实时性: 在通信、金融等实时应用场景中,服务器必须能够快速响应,以保证业务数据的准确性和完整性。例如,秒杀等活动中,需要及时响应用户的请求,以保证交易的成功率。
-
低延迟: 在搜索引擎、直播、即时通讯等实时应用场景中,服务器的响应时间不能超过100ms,否则将影响用户体验。例如,YouTube、Facebook Messenger等视频网站均使用了并发Web服务器。
-
海量数据处理: 有些情况下,服务器需要处理海量的数据,这就要求服务器具有较强的处理能力。例如,搜索引擎需要处理海量的索引数据,电商平台需要处理海量的订单数据。
-
游戏服务器: 游戏服务器对实时的响应速度十分敏感,因此并发Web服务器更加适宜。例如,某著名的手游公司的游戏服务器就采用了并发Web服务器。
-
其他领域: 在传统服务器架构不能满足需求的情况下,可以使用并发Web服务器,如物联网、区块链等领域。
3. 技术选型
为了实现一个高性能、易扩展、可伸缩的HTTP服务器,本文选择了Rust语言和Tokio异步运行时。Tokio提供高度抽象的异步I/O接口,可用于构建高性能的I/O密集型应用程序。
4. 项目计划
4.1 HTTP请求解析器
HTTP请求的结构非常复杂,而且请求头部还有可能被分割成多段。为了方便后续处理,需要先对HTTP请求进行解析,提取必要的信息,如请求方法、路径、请求参数等。因此,我们会实现一个HTTP请求解析器。
请求解析器原理
请求解析器可以根据HTTP协议规范对HTTP请求进行解析,包括请求行、请求头部和请求正文。解析流程如下:
(1)接收客户端请求报文;
(2)解析请求行,获取请求方法、URL等信息;
(3)解析请求头部,获取请求头部字段及对应的值;
(4)判断是否有请求正文,如果有,则读取并保存到内存;
(5)构造请求对象,保存相关信息;
(6)返回请求对象。
请求解析器实现
use std::collections::HashMap;
use httparse::{
Request, parse_request};
use bytes::BytesMut;
#[derive(Debug)]
pub struct HttpRequest {
method: String, // 请求方法 GET / POST...
path: String, // 请求路径 /index.html?key=value&...
headers: HashMap<String, String>, // 请求头
body: Option<Vec<u8>>, // 请求正文
}
impl HttpRequest {
/// 从 TCP Socket 中读取 HTTP 请求
async fn read_from_socket(&mut self, mut reader: &mut dyn AsyncRead) -> Result<(), io::Error> {
let mut buf = BytesMut::with_capacity(2 * 1024);
loop {
let n = reader.read_buf(&mut buf).await?;
if n == 0 && buf.is_empty() {
break;
}
match parse_request(&buf[..]) {
Ok((nparsed, req)) => {
println!("Parsed request:
{:#?}", req);
self.parse(req)?;
return Ok(());
},
Err(_) => continue,
}
// 如果请求还没有结束,则继续读取剩余部分
buf.split_to(nparsed);
}
// 如果没有完整的请求,则认为客户端断开连接
Err(io::ErrorKind::ConnectionReset.into())
}
fn parse(&mut self, req: Request) -> Result<(), ()> {
// 解析请求行
if req.method.len() > 0 {
self.method = unsafe {
String::from_utf8_unchecked(req.method.to_vec()) };
} else {
return Err(());
}
if req.path.len() > 0 {
self.path = unsafe {
String::from_utf8_unchecked(req.path.to_vec()) };
} else {
return Err(());
}
// 解析请求头
for header in req.headers.iter() {
let key = unsafe {
String::from_utf8_unchecked(header.name.to_vec()) };
let value = unsafe {
String::from_utf8_unchecked(header.value.to_vec()) };
self.headers.insert(key, value);
}
// 判断是否有请求正文
if let Some(body) = req.body {
self.body = Some(body.to_vec());
}
Ok(())
}
}
4.2 浏览器缓存机制
当浏览器向服务器发送HTTP请求时,它可以设置Cache-Control、If-Modified-Since等HTTP头字段控制缓存行为。其中,Cache-Control头字段指定请求/响应遵循的缓存规则,如public、private、max-age等;If-Modified-Since头字段表示客户机只想比指定的日期更新的缓存,服务器端在收到该请求时,检查文件是否有变化,有变化的话才返回新的响应。
通过Cache-Control头字段,可以在服务器端实现以下缓存策略:
(1)public:可以被所有中间件缓存;
(2)private:不允许被共享缓存(比如CDN缓存);
(3)no-cache:每次需要去源站校验资源的有效性;
(4)max-age:缓存的最大有效期,单位为秒;
(5)no-store:所有内容都不会被缓存。
目前,缓存管理模块还处于初级阶段,只支持部分功能。
#[derive(Debug, Clone)]
enum CacheType {
Public, // 可以被所有中间件缓存
Private, // 不允许被共享缓存(比如CDN缓存)
NoCache, // 每次需要去源站校验资源的有效性
MaxAge(i32),// 缓存的最大有效期,单位为秒
NoStore, // 所有内容都不会被缓存
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
cache_type: CacheType,
max_stale: i32,
min_fresh: i32,
no_transform: bool,
}
#[derive(Debug)]
pub struct CachedResponse {
status_code: u16,
version: String, // "HTTP/1.1" 或 "HTTP/2.0"
headers: Vec<(String, String)>, // 响应头
content: Vec<u8>, // 响应正文
}
#[derive(Debug)]
pub struct HttpCacheManager {
}
impl HttpCacheManager {
pub fn new() -> Self {
Self {
}
}
pub fn is_cached(&self, config: &CacheConfig, response: &CachedResponse) -> bool {
true // 此处添加判断逻辑
}
pub fn save_response(&self, config: &CacheConfig, response: &mut HttpResponse) -> Result<bool, ()> {
false // 此处添加存储逻辑
}
}
4.3 文件处理
在HTTP服务器中,一般都会涉及文件的上传下载、缓存查找、静态资源托管等。因此,需要有一个文件处理模块,负责处理HTTP请求中的文件。
文件处理模块需要具备以下功能:
- 支持Range请求,可以实现断点续传;
- 实现基本的文件权限验证和目录列表显示;
- 支持压缩传输;
- 支持虚拟主机,可以根据域名来决定服务哪些目录;
文件处理原理
文件处理模块根据HTTP请求中的URI,定位对应的文件并读取内容。下面给出文件的读取过程:
- 根据URI定位对应的文件,可以考虑采用虚拟目录(Virtual Directory)的方式,把目录映射到本地磁盘上的特定位置。虚拟目录的配置文件通常保存在服务器上,所以修改起来比较简单。
- 检查文件权限,如果文件不可读或者不存在,则返回错误页面;
- 对GET方式的请求,读取文件内容并组装响应消息;
- 对HEAD方式的请求,只需要复制响应消息头即可,不需要实际读取文件内容;
- 对POST方式的请求,写入文件的内容,或者执行文件上传操作;
- 对PUT方式的请求,创建一个新的文件,写入新内容,或者执行文件上传操作;
- 对DELETE方式的请求,删除文件。
文件处理实现
use std::collections::HashMap;
use tokio::fs::File;
use mime_guess::MimeGuess;
use hyper::{
Body, Response, StatusCode};
#[derive(Debug, Clone)]
pub struct FileContext {
base_dir: PathBuf, // 服务根目录
virtual_dirs: HashMap<String, PathBuf>, // 虚拟目录配置
index_files: Vec<String>, // 默认索引文件
}
impl Default for FileContext {
fn default() -> Self {
Self {
base_dir: "/var/www".into(),
virtual_dirs: HashMap::new(),
index_files: vec!["index.html", "default.htm"],
}
}
}
impl FileContext {
/// 设置服务根目录
pub fn set_base_dir(&mut self, dir: &str) -> Result<(), std::io::Error> {
self.base_dir = PathBuf::from(dir);
Ok(())
}
/// 添加虚拟目录
pub fn add_virtual_dir(&mut self, name: &str, dir: &str) -> Result<(), std::io::Error> {
self.virtual_dirs.insert(name.into(), PathBuf::from(dir));
Ok(())
}
/// 删除虚拟目录
pub fn remove_virtual_dir(&mut self, name: &str) -> bool {
self.virtual_dirs.remove(name).is_some()
}
/// 获取文件内容
pub async fn get_file_content(&self, uri: &str) -> Result<Option<Vec<u8>>, std::io::Error> {
let file_path = self.get_file_path(uri)?;
if!file_path.exists() ||!file_path.is_file() {
return Ok(None);
}
let f = File::open(file_path).await?;
Ok(Some(f.bytes().await?.collect()))
}
/// 获取文件路径
fn get_file_path(&self, uri: &str) -> Result<PathBuf, std::io::Error> {
let (virtual_dir, real_path) = self.resolve_virtual_dir(uri)?;
let mut p = self.base_dir.clone();
if let Some(vd) = virtual_dir {
p.push(vd);
}
p.push(real_path);
Ok(p)
}
/// 解析虚拟目录和真实路径
fn resolve_virtual_dir(&self, uri: &str) -> Result<(Option<&str>, &str), std::io::Error> {
let parts: Vec<_> = uri.trim_start_matches('/').split('/').collect();
if parts.len() < 2 {
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Not Found"));
}
let vd = parts[0];
let vp = parts[1..].join("/");
if let Some(vdp) = self.virtual_dirs.get(vd) {
return Ok((Some(&*vdp.display()), &vp));
}
Ok((None, &vp))
}
}
trait MimeExt {
fn to_mime_string(&self) -> String;
}
impl MimeExt for MimeGuess {
fn to_mime_string(&self) -> String {
format!("{}", self.first_raw())
}
}
impl FileContext {
/// 返回默认索引文件内容
pub async fn get_default_page(&self) -> Option<Vec<u8>> {
for filename in &self.index_files {
if let Some(data) = self.get_file_content(filename).await? {
return Some(data);
}
}
None
}
/// 返回HTTP响应
pub async fn create_response(&self, uri: &str) -> Option<Response<Body>> {
if let Some(data) = self.get_file_content(uri).await? {
return Some(create_http_response(&uri, data)?);
}
None
}
}
fn create_http_response(uri: &str, data: Vec<u8>) -> Option<Response<Body>> {
let mut resp = Response::builder();
// 设置响应状态码和版本
let code = if uri.ends_with("/") {
StatusCode::OK
} else {
StatusCode::NOT_FOUND
};
resp.status(code);
resp.version("HTTP/1.1");
// 设置响应头
let ext = uri.rfind('.').unwrap_or(0);
let ct = if ext >= 0 {
let mt = mime:: guess_mime_type(&uri[(ext + 1)..]);
format!("{}; charset={}", mt, encoding_for_mime_type(mt))
} else {
DEFAULT_MIME_TYPE.into()
};
resp.header("Content-Type", ct.as_str());
resp.header("Content-Length", data.len());
resp.header("Last-Modified", Utc::now().to_rfc2822());
resp.header("Accept-Ranges", "bytes");
resp.body(data.into()).ok()
}
const DEFAULT_MIME_TYPE: &str = "application/octet-stream";
fn encoding_for_mime_type(ct: &str) -> &'static str {
match ct {
_ if ct.starts_with("text/") => "UTF-8",
"image/" | "video/" | "audio/" => "binary",
_ => "",
}
}