14. 從零開始編寫一個類nginx工具, HTTP文件伺服器的實現過程及參數

来源:https://www.cnblogs.com/luojiawaf/archive/2023/10/19/17773837.html
-Advertisement-
Play Games

我們每天都在使用著文件伺服器,那你知道他其中有多少功能嗎?壓縮功能在其中占了多大的作用嗎?瀏覽器又是如何的正確識別文件的功能? ...


wmproxy

wmproxy將用Rust實現http/https代理, socks5代理, 反向代理, 靜態文件伺服器,後續將實現websocket代理, 內外網穿透等, 會將實現過程分享出來, 感興趣的可以一起造個輪子

項目 ++wmproxy++

gite: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

HTTP文件伺服器的意義

HTTP文件伺服器的意義是可以放置網站文件,可以放置數據文件
HTTP伺服器一般指網站伺服器,是指駐留於網際網路上某種類型電腦的程式,可以處理瀏覽器等Web客戶端的請求並返回相應響應。
當前大量的應用會依賴到文件伺服器,比如我們非常熟悉的網站(會載入index.html)文件及各種css及js文件,比如我們的各種APP會有相對應的版本信息,會有相應的版本文件,又或者小程式本身就是一個可執行文件,當你點擊的時候,應用去下載相應的小程式文件,然後在本地進行載入,然後打開提供服務。目前我們的互聯網上衝浪完全無法離開文件伺服器。

HTTP文件伺服器幾大作用

1. 文件共用

文件伺服器的主要功能是提供文件共用功能。它允許用戶從他們自己的電腦或設備訪問共用文件和文件夾,而不管他們的物理位置。用戶可以查看、編輯和保存存儲在伺服器上的文件,所有有權訪問該文件的用戶都會自動更新更改。
以下是我們在聊天軟體上發送一張圖片給另一個人的流程

flowchart TD A[你] B[APP] C[文件伺服器] D[APP伺服器] E[聊天對象] A -->|將圖片共用|B B -->|將圖片上傳|C C -->|返回圖片地址|B B -->|將圖片地址推送給|D D -->|將地址通知給|E E -->|從文件伺服器中獲取圖片|C

2. 集中存儲

此時你的不小心將數據刪除,此時你想找回原來的圖片,以下是整個過程

flowchart TD A[你] B[APP] C[文件伺服器] D[APP伺服器] D -->|同步舊的聊天記錄, 獲取圖片地址|B B -->|重新下載圖片|C B -->|獲取完圖片後推送給|A

此時文件伺服器擔任著集中存儲的角色,海量的數據將匯聚在中心伺服器上,我們可以通過網路訪問到海量的數據資源。
3. 備份與恢復
上述過程,相當於伺服器幫你備份了圖片數據,在你不小心丟失的時候,可以恢復您的數據,我們最經常使用的如圖片備份到網盤,一方面可以釋放掉本地的空間,另一方面我們可以將數據保存到很久之後。
4. 訪問控制
我們在獲取到圖片地址的時候,並不是任何的角色都可以獲取到該圖片的資源,在伺服器內部中,會有相關的許可權驗證,在為您提供數據的同時,並保護著您的數據安全。

file_server文件伺服器

一個靜態文件伺服器,支持真實和虛擬文件系統。它通過將請求的URI路徑附加到站點的根路徑來形成文件路徑。
最常見的是,file_server指令與root指令配對,為整個網站設置文件根。其中保證所有的訪問僅能在root指定的目錄之下,不能訪問其上級的任何數據,故在root下的目錄理論上即使禁目錄訪問也可能被全部訪問到(暴力遍歷),但在root上級的目錄不可能被以任何的方式進行訪問,即使添加../相對路徑也不行。

file_server參數相關

結構定義如下:

