Rust Web Server
TCP连接
监听
TcpListener
用来监听Tcp的连接,他的incoming()
返回的TcpStream
表示了一个tcp连接。通过遍历这个stream可以获取客户端发来的数据,并进行应答。当stream执行出循环体后,就会断开这个连接,下面的例子种一个循环对应一个连接。
1 | let listener = TcpListener::bind("127.0.0.1:7878").unwrap() |
端口号在1204以下需要管理员权限,这里7878是rust四个字母在手机的9宫格按键。
运行程序后,直接在浏览器访问http://127.0.0.1:7878/
会得到The connection was reset.
的提示。程序的控制台实际上已经输出了很多次new connection established
。之所以有多次请求是因为浏览器还在请求其他的网站数据,例如icon等。
在浏览器的控制台可以看到有很多次请求,也就建立了多次连接,每一次服务端执行出循环,这个连接就被drop了。
处理请求
使用BufReader
来包装一个stream的可变引用,它提供了buffer机制方便读取数据,例如下面的lines()
方法。
1 | fn handle_connection(mut stream: TcpStream) { |
控制台会输出浏览器的请求。
1 | new connection established |
HTTP协议
http是超文本传输协议,它的请求都是文本类型。
请求协议
1 | Method Request-URI HTTP-Version CRLF ---> "GET / HTTP/1.1" |
应答协议
应答和请求类似
1 | HTTP-Version Status-Code Reason-Phrase CRLF |
通过读取一个文件index.html
应答给客户端,按照协议把三行信息通过stream.write_all()
应答给客户端
1 | fn handle_connection(mut stream: TcpStream) { |
处理请求不同地址
http请求"GET / HTTP/1.1"
中的第2段表示了请求的地址,因此根据不同的请求地址可以转到不同的应答处理函数。这里可以简单将非/
根目录的请求都应答为404.
1 | fn handle_connection(mut stream: TcpStream) { |
使用线程池处理多个请求
每当有一个新任务时,可以从线程池中取出一个线程执行这个任务。线程池中通过一个队列处理所有收到的请求,它最多并发执行线程池大小的任务。使用线程池是最简单的方案,还可以有fork/join模型
,单线程的异步IO
以及多线程的异步IO
。
单独创建一个src/lib.rs
来存放线程池实现代码,这样这个库以后还可以被其他应用程序使用
1 | use std::{sync::{mpsc, Arc, Mutex}, thread}; |
在main.rs文件中使用这个线程池,首先要引入进来use webserver::ThreadPool;
1 | use webserver::ThreadPool; |
需要特别注意的是Worker中的循环写法使用了loop,而不是while
1 | let thread = thread::spawn(move || { |
如果使用了while,receiver.lock()的声明周期在while循环体这一次的执行完成后,才能释放,也就是锁也会在job()执行完成后才能释放,导致其他线程在这个job没有执行完前都不能获取锁,也就不能同通道中获取新的任务信息,其实就没有多线程执行的效果了,因为其他线程获取receiver.lock().unwrap().recv()
这个操作被正在执行任务的这个线程的lock阻塞了。而使用let的方式,=右边的表达式在let执行完后,就会被释放了,锁的释放在执行Job之前,所以如果job耗时也不会影响其他线程拿锁。
释放线程资源
当程序执行不需要线程池时,可以通过让线程池实现Drop
trait来释放资源,结束线程。
工作线程中的线程闭包函数是一个死循环,因此需要跳出那个循环结束线程执行。线程函数中通过channel接收信号,因此可以通过在外部把sender释放,来断开通道,这样线程函数就能捕获到错误消息,从而跳出循环。释放sender时,需要把sender从ThreadPool取出来,如果它是ThreadPool的成员,因为drop的参数&mut self
拿了ThreadPool的可变引用,所以不能直接获取sender的引用,使用Option可以把sender包一下,通过take取出。
Option的take()方法可以把其中的值拿出去,并换一个None在里面,这样原来的Option对象并没有改变。例如
1 | let mut x = Some(2); |
ThreadPool 重新调整后
1 | pub struct ThreadPool { |
由于线程的join函数需要获取线程对象thread的所有权,而thread已经是一个可变引用的成员了。这时可以通过把thread改为一个Option<>类型,通过Option的take()函数获取其中的Some变量并留下None,这样外部就可以调用thread.join()
。需要同步修改Worker的thread成员为Option类型,并修改对应的new方法。
1 | struct Worker { |