文章目录
  1. 1. Rust网络库hyper
  2. 2. Rust异步框架Tokio
  3. 3. Zola cmd::serve() 命令实现

zola serve是静态站点生成工具Zola的命令,负责构建及启动本地访问的服务器。命令执行时会先构建项目,然后启动一个web服务器监听网络请求;同时启动一个新线程监听文件,当有文件内容发生变化时触发构建项目并刷新页面。

在前一篇文章中梳理了下Zola项目的大概结构,可知zola serve命令是由cmd::serve()来进行处理的,相关的文件是src/main.rssrc/cmd/serve.rs。 这里将学习下zola创建本地服务器的流程,及网络库hyper,tokio的使用。

Cargo.toml中依赖的web网络服务相关的库如下,除了hyper,tokio外,还有处理mime内容类型的mime, mime_guess,以及用于监听文件变更notify和websocket ws

1
2
3
4
5
6
7
8
9
10
11
12
13
# Below is for the serve cmd
hyper = { version = "0.14.1", default-features = false, features = ["runtime", "server", "http2", "http1"] }
tokio = { version = "1.0.1", default-features = false, features = ["rt", "fs", "time"] }
time = { version = "0.3", features = ["formatting", "macros", "local-offset"] }
notify = "4"
ws = "0.9"
ctrlc = "3"
open = "5"
pathdiff = "0.2"
# For mimetype detection in serve mode
mime_guess = "2.0"
# For essence_str() function, see https://github.com/getzola/zola/issues/1845
mime = "0.3.16"

Rust网络库hyper

hyper是Rust实现的http协议的库,可用于构建客户端与服务器端程序。hyper处理请求、响应的解析, 请求处理方法Service及服务器Server的抽象与构建,而接收网络请求则要依赖库tokio。hyper是一个比较底层的库,并不是开箱即用,需要使用比较方面的客户端可以使用reqwest https://crates.io/crates/reqwest。

下面来看一个简单的Hyper实现的http服务器,每个请求都返回固定的值。来自于hyper文档。

首先是在Cargo.toml中添加依赖的crate

1
2
3
4
5
[dependencies]
hyper = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["full"] }
http-body-util = "0.1"
hyper-util = { version = "0.1", features = ["full"] }

在Rust文件头部引入hyper与tokio的类型

1
2
3
4
5
6
7
8
9
10
use std::convert::Infallible;
use std::net::SocketAddr;

use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;

hello()是请求处理方法,接收Request类型的参数,返回Response,响应总是固定的值Hello, World!。 注意方法名的关键字async,指明是一个异步执行的方法, 执行时返回的是Future。

1
2
3
async fn hello(_: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
Ok(Response::new(Full::new(Bytes::from("Hello, World!"))))
}

最后一部分就是创建服务器,监听网络端口,处理http请求。main()方法前也使用了关键字async,而前一行的#[tokio::main]指定使用tokio异步函数运行时。
主函数中先使用tokio::net::TcpListener绑定到127.0.0.1:3000, 然后循环调用listener.accept()接收请求,将接收的连接在tokio::task::spawn新起一个异步协程进行处理。使用service_fn将处理函数hello转换为一个Service后,再用http1的`serve_connection`将接收的连接关联起来,从而实现对请求的处理。

TcpListener::bind(addr).await?中使用了.await操作,等待异步方法执行结束。这里用到了Rust的async/await异步操作,与ES6/Typescript中的异步实现并不一样,Rust中未调用await之前协程是不会执行的。语句末尾的?,是用于自动检查Result的错误,相当于简化unwrap()操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

// We create a TcpListener and bind it to 127.0.0.1:3000
let listener = TcpListener::bind(addr).await?;

// We start a loop to continuously accept incoming connections
loop {
let (stream, _) = listener.accept().await?;

// Use an adapter to access something implementing `tokio::io` traits as if they implement
// `hyper::rt` IO traits.
let io = TokioIo::new(stream);

// Spawn a tokio task to serve multiple connections concurrently
tokio::task::spawn(async move {
// Finally, we bind the incoming connection to our `hello` service
if let Err(err) = http1::Builder::new()
// `service_fn` converts our function in a `Service`
.serve_connection(io, service_fn(hello))
.await
{
println!("Error serving connection: {:?}", err);
}
});
}
}

参考:

Rust异步框架Tokio

tokio是一个异步运行时框架,支持async/await语法,用于异步任务的创建、执行,同时也提供了网络、IO的异步处理方法。此时我想起了Java中的Netty框架。
如是来自tokio文档的例子,展示async/await的用法。 使用async fn声明的函数,调用只是返回一个Future, 在调用.await操作后才开始真正执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async fn say_world() {
println!("world");
}

#[tokio::main]
async fn main() {
// Calling `say_world()` does not execute the body of `say_world()`.
let op = say_world();

// This println! comes first
println!("hello");

// Calling `.await` on `op` starts executing `say_world`.
op.await;
}

