我與Rust的緣分起始於當我在程式設計論壇上閒逛時,無意間發現了這麼一門現代型的系統安全的函式式系統程式語言,但是當時只是大致瞭解,並無深入學習,所以此次便將它細緻性地學習了一遍。
學習內容為書籍《The Rust Programming language》的全部內容(已完成)、《Rust程式設計之道》的全部內容(未完成)和《The Rustonomicon》的部分內容(未完成)。
一. 內容概述
我將 《Rust 程式語言》 的學習內容分為基礎學習(1至9章)與進階學習(10至19章),這兩個部分是對我學習內容的一個大概縮略。而後是一個根據書上最後一章(20章)進行的簡單的 web server 程式構建,最後是對比 Rust 社群已有的actix web 框架的一個簡單 example。
本文為《The Rust Programming language》最後一章的《 Web 實戰專案》練習,此部分學習練習程式碼已經發在了開源平臺 Gitee 和 GitHub 平臺上.
檢視上一部分請轉至
4.Rust簡單 Web server 構建
4.1Rust 構建單執行緒web伺服器:
構建的單執行緒web伺服器僅進行get方法處理和響應,以及出現錯誤之後的404錯誤返回,進行了三次簡單的重構之後,Rust程式碼如下:
src/main.rs:
use multithreaded_server::ThreadPool;
use std::time::Duration;
use std::fs;
use std::thread;
use std::io::prelude::*;
use std::net::{TcpListener,TcpStream};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4); //建立含有執行緒數為4的執行緒池
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 512]; //棧區建立一個 512 位元組的緩衝區
stream.read(&mut buffer).unwrap(); //從TcpStream中讀取位元組並放入緩衝區
println!("Request: {}", String::from_utf8_lossy(&buffer[..]));
let get = b"GET / HTTP/1.1\r\n";
let sleep = b"GET /sleep HTTP/1.1\r\n";
let (status_line, filename) = if buffer.starts_with(get) {
("HTTP/1.1 200 OK\r\n\r\n", "test.html")
} else if buffer.starts_with(sleep) {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK\r\n\r\n", "test.html")
} else {
("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
};
let contents = fs::read_to_string(filename).unwrap();
let response = format!("{}{}", status_line, contents);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
println!("Response: ");
println!("{}", response);
}
當然,基礎的TCP socket只能使用由rust官方標準庫的實現。
上述簡單單執行緒web server實現中建立了TCP socket,並進行了對7878埠的繫結和監聽,對於每一個客戶端和服務端之間開啟的TCP socket流連線和請求/響應過程進行了處理,並獲取了連線組成的迭代器。對每個連線進行錯誤處理之後,呼叫自定義連線處理函式handle_connection進行請求放置緩衝區處理後,響應web瀏覽器對應資源。
程式碼中為方便除錯,將請求與響應的內容進行了控制檯輸出。
以下是測試用的資原始碼:
test.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>test web server</title>
</head>
<body>
<h1>test</h1>
<p>this is test for little web server</p>
</body>
</html>
404.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>404-對不起!您訪問的頁面不存在</title>
<style type="text/css">
.head404 {
width: 580px;
height: 234px;
margin: 50px auto 0 auto;
background: url(https://www.daixiaorui.com/Public/images/head404.png) no-repeat;
}
.txtbg404 {
width: 499px;
height: 169px;
margin: 10px auto 0 auto;
background: url(https://www.daixiaorui.com/Public/images/txtbg404.png) no-repeat;
}
.txtbg404 .txtbox {
width: 390px;
position: relative;
top: 30px;
left: 60px;
color: #eee;
font-size: 13px;
}
.txtbg404 .txtbox p {
margin: 5px 0;
line-height: 18px;
}
.txtbg404 .txtbox .paddingbox {
padding-top: 15px;
}
.txtbg404 .txtbox p a {
color: #eee;
text-decoration: none;
}
.txtbg404 .txtbox p a:hover {
color: #FC9D1D;
text-decoration: underline;
}
</style>
</head>
<body bgcolor="#494949">
<div class="head404"></div>
<div class="txtbg404">
<div class="txtbox">
<p>對不起,您請求的頁面不存在、或已被刪除、或暫時不可用</p>
<p class="paddingbox">請點選以下連結繼續瀏覽網頁</p>
<p>》<a style="cursor:pointer" onclick="history.back()">返回上一頁面</a></p>
<p>》<a href="https://www.daixiaorui.com">返回網站首頁</a></p>
</div>
</div>
</body>
</html>
</html>
執行該簡單web server後,瀏覽器請求127.0.0.1:7878/ 或http://127.0.0.1:7878/sleep:
console也輸出了相應request/respons的內容:
如果訪問未定義的路由,例如:127.0.0.1:7878/test,則會返回404.html如下:
由此便完成了簡單的單執行緒web server。
雖然此單執行緒web server也能進行資源處理與響應,但是Rust的高階特性並未在其中體現,譬如多執行緒無畏併發和高階trait。而且因為server執行於單執行緒中,一次只能處理一個請求,意味著未完成第一個處理之前不會處理第二個連線,我在程式碼中增加了sleep模擬了慢請求,如果請求/sleep五秒之內(即當前sleep請求正在處理)請求/,則會發現只能等sleep休眠五秒結束之後才會出現。
即如果 server 正接收越來越多的請求,這類序列操作會使效能越來越差。如果一個請求花費很長時間來處理,隨後而來的請求則不得不等待這個長請求結束,即便這些新請求可以很快就處理完。於是我打算利用Rust的併發來進行server的改進。
4.2Rust 構建多執行緒web伺服器
構建多執行緒web server常用的辦法是為其實現執行緒池,同樣此處也將為此實現一個簡單的執行緒池,其中含有固定數量的執行緒,將新請求傳送到執行緒池做處理之後,執行緒池將維護接收到的請求佇列,每個執行緒從佇列中取出一個請求執行處理,然後向佇列索取另一個請求,即設計的該執行緒池的併發數N為執行緒池執行緒數,如果每個執行緒都在做慢請求響應,則依然會造成阻塞佇列,不過效能相比上面的單執行緒web server 增加了許多,所能處理的慢請求也變多了。
構建執行緒池的程式碼如下:
src/lib.rs:
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
struct Worker {
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) -> Worker {
let thread = thread::spawn(move || loop {
let message = receiver.lock().unwrap().recv().unwrap();
match message {
Message::NewJob(job) => {
println!("Worker {} got a job; executing.", id);
job();
}
Message::Terminate => {
println!("Worker {} was told to terminate.", id);
break;
}
}
});
Worker {
id,
thread: Some(thread),
}
}
}
type Job = Box<dyn FnOnce() + Send + 'static>;
enum Message {
NewJob(Job),
Terminate,
}
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Message>,
}
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(Message::NewJob(job)).unwrap();
}
}
//丟棄執行緒池
impl Drop for ThreadPool {
fn drop(&mut self) {
println!("Sending terminate message to all workers.");
for _ in &mut self.workers {
self.sender.send(Message::Terminate).unwrap();
}
println!("Shutting down all workers.");
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
這裡建立了ThreadPool的關聯函式new,其用以建立執行緒池,new通過通道建立傳送者和接收者,接收者實際上就是等待執行緒,所以在new中被數次克隆進等待佇列works裡,然後等待執行緒佇列works的每個執行緒通過執行緒安全智慧指標來實現共享任務佇列,即同一個傳送者sender。
Worker的關聯函式new中用迴圈實現了對通道的接收者請求任務,如果沒有得到任務,則會阻塞執行緒本身,並在得到時候執行,並且採用互斥器用以實現安全共享。當執行緒池被丟棄的時候會呼叫其drop函式進行執行緒佇列的清理,清理執行緒池中的所有執行緒。
由此實現了一個簡單的非同步執行連線的執行緒池,其對應的main函式如下:
use multithreaded_server::ThreadPool;
use std::time::Duration;
use std::fs;
use std::thread;
use std::io::prelude::*;
use std::net::{TcpListener,TcpStream};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
println!("Shutting down.");
}
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
let sleep = b"GET /sleep HTTP/1.1\r\n";
let (status_line, filename) = if buffer.starts_with(get) {
("HTTP/1.1 200 OK\r\n\r\n", "test.html")
} else if buffer.starts_with(sleep) {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK\r\n\r\n", "test.html")
} else {
("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
};
let contents = fs::read_to_string(filename).unwrap();
let response = format!("{}{}", status_line, contents);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
執行之後在瀏覽器進行慢請求測試與併發測試之後發現,當前建立的多執行緒server較單執行緒版本所能處理的併發連線增加,並且一定範圍內不受慢請求的影響,4個測試用併發數目獲取資源的情況在進行6次/與/sleep請求之後,console輸出情況如下:
即併發數為4,當新請求建立之後,執行緒池中的執行緒依然是佇列形式進行處理,吞吐量增加,符合預期。
如果將web server的請求數量設定在4的話,進行4次請求之後,該web server程式停止執行,console輸出如下情況:
可以看見執行緒佇列進行了訊息處理後被清理,符合預期。
同樣,執行該多執行緒 webserver後,瀏覽器能正確顯示與獲取資源。
4.3Rust acitx web框架
實際上Rust社群已經有許多不同的web框架的實現了,其中最出名的有actix和rocket,這裡以actix web框架為例,其實現了相應的Http server、請求處理程式以及型別安全的資訊提取器、錯誤處理、URL排程等,並且也實現了http2協議。
下面是一個簡單的actix web應用:
對應專案構成如下:
main.rs:
use actix_files as fs;
use actix_session::{CookieSession, Session};
use actix_utils::mpsc;
use actix_web::http::{header, Method, StatusCode};
use actix_web::{
error, get, guard, middleware, web, App, Error, HttpRequest, HttpResponse,
HttpServer, Result,
};
use std::{env, io};
/// favicon handler
#[get("/favicon")]
async fn favicon() -> Result<fs::NamedFile> {
Ok(fs::NamedFile::open("static/favicon.ico")?)
}
/// simple index handler
#[get("/welcome")]
async fn welcome(session: Session, req: HttpRequest) -> Result<HttpResponse> {
println!("{:?}", req);
// session
let mut counter = 1;
if let Some(count) = session.get::<i32>("counter")? {
println!("SESSION value: {}", count);
counter = count + 1;
}
// set counter to session
session.set("counter", counter)?;
// response
Ok(HttpResponse::build(StatusCode::OK)
.content_type("text/html; charset=utf-8")
.body(include_str!("../static/welcome.html")))
}
/// 404 handler
async fn p404() -> Result<fs::NamedFile> {
Ok(fs::NamedFile::open("static/404.html")?.set_status_code(StatusCode::NOT_FOUND))
}
/// response body
async fn response_body(path: web::Path<String>) -> HttpResponse {
let text = format!("Hello {}!", *path);
let (tx, rx_body) = mpsc::channel();
let _ = tx.send(Ok::<_, Error>(web::Bytes::from(text)));
HttpResponse::Ok().streaming(rx_body)
}
/// handler with path parameters like `/user/{name}/`
async fn with_param(
req: HttpRequest,
web::Path((name,)): web::Path<(String,)>,
) -> HttpResponse {
println!("{:?}", req);
HttpResponse::Ok()
.content_type("text/plain")
.body(format!("Hello {}!", name))
}
#[actix_web::main]
async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "actix_web=debug,actix_server=info");
env_logger::init();
HttpServer::new(|| {
App::new()
// cookie session middleware
.wrap(CookieSession::signed(&[0; 32]).secure(false))
// enable logger - always register actix-web Logger middleware last
.wrap(middleware::Logger::default())
// register favicon
.service(favicon)
// register simple route, handle all methods
.service(welcome)
// with path parameters
.service(web::resource("/user/{name}").route(web::get().to(with_param)))
// async response body
.service(
web::resource("/async-body/{name}").route(web::get().to(response_body)),
)
.service(
web::resource("/test").to(|req: HttpRequest| match *req.method() {
Method::GET => HttpResponse::Ok(),
Method::POST => HttpResponse::MethodNotAllowed(),
_ => HttpResponse::NotFound(),
}),
)
.service(web::resource("/error").to(|| async {
error::InternalError::new(
io::Error::new(io::ErrorKind::Other, "test"),
StatusCode::INTERNAL_SERVER_ERROR,
)
}))
// static files
.service(fs::Files::new("/static", "static").show_files_listing())
// redirect
.service(web::resource("/").route(web::get().to(|req: HttpRequest| {
println!("{:?}", req);
HttpResponse::Found()
.header(header::LOCATION, "static/welcome.html")
.finish()
})))
// default
.default_service(
// 404 for GET request
web::resource("")
.route(web::get().to(p404))
// all requests that are not `GET`
.route(
web::route()
.guard(guard::Not(guard::Get()))
.to(HttpResponse::MethodNotAllowed),
),
)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
執行後示例如下:
當然,這只是一個簡單的應用example。
總結
對比自己編寫的多執行緒非同步web server,可以發現actix有其完善的非同步處理、路由控制和資源控制,可見actix是一個簡易且快速的web框架。
本作品採用《CC 協議》,轉載必須註明作者和本文連結