pub struct FileServer {
    #[serde(default = "default_root")]
    pub root: String,
    #[serde(default)]
    pub prefix: String,
    #[serde(default="default_hide")]
    pub hide: Vec<String>,
    #[serde(default = "default_index")]
    pub index: Vec<String>,
    #[serde(default = "default_status")]
    pub status: u16,
    #[serde(default = "default_precompressed")]
    pub precompressed: Vec<String>,
    #[serde(default)]
    pub disable_compress: bool,
    #[serde(default = "default_bool_true")]
    pub browse: bool,
}
  • browse 對沒有索引文件的目錄的請求,當前又是一個目錄的情況下啟用文件列表。
  • root 設置網站根目錄。指向的是當前文件磁碟下的路徑首碼,如/file/,那麼提供服務的將是/file/的文件服務
  • prefix Url的首碼,如/static/,如果我們獲取到一個請求路徑如/static/src/wmproxy.md,那麼我們會去掉首碼得到src/wmproxy.md,那麼實際的指向為/file/src/wmproxy.md進行文件服務
  • hide 是一個要隱藏的文件或文件夾的列表;如果要求,文件伺服器將假裝它們不存在。該指令接受占位符和glob模式。註意,這些是 文件系統 路徑,不是請求路徑。換句話說,相對路徑使用當前工作目錄作為基礎,而不是網站根目錄;所有的路徑在比較之前都會被轉換為絕對形式(如果可能的話)。指定一個沒有路徑分隔符的文件名或模式,將隱藏所有具有匹配名稱的文件,無論其位置如何;
  • index 是一個尋找索引文件的文件名列表。預設:index.html index.htm
  • precompressed 是用於搜索預壓縮挎包文件的編碼格式列表。支持的格式有gzip(.gz),和br(.br)。所有的文件查找將首先尋找未壓縮文件的存在。一旦找到,我們將以未添加之前的格式做mimetype,如README.md.gz取的是md的mimetype,也就是text/plain。並適當地設置Content-Encoding響應頭。否則,將以正常的未壓縮文件進行響應。如果encode指令被啟用,那麼如果沒有預壓縮,它可能會對響應進行即時壓縮。如我們訪問README.md,但此時目錄下存在README.md.gz,那我們我們響應的是gz的文件,並設置Content-Encoding: gzip,如此做的好處,我們對該文件的任何請求,我們都無須耗任何壓縮的時間,響應更快,我們可以用更高的壓縮比來進行預壓縮,可節省更多時間。
  • status 是一個可選的狀態代碼覆蓋,在編寫響應時使用。在用自定義錯誤頁面響應請求時特別有用。可以是一個3位數的狀態代碼,例如:404。支持占位符。預設情況下,寫入的狀態代碼通常是200,或206,用於部分內容。
  • 當前目錄外的靜態文件伺服器。
reverse:
  file_server:
  • 啟用了文件列表:
reverse:
  file_server:
    browse: true
  • 只服務於/static文件夾中的靜態文件:
reverse:
  file_server:
    root: /static/
    browse: true
  • 隱藏所有.git文件夾及其內容。