输出:

1
2
hello
world

async fn是进入一个异常执行的上下文,异步函数的执行需要一个运行时runtime,需要有函数来调用。添加的注解#[tokio::main]是一个宏,用于将异步的async fn main转成一个同步的fn main,初始化运行时runtime后再执行异步函数。

1
2
3
4
#[tokio::main]
async fn main() {
println!("hello");
}

执行时将转换成如下的形式。

1
2
3
4
5
6
fn main() {
let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
println!("hello");
})
}

参考:

Zola cmd::serve() 命令实现

回到zola的源码中,只关注zola build中服务器的创建过程,源文件为src/cmd/serve.rs

请求处理函数为async fn handle_request(), 对接收的请求先检查参数是否合法,req.uri().path()获取请求路径,req.method()获取请求方法,如果构建内容存在于内存时就使用内存的内容返回; 然后根据请求路径拼接出本地文件的全路径,文件存在时则返回文件内容。 读取文件内容使用tokio::fs::read(&root).await,也是一个异步方法,来自于tokio库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
async fn handle_request(req: Request<Body>, mut root: PathBuf) -> Result<Response<Body>> {
let original_root = root.clone();
let mut path = RelativePathBuf::new();
// https://zola.discourse.group/t/percent-encoding-for-slugs/736
let decoded = match percent_encoding::percent_decode_str(req.uri().path()).decode_utf8() {
Ok(d) => d,
Err(_) => return Ok(not_found()),
};

for c in decoded.split('/') {
path.push(c);
}
...
// 动态生成的内容在内存中可以找到时,就返回内存中的内容
if let Some(content) = SITE_CONTENT.read().unwrap().get(&path) {
return Ok(in_memory_content(&path, content));
}

// 只支`GET`/`HEAD`请求
match *req.method() {
Method::HEAD | Method::GET => {}
_ => return Ok(method_not_allowed()),
}

// Handle only simple path requests
if req.uri().scheme_str().is_some() || req.uri().host().is_some() {
return Ok(not_found());
}

// 去掉decoded第一个斜杠/,否则`PathBuf`会被当作绝对路径处理
root.push(&decoded[1..]);

// Resolve the root + user supplied path into the absolute path
// this should hopefully remove any path traversals
// if we fail to resolve path, we should return 404
root = match tokio::fs::canonicalize(&root).await {
Ok(d) => d,
Err(_) => return Ok(not_found()),
};

// Ensure we are only looking for things in our public folder
if !root.starts_with(original_root) {
return Ok(not_found());
}

let metadata = match tokio::fs::metadata(root.as_path()).await {
Err(err) => return Ok(io_error(err)),
Ok(metadata) => metadata,
};
if metadata.is_dir() {
// 如果root是路径,则添加index.html作为将读取的文件
root.push("index.html");
};

   // 读取文件内容
let result = tokio::fs::read(&root).await;

let contents = match result {
Err(err) => return Ok(io_error(err)),
Ok(contents) => contents,
};

// 返回文件路径
Ok(Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
mimetype_from_path(&root).first_or_octet_stream().essence_str(),
)
.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(Body::from(contents))
.unwrap())
}

还有一部分即是server的创建与启动,在方法serve()中, 使用标准库std::thread新起线程来创建服务器,hyper::server::Server绑定网络端口,请求处理方法即前面的async fn handle_request()。这里显示的创建tokio::runtime异步执行运行时,然后执行rt.block_on(async {})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
use std::thread;
use hyper::server::Server;
use hyper::service::{make_service_fn, service_fn};

pub fn serve(
root_dir: &Path,
interface: &str,
interface_port: u16,
output_dir: Option<&Path>,
force: bool,
base_url: &str,
config_file: &Path,
open: bool,
include_drafts: bool,
fast_rebuild: bool,
no_port_append: bool,
utc_offset: UtcOffset,
) -> Result<()> {
...
let broadcaster = {
thread::spawn(move || {
let addr = address.parse().unwrap();

let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Could not build tokio runtime");

rt.block_on(async {
let make_service = make_service_fn(move |_| {
let static_root = static_root.clone();

async {
Ok::<_, hyper::Error>(service_fn(move |req| {
response_error_injector(handle_request(req, static_root.clone()))
}))
}
});

let server = Server::bind(&addr).serve(make_service);

println!("Web server is available at http://{}\n", &address);
if open {
if let Err(err) = open::that(format!("http://{}", &address)) {
eprintln!("Failed to open URL in your browser: {}", err);
}
}

server.await.expect("Could not start web server");
});
});
...
}
}

参考:

文章目录
  1. 1. Rust网络库hyper
  2. 2. Rust异步框架Tokio
  3. 3. Zola cmd::serve() 命令实现