一步一Web
本文尝试使用Rust构建一个简单的Hello world服务程序。
仅使用标准库的简易Web服务器
Rust的标准库提供了很多方便的基础组件:socket的操作、线程的操作、带缓存的读写操作等,这几个点,可以拿出来构建一个简易的Web程序。
从简易的每个链接启动一个线程的开始
在很久以前,在建立一个Web服务器,经常会看到这种的处理方式。每accept到一个socket,就新建一个线程来处理。这是一个简易的做法。
use std::net::{TcpListener, TcpStream};
fn main() {
let listener = TcpListener::bind("127.0.0.1:5000")?;
loop {
let (socket, remote_addr) = listener.accept()?;
std::thread::spawn(move || match handle_conn(socket, remote_addr) {
Ok(_) => {
println!("handle conn done");
}
Err(e) => {
println!("handle conn error: {:?}", e);
}
});
}
}
看一下HTTP协议
HTTP的简单说明可以看一下这个文档:MDN上的说明。
首先从请求(Request)的出发:
摘抄如下:
![ad688832939b24ef1e7ee014ba063469.png](https://i-blog.csdnimg.cn/blog_migrate/4d719db4ecc9816279a9236e4b0ce7be.png)
请求由以下元素组成: 一个HTTP的method,经常是由一个动词像GET, POST 或者一个名词像OPTIONS,HEAD来定义客户端的动作行为。通常客户端的操作都是获取资源(GET方法)或者发送HTML form表单值(POST方法),虽然在一些情况下也会有其他操作。 要获取的资源的路径,通常是上下文中就很明显的元素资源的URL,它没有protocol (http://),domain(http://developer.mozilla.org),或是TCP的port(HTTP一般在80端口)。 HTTP协议版本号。 为服务端表达其他信息的可选头部headers。 对于一些像POST这样的方法,报文的body就包含了发送的资源,这与响应报文的body类似。
基于这个简易说明,现在让我们添加HTTP的请求的处理吧。
#[derive(Debug, Clone, Default)]
struct Request {
method: String,
target: String,
version: String,
headers: Headers,
body: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
struct Headers {
inner: HashMap<String, Vec<String>>,
}
可以看到,我们是依照协议的定义来进行我们的结构体定义:method、target、version字段呈现第一行的请求信息;headers保存请求头的信息,这里是一个简易的的HashMap;body是一个可有可无的数据,在我们的实现中这里基本是忽略了它的。
带缓存的读请求
那么是要怎样从socket里面的读取信息来填充我们Request
呢?Rust提供了一个很好用的带缓存的的数据读写组件:BufReader和BufWriter。它们内部带了缓存,这样我们不用为每次read的中断和等待数据而烦恼,为我们的读写带来很多的便利。
BufReader::read_line
: 直接读出一行文本,这里在我们读取请求头时很方便。
BufReader::read_exact
: 读出所需要长度的数据。
利用read_line读取header:
fn read_from(reader: &mut BufReader<TcpStream>) -> Result<Headers> {
let mut headers: HashMap<String, Vec<String>> = HashMap::new();
loop {
let mut line = String::new();
// 按行读取
reader.read_line(&mut line)?;
let line = line.trim_end();
if line.is_empty() {
break;
}
// 以`:`切割
let parts: Vec<&str> = line.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(error_message("header line not ok"));
}
let key = parts[0].to_string();
let value = parts[1].trim_start().to_string();
// 添加到HashMap
headers
.entry(key)
.and_modify(|values| values.push(value.clone()))
.or_insert(vec![value]);
}
let headers = Headers { inner: headers };
Ok(headers)
}
注意:这里的读取一行是按n
分割,而不是按标准的rn
处理
可以看到,在这个读取HTTP的Header的函数中,整个流程很清晰,不断的读取一行,分割解析保存。 在BufReader
的内置缓存帮助下,这里很简单的就可以读取socket的内容了。
在典型的read操作,在Rust、C语言中,直接读socket,都是要自己维护buffer,而且需要处理读取被中断的情况:
#define E_OK 0
#define E_ERR -1
#define E_EOF -2
#define BUF_SIZE 8 * 1024
// 注意:返回的buf需要调用者手动free!
void* read_expect(int fd, size_t expect, size_t* buf_size, int* ret)
{
size_t total = 0;
size_t rest = expect;
void* buf = malloc(expect);
*ret = E_OK;
while (rest) {
size_t nread;
char* tmp = (char*)buf;
nread = read(fd, tmp + total, rest);
if (nread == -1 && errno != EINTR) {
*ret = E_ERR;
break;
}
if (nread == 0) {
*ret = E_EOF;
break;
}
total += nread;
rest -= nread;
}
*buf_size = total;
return buf;
}
可以说,BufReader
这一类的方法使整个流程的处理便利了很多。
返回响应
![016798e5e624a869aaf8653e0b945b01.png](https://i-blog.csdnimg.cn/blog_migrate/b67b2cec977947315d15b443b6ba39c1.jpeg)
同样的,根据HTTP协议定义我们的响应:
#[derive(Debug, Clone)]
struct Response {
version: String,
status_code: u16,
status_text: String,
headers: Headers,
body: Option<Vec<u8>>,
}
在我们简易实现中,一切都从简,不做读取Response的操作,只返回它。
fn write_to(&self, writer: &mut BufWriter<TcpStream>) -> Result<()> {
self.write_status_line(writer)?;
self.write_headers(writer)?;
self.write_connection_close(writer)?;
self.write_content_length(writer)?;
self.write_empty_line(writer)?;
self.write_body(writer)?;
Ok(())
}
同样地,依然是使用BufWrite
来进行写操作。
看一下最后的效果
完整的代码
git cloen https://github.com/zzzdong/one-step-one-web
cd one-step-one-web/stdthread-web
cargo run
curl -i http://127.0.0.1:5000/
或者打开浏览器看看效果
初见多路复用
在前面,我们使用了每个链接一个线程的方式,这样处理,在高并发的时候,会导致系统中出现多个线程,占用的系统资源高,影响性能。
在历史的发展进程,从C10K等问题中,应用多路复用的处理方式越来越多了,Linux(epoll)、FreeBSD(kqueue)等。
在Rust的领域,我们可以看到mio这个优秀的封装库。
简述事件驱动
事件驱动的实现一般会有以下的特征:
- 有一个东西提供对事件的监控,事件可能是:socket可读(readable)、可写(writeable)、有新链接进来。
- 当事件发生时,可以知道发生事件的来源(source)。
- 可以动态变更要监控的事件来源。
这里尝试用简单的语言描述下应用mio这些事件驱动的套路。 所以先建立一个链接监听指定端口,监听该socket的有新stream进入的事件,在新stream连接成功后,把这个stream也加入监听的队列中。 对于每个被监控的stream,我们关注stream的可读、可写事件:
- 当可读时候,读完stream中的数据并尝试解析读到的数据;在请求的数据已经完成被解析完成后,就可以处理请求的数据,做出相应的处理。
- 当可写时候,我们尝试把要返回给stream对端的数据发出去,直到发完为止。
废话少说,献上事件循环时的处理:
fn run_loop(&mut self) -> io::Result<()> {
let mut events = Events::with_capacity(EVENT_SIZE);
loop {
self.poller.poll(&mut events, None)?;
let mut to_removed = Vec::new();
let mut new_conns = Vec::new();
// 处理每一个事件
for event in &events {
// 根据事件token取出链接
let token = event.token();
let stream = { self.conns.get_mut(&token).unwrap() };
// 处理读事件
if event.is_readable() {
match stream.on_readable() {
Ok(State::Accepted(conn)) => {
for c in conn {
new_conns.push(c);
}
}
Ok(State::Done) => {
to_removed.push(token);
}
Ok(State::Running) => {}
Err(e) => {
println!("read stream error: {:?}", e);
to_removed.push(token);
}
}
}
// 处理写事件
if event.is_writable() {
match stream.on_writable() {
Ok(State::Done) => {
to_removed.push(token);
}
Err(e) => {
println!("write stream error: {:?}", e);
to_removed.push(token);
}
_ => {}
}
}
}
for token in to_removed {
self.conns.remove(&token);
}
for c in new_conns {
let mut conn = HttpConn::new(c);
let token = self.register(&mut conn, Interest::READABLE.add(Interest::WRITABLE))?;
self.conns.insert(token, Box::new(conn));
}
}
}
其实这里使用mio写起来要比直接用epoll来麻烦,因为epoll里面的epoll_event.data
可以直接保存结构体的指针,从而使拿到event的时候直接取出上下文,而mio事件时拿到的是token,要用token从HashMap中拿出对应的上下文。
读取请求数据
另外由于少了BufReader
这些的辅助,我们需要自己的缓存,读取的操作变得繁重起来。和前面的举例的read_exact
很像,需要不断的循环读。
fn on_readable(&mut self) -> io::Result<State> {
let mut len;
loop {
let old_len = self.r_buf.len();
len = old_len + BUF_SIZE;
self.r_buf.resize(len, 0x00);
match self.stream.read(&mut self.r_buf[old_len..]) {
Ok(nread) => len = old_len + nread,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
Err(e) => return Err(e),
};
}
self.r_buf.resize(len, 0);
match self.req_codec.decode(&mut self.r_buf)? {
Some(req) => {
println!("=> {:?}", req);
let mut resp = Response::ok();
resp.set_body("<h1>Hello, world</h1>");
let mut codec = ResponseCodec;
codec.encode(&resp, &mut self.w_buf)?;
self.flush()?;
self.set_finished();
}
None => return Ok(State::Running),
}
Ok(State::Running)
}
每一次可读的事件发生时,进入了on_readable的处理。我们只能使用自己内部缓存,每次尝试读取尽可能多的数据,当读取了数据之后,就去尝试解析请求的内容。当整个请求都接收完成之后,就可以做出响应,返回数据。
基于状态机解析数据
同样的,我们需要换一个方式来进行请求的解码。
fn parse_request(&mut self, buf: &mut BytesMut) -> io::Result<Option<()>> {
loop {
match self.state {
RequestState::RequestLine => match self.parse_request_line(buf)? {
Some(_offset) => self.state = RequestState::Headers,
None => return Ok(None),
},
RequestState::Headers => match self.headers.parse_headers(buf)? {
Some(_offset) => match self.headers.get(HEADER_CONTENT_LENGTH) {
Some(length) => {
let len = length
.last()
.ok_or_else(|| error_message("content-length error"))?;
let len = len
.parse::<usize>()
.map_err(|e| error_message("content-length error"))?;
self.state = RequestState::Body(BodyMode::Lengthed(len))
}
None => self.state = RequestState::Body(BodyMode::None),
},
None => return Ok(None),
},
RequestState::Body(ref mode) => match mode {
// TODO: add receive body
BodyMode::None => self.state = RequestState::Done,
BodyMode::Lengthed(len) => {
let len = *len;
// wait all body data
if buf.len() < len {
return Ok(None);
} else {
let body = buf[0..len].to_vec();
buf.advance(len);
self.body = Some(body);
self.state = RequestState::Done
}
}
BodyMode::Chunked => unimplemented!(),
},
RequestState::Done => {}
};
if self.state == RequestState::Done {
return Ok(Some(()));
}
}
}
这里是由状态来控制整个流程:
RequestState::RequestLine -> RequestState::Headers -> RequestState::Body -> RequestState::Done
在成功读到一个状态的数据后,可以跳转到下一个状态继续操作,都完成就是Done。 其实可以考虑不利用状态来判断区分当前的流程,每次都从头读写,但是这样在网络较差的情况下会出现多次回溯已处理的数据。
事件驱动的写数据
同样的,我们利用BytesMut作为缓存,把响应(Response)一股脑的写进入。
fn write_response(&self, buf: &mut BytesMut) -> io::Result<()> {
self.write_status_line(buf)?;
self.write_headers(buf)?;
self.write_connection_close(buf)?;
self.write_content_length(buf)?;
self.write_empty_line(buf)?;
self.write_body(buf)?;
Ok(())
}
然后调用的flush
来触发数据的写入。
fn flush(&mut self) -> io::Result<()> {
if self.w_buf.len() > 0 {
let nwrite = match self.stream.write(&self.w_buf[..]) {
Ok(n) => n,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => 0,
Err(e) => return Err(e),
};
self.w_buf.advance(nwrite);
}
Ok(())
}
这样就可以依赖于可读事件的来不断写了。
fn on_writable(&mut self) -> io::Result<State> {
if self.w_buf.len() > 0 {
let nwrite = self.stream.write(&self.w_buf[..])?;
self.w_buf.advance(nwrite);
}
if self.finished && self.w_buf.len() == 0 {
return Ok(State::Done);
}
Ok(State::Running)
}
看一下效果
完整的代码
cd one-step-one-web/mio-web
cargo run
curl -i http://127.0.0.1:5000/
或者打开浏览器看看效果