reverse:
  file_server:
    root: /static/
    browse: true
    hide: [.git]
  • 如果客戶端支持(Accept-Encoding頭),發送gzip,br,則檢查請求的文件是否存在預壓縮的文件。因此,如果/path/to/file被請求,/path/to/file.br和/path/to/file.gz`,並提供第一個具有相應內容編碼的可用文件。
reverse:
  file_server:
    root: /static/
    browse: true
    hide: [.git]
    precompressed: [br, gzip]

mimetype作用

多用途互聯網郵件擴展(MIME,Multipurpose Internet Mail Extensions)是一個 互聯網標準,它擴展了 電子郵件標準,使其能夠支持非 ASCII字元、 二進位格式附件等多種格式的郵件消息。
內容類型(Content-Type),這個頭部領域用於指定消息的類型。一般以下麵的形式出現。[type]/[subtype]

type有下麵的形式。

Text:用於標準化地表示的文本信息,文本消息可以是多種字元集和或者多種格式的;
Multipart:用於連接消息體的多個部分構成一個消息,這些部分可以是不同類型的數據;
Application:用於傳輸應用程式數據或者二進位數據;
Message:用於包裝一個E-mail消息;
Image:用於傳輸靜態圖片數據;
Audio:用於傳輸音頻或者音聲數據;
Video:用於傳輸動態影像數據,可以是與音頻編輯在一起的視頻數據格式。

subtype用於指定type的詳細形式。type/subtype配對的集合和與此相關的參數,將隨著時間而增長。為了確保這些值在一個有序而且公開的狀態下開發,MIME使用Internet Assigned Numbers Authority (IANA)作為中心的註冊機制來管理這些值。常用的subtype值如下所示:

  • text/plain(純文本)
  • text/html(HTML文檔)
  • application/xhtml+xml(XHTML文檔)
  • image/gif(GIF圖像)
  • image/jpeg(JPEG圖像)
  • image/png(PNG圖像)
  • video/mpeg(MPEG動畫)
  • application/octet-stream(任意的二進位數據)
  • application/pdf(PDF文檔)
  • application/msword(Microsoft Word文件)
  • message/rfc822( RFC 822形式)
  • multipart/alternative(HTML郵件的HTML形式和純文本形式,相同內容使用不同形式表示)
  • application/x-www-form-urlencoded(使用HTTP的POST方法提交的表單)
  • multipart/form-data(同上,但主要用於表單提交時伴隨文件上傳的場合)

我們根據現有的已知的,我們用了靜態變數做了以下數據定義,後續將會進行數據補充或者自定義

lazy_static! {
    static ref DEFAULT_MIMETYPE: HashMap<&'static str, &'static str> = {
        let mut m = HashMap::<&'static str, &'static str>::new();
        m.insert("doc", "application/msword");
        m.insert("pdf", "application/pdf");
        m.insert("rtf", "application/rtf");
        m.insert("xls", "application/vnd.ms-excel");
        m.insert("ppt", "application/vnd.ms-powerpoint");
        m.insert("rar", "application/application/x-rar-compressed");
        m.insert("swf", "application/x-shockwave-flash");
        m.insert("zip", "application/zip");
        m.insert("json", "application/json");
        m.insert("yaml", "text/plain");
        m.insert("mid", "audio/midi");
        m.insert("midi", "audio/midi");
        m.insert("kar", "audio/midi");
        m.insert("mp3", "audio/mpeg");
        m.insert("ogg", "audio/ogg");
        m.insert("m4a", "audio/m4a");
        m.insert("ra", "audio/x-realaudio");
        m.insert("gif", "image/gif");
        m.insert("jpeg", "image/jpeg");
        m.insert("jpg", "image/jpeg");
        m.insert("png", "image/png");
        m.insert("tif", "image/tiff");
        m.insert("tiff", "image/tiff");
        m.insert("wbmp", "image/vnd.wap.wbmp");
        m.insert("ico", "image/x-icon");
        m.insert("jng", "image/x-jng");
        m.insert("bmp", "image/x-ms-bmp");
        m.insert("svg", "image/svg+xml");
        m.insert("svgz", "image/svg+xml");
        m.insert("webp", "image/webp");
        m.insert("svg", "image/svg+xml");
        m.insert("css", "text/css");
        m.insert("html", "text/html");
        m.insert("htm", "text/html");
        m.insert("shtml", "text/html");
        m.insert("txt", "text/plain");
        m.insert("md", "text/plain");
        m.insert("xml", "text/xml");
        m.insert("3gpp", "video/3gpp");
        m.insert("3gp", "video/3gpp");
        m.insert("mp4", "video/mp4");
        m.insert("mpeg", "video/mpeg");
        m.insert("mpg", "video/mpeg");
        m.insert("mov", "video/quicktime");
        m.insert("webm", "video/webm");
        m.insert("flv", "video/x-flv");
        m.insert("m4v", "video/x-m4v");
        m.insert("wmv", "video/x-ms-wmv");
        m.insert("avi", "video/x-msvideo");
        m
    };
}

源碼實現

源碼主要實現在file_server.rsdeal_request函數。節選

pub async fn deal_request(
    &self,
    req: Request<RecvStream>,
) -> ProtResult<Response<RecvStream>> {
    let path = req.path().clone();
    // 無效首碼,無法處理
    if !path.starts_with(&self.prefix) {
        return Ok(self.ret_error_msg("unknow path"));
    }
    let root_path = Path::new(&self.root);
    let mut real_path = Path::new(&real_path).to_owned();
    // 必須保證不會跑出root設置的目錄之外,如故意訪問`../`之類的
    if !real_path.starts_with(root_path) || self.is_hide_path(root_path.as_ref()) {
        return Ok(self.ret_error_msg("can't view parent file"));
    }
    // 訪問路徑是目錄,嘗試是否有index的文件,如果有還是以文件訪問
    if real_path.is_dir() {
        for index in &self.index {
            let new_path = real_path.join(index);
            if new_path.exists() {
                real_path = new_path;
                break;
            }
        }
    }

    // 訪問為目錄,如果啟用目錄訪問,則返回當前的文件夾的內容
    if real_path.is_dir() {
        if !self.browse {
            return Ok(self.ret_error_msg("can't view parent file"));
        }
        let mut binary = BinaryMut::new();
        // ... 
        let recv = RecvStream::only(binary.freeze());
        let builder = Response::builder().version(req.version().clone());
        let mut response = builder
            .header(HeaderName::CONTENT_TYPE, "text/html; charset=utf-8")
            .body(recv)
            .map_err(|_err| io::Error::new(io::ErrorKind::Other, ""))?;
        if self.disable_compress {
            response.headers_mut().insert(HeaderName::CONTENT_ENCODING, "");
        }
        return Ok(response);
    } else {

        // 訪問為文件,判斷當前的尾碼,返回合適的mimetype,如果有合適的預壓縮文件,也及時返回
        if self.is_hide_path(path.as_ref()) {
            return Ok(self.ret_error_msg("can't view file"));
        }
        // 獲取尾碼
        let extension = if let Some(s) = real_path.extension() {
            s.to_string_lossy().to_string()
        } else {
            String::new()
        };
        let application = DEFAULT_MIMETYPE.get(&*extension).unwrap_or(&"");
        //查找是否有合適的預壓縮文件
        if let Some(accept) = req.headers().get_option_value(&HeaderName::ACCEPT_ENCODING) {
            for pre in &self.precompressed {
                // 得客戶端發送支持該格式
                if !accept.contains(pre.as_bytes()) {
                    continue;
                }
                let mut new = real_path.clone();
                new.as_mut_os_string().push(".");
                match &**pre {
                    "gzip" => new.as_mut_os_string().push("gz"),
                    "br" => new.as_mut_os_string().push("br"),
                    _ => continue,
                };
                // 如果預壓縮文件存在
                if new.exists() {
                    println!("convert to new file {}", new.to_string_lossy());
                    let file = File::open(new).await?;
                    let mut recv = RecvStream::new_file(file, BinaryMut::new(), false);
                    match &**pre {
                        "gzip" => recv.set_compress_origin_gzip(),
                        "br" => recv.set_compress_brotli(),
                        _ => unreachable!(),
                    }
                    // ...
                    return Ok(response);
                }
            }
        }

        if !real_path.exists() {
            return Ok(self.ret_error_msg("can't view file"));
        }

        // ...
        return Ok(response);
    }
}

結語

如此靜態文件伺服器則已初步實現,文件服務中的壓縮及流式傳輸已基本完成


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 目錄🎈 安裝PHP-FFMpeg🎈 視頻中提取一張圖片🎈 視頻中提取多張圖片🎈 調整視頻大小🎈 視頻添加水印🎈 生成音頻波形🎈 音頻轉換🎈 給音頻添加元數據🎈 拼接多個音視頻🎈 截取音視頻🎈 提取 gif 動圖🎈 裁剪視頻🎈 轉換視頻格式🎈 調整視頻幀率🎈 獲取音視頻信 ...
  • 創建名為spring_mvc_rest的新module,過程參考5.2節和6.6節 7.1、簡介 RESTful 也稱為REST(英文:Representational State Transfer)即表現層狀態傳遞,它是一種軟體架構風格或設計風格; REST 是 Roy Fielding 博士( ...
  • 將PDF轉為圖片能方便我們將文檔內容上傳至社交媒體平臺進行分享。此外,轉換為圖片後,還可以對圖像進行進一步的裁剪、調整大小或添加標記等操作。 用Python將PDF文件轉JPG/ PNG圖片可能是大家在一些項目中會遇到的需求,下麵將詳細介紹如何使用第三方庫Spire.PDF for Python來實 ...
  • 如何保持數據一致性 資料庫和緩存(比如:redis)雙寫數據一致性問題,是一個跟開發語言無關的公共問題。尤其在高併發的場景下,這個問題變得更加嚴重。 以下是我無意間瞭解很好的文章,分享給大家。 1. 常見方案 通常情況下,我們使用緩存的主要目的是為了提升查詢的性能。大多數情況下,我們是這樣使用緩存的 ...
  • CompletableFuture非同步編排優化代碼 我們在項目開發中,有可能遇到一個介面需要調用N個服務的介面。比如用戶請求獲取訂單信息,需要調用用戶信息、商品信息、物流信息等介面,最後再彙總數據統一返回。如果使用串列的方法按照順序挨個調用介面,這樣介面的響應的速度就很慢。如果並行調用介面,同時調用 ...
  • 本文分享自華為雲社區《從入門到精通:SimpleDateFormat類高深用法,讓你的代碼更簡潔!》,作者:bug菌。 環境說明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8 @[toc] 前言 日期時間在開發中是非常常見的需求,尤其是在處理與時間相關的 ...
  • 鏈表(Linked List)是一種線性數據結構,它由一系列節點(Node)組成,每個節點包含兩部分:數據和指向下(上)一個節點的引用(或指針)。鏈表中的節點按照線性順序連接在一起(相鄰節點不需要存儲在連續記憶體位置),不像數組一樣存儲在連續的記憶體位置。鏈表通常由頭節點(Head)來表示整個鏈表,而尾... ...
  • Ping 使用 `Internet` 控制消息協議(`ICMP`)來測試主機之間的連接。當用戶發送一個 `ping` 請求時,則對應的發送一個 `ICMP Echo` 請求消息到目標主機,並等待目標主機回覆一個 `ICMP Echo` 回應消息。如果目標主機接收到請求並且網路連接正常,則會返回一個回... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...