文章目录
  1. 1. 创建Rust项目
  2. 2. 定义Cli命令行参数
  3. 3. 主函数main的定义
  4. 4. 启动服务器线程serve()
  5. 5. 查看执行结果

通过分析Zola命令zola build的实现代码, 了解到使用hypertokio实现静态服务器的大概逻辑。Zola的开发服务只能使用在zola项目中, 执行流程中会组成会触发Zola的构建过程与文件变更监听。 这里将抽取相关的代码,生成的一个简单的文件服务器,可以在平常开发中的静态文件服务器来使用, 类似于python中的python http.server或者nodejs中的serve命令。

主要有如下特点:

  • 可以在任意有访问权限的目录下执行,请求路径对应到当前目录下的文件,返回文件内容
  • 通过命令行参数设定绑定网络地址与端口
  • 无配置文件

创建Rust项目

通过Cargo命令创建新项目,项目名称为static-serv-rs

1
2
cargo new static-serv-rs --bin
cd static-serv-rs

编辑Cargo.toml添加claphypertokio 等包的依赖,版本与Zola的相同。ctrlc是用来处理CTRL+C终止进程,open用于打开浏览器,anyhow声明异常类型,percent-encoding用于处理路径中的百分号,即路径转义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[dependencies]
clap = { version = "4", features = ["derive"] }
# 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"] }
ctrlc = "3"
open = "5"
# 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"

anyhow = "1.0.56"
percent-encoding = "2"

定义Cli命令行参数

命令行参数处理使用库clap。 新增文件src/cli.rs,添加结构体Cli来定义命令行支持的参数。#[derive(Parser)]注解用于自动生成parse(),在main中就可以使用let cli = Cli::parse();来解析命令行了。#[clap(version, author, about)] 用来添加--version,--author, --about参数开关。

struct Cli的每一个字段添加上注解#[clap(short = '..', long)], 会生成相应的参数。 使用parse()解析之后,就可以直接使用字段如cli.root来获取参数值了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::path::PathBuf;
use clap::Parser;

#[derive(Parser)]
#[clap(version, author, about)]
pub struct Cli {
/// Directory to use as root of project
#[clap(short = 'r', long, default_value = ".")]
pub root: PathBuf,

/// Interface to bind on
#[clap(short = 'i', long, default_value = "127.0.0.1")]
pub interface: String,

/// Which port to use
#[clap(short = 'p', long, default_value_t = 1111)]
pub port: u16,

/// Open site in the default browser
#[clap(short = 'O', long)]
pub open: bool,
}

主函数main的定义

修改src/main.rs的内容,其中main()的内容如下。Cli::parse()解析命令行参数后,先检查执行目录与端口是否可用,出错时调用std::process::exit(1);退出;最后调用serve()启动服务线程。

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
fn main() {

// 解析命令行参数
let cli = Cli::parse();

// 检查主目录
let root_dir: PathBuf = cli.root.canonicalize().unwrap_or_else(|e| {
unravel_errors(
&format!("Could not find canonical path of root dir: {}", cli.root.display()),
&e.into(),
);
std::process::exit(1);
});

// 检查端口是否可用
let mut port = cli.port;
if port != 1111 && !port_is_available(port) {
println!("Error: {}", "The requested port is not available");
std::process::exit(1);
}

if !port_is_available(port) {
port = get_available_port(1111).unwrap_or_else(|| {
println!("Error: {}", "No port available");
std::process::exit(1);
});
}

// 启动服务线程
serve(&root_dir, &cli.interface, port, cli.open).unwrap();
}

启动服务器线程serve()

serve() 方法相对于zola源代码要简单很多。 thread::spawn()新起一个线程,在新线程中创建hyper服务器,使用到了tokio异步框架来执行。

创建过程只有一行let server = Server::bind(&addr).serve(make_service);。服务器server创建完之后, 根据参数--open判断是否打开浏览器,打开时调用open::that('http://127.0.0.1:1111')。最后再调用server.await.expect(..), 等待服务器执行结束后退出。

ctrlc::set_handler()用来设置Ctrl+C进程中断操作的处理函数,这里是直接退出进程,不需要其它资源清理逻辑。serve()的倒数第2条语句是调用线程的join函数,thread_handle.join().unwrap();,不加这句的话,进程就直接退出了,因为处理请求的主逻辑在thread::spawn的新线程中,主线程执行完就结束了。

请求处理函数在handle_request()中,根据请求路径读取文件内容后作为响应返回,与zola的基本相同,代码也比较长,这里就不列出了。另外还需要有个404的处理函数。

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
fn serve(root_dir: &Path,
interface: &str,
interface_port: u16,
open: bool
) -> Result<()> {
let address = format!("{}:{}", interface, interface_port);
// Stop right there if we can't bind to the address
let bind_address: SocketAddrV4 = match address.parse() {
Ok(a) => a,
Err(_) => return Err(anyhow!("Invalid address: {}.", address)),
};
//检查端口是否可用
if (TcpListener::bind(bind_address)).is_err() {
return Err(anyhow!("Cannot start server on address {}.", address));
}

let static_root = root_dir.to_path_buf().clone();
let address_print = address.clone();

let thread_handle = 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| {
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");
});
});

println!(
"Listening {}, root {}",
address_print,
root_dir.display()
);

println!("Press Ctrl+C to stop\n");

// 处理CTRL+C
ctrlc::set_handler(move || {
::std::process::exit(0);
})
.expect("Error setting Ctrl-C handler");

thread_handle.join().unwrap();
Ok(())
}

async fn handle_request(req: Request<Body>, mut root: PathBuf) -> Result<Response<Body>> {
...
}

查看执行结果

代码完成后, 就可以执行cargo run编译代码,查看执行结果了。

1
2
3
4
5
6
> cargo run
...
Listening 127.0.0.1:1111, root \\?\C:\workspace\rust\static-serv-rs
Press Ctrl+C to stop

Web server is available at http://127.0.0.1:1111

static-serve-rs-cmdline.png

打开浏览器查看效果,访问http://localhost:1111/时返回404, 因为当前项目目录下无index.html文件。
static-serve-rs-view-1.png

当访问http://localhost:1111/Cargo.toml时,返回Cargo.toml文件的内容。
static-serve-rs-view-2.png

参考:

文章目录
  1. 1. 创建Rust项目
  2. 2. 定义Cli命令行参数
  3. 3. 主函数main的定义
  4. 4. 启动服务器线程serve()
  5. 5. 查看执行结果