《The Rust Programming language》程式碼練習(part 3 簡單web )

chen0adapter發表於2021-01-18
我與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 實戰專案》練習,此部分學習練習程式碼已經發在了開源平臺 GiteeGitHub 平臺上.

檢視上一部分請轉至

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:

server_1

console也輸出了相應request/respons的內容:
image-20210112002836825

如果訪問未定義的路由,例如:127.0.0.1:7878/test,則會返回404.html如下:
server_3

由此便完成了簡單的單執行緒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輸出情況如下:
server_4
即併發數為4,當新請求建立之後,執行緒池中的執行緒依然是佇列形式進行處理,吞吐量增加,符合預期。

如果將web server的請求數量設定在4的話,進行4次請求之後,該web server程式停止執行,console輸出如下情況:
server_5

可以看見執行緒佇列進行了訊息處理後被清理,符合預期。

同樣,執行該多執行緒 webserver後,瀏覽器能正確顯示與獲取資源。

4.3Rust acitx web框架

​ 實際上Rust社群已經有許多不同的web框架的實現了,其中最出名的有actix和rocket,這裡以actix web框架為例,其實現了相應的Http server、請求處理程式以及型別安全的資訊提取器、錯誤處理、URL排程等,並且也實現了http2協議。

​ 下面是一個簡單的actix web應用:

​ 對應專案構成如下:

server_6

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
}

​ 執行後示例如下:
server_7

​當然,這只是一個簡單的應用example。

總結

對比自己編寫的多執行緒非同步web server,可以發現actix有其完善的非同步處理、路由控制和資源控制,可見actix是一個簡易且快速的web框架。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章