超詳解讀web伺服器知識「web伺服器的域名格式」

eJet是一款在GitHub上開源的Web伺服器,下載地址為 ,利用 adif數據結構和演算法庫 和 ePump框架 開發的嵌入式Web伺服器、代理伺服器、Web Cache系統,可以庫的形式嵌入到應用程序中,提供Web服務功能。

一. eJet是什麼?

eJet Web伺服器是利用GitHub上的開源項目 adif數據結構和演算法庫 和 ePump框架,用C語言開發的一個事件驅動模型、多線程、大並發連接的輕量級的高性能Web伺服器,支持HTTP/1.0和HTTP/1.1協議,並支持HTTP Proxy、Tunnel等功能。

在Linux下,eJet Web伺服器編譯成動態庫或靜態庫的大小約為300K,可集成嵌入到任何應用程序中,增加應用程序使用HTTP通信和服務承載的能力,使其具備像Nginx伺服器一樣強大的Web功能。

eJet Web伺服器完全構建在ePump框架之上,利用ePump框架的多線程事件驅動模型,實現完整的HTTP請求<–>HTTP響應事務流程。eJet並沒有創建進程或線程,利用ePump框架的事件驅動多線程,高效地運用伺服器的CPU處理能力。

eJet接收和處理各TCP連接上的HTTP請求頭和請求體,經過解析、校驗、關聯、實例化等處理,執行HTTP請求,或獲取Web伺服器特定目錄下的文件,或代理客戶端發起向源HTTP伺服器的請求,或將HTTP請求通過FastCGI介面轉發到CGI伺服器,或將客戶端HTTP請求交給上層設置的回調函數處理等。所有處理結果,最終以HTTP響應方式,包括HTTP響應頭和響應體,通過客戶端建立的TCP連接,返回給客戶端。該TCP連接可以Pipe-line方式繼續發送和接收多個HTTP請求和響應。

eJet伺服器提供了作為Web伺服器所需的其他各項功能,包括基於TLS/SSL的安全和加密傳輸、虛擬主機、資源位置Location的各種匹配策略、對請求URI執行動態腳本指令(包括rewrite、reply、return、try_files等)、在配置文件中使用HTTP變數、正向代理和反向代理、HTTP Proxy、FastCGI、HTTP Proxy Cache功能、HTTP Tunnel、MultiPart文件上傳、動態庫回調或介面函數回調機制、HTTP日誌功能、CDN分發等。

eJet Web伺服器採用JSon格式的配置文件,進行系統配置管理。對JSon語法做了一定的擴展,使得JSon支持include文件指令,支持嵌入Script腳本程序語言。使用擴展JSon功能的配置文件,可更加靈活、方便地擴展Web服務功能。

eJet系統大量採用了Zero-Copy、內存池、緩存等技術,來提升Web伺服器處理性能和效率,加快了請求響應的處理速度,支撐更大規模的並發處理能力,支持更大規模的網路吞吐容量等。

eJet Web伺服器既可以面向程序員、系統架構師提供應用程序開發介面或直接嵌入到現有系統中,也可以面向運維工程師部署完全類似Nginx Web伺服器、Web Cache、CDN回源等商業服務系統,還是面向程序員提供學習、研究開發框架、通信系統等的理想平台。

開發eJet Web伺服器的原則是儘可能不依賴於第三方代碼和庫,降低版權和複雜部署等因素帶來的潛在風險。系統使用的第三方代碼或庫主要為:OpenSSL庫、Linux系統自帶的符合POSIX標準的正則表達式regex庫。gzip壓縮需要依賴zlib開源庫,目前沒有添加進來,所以eJet Web伺服器暫時不提供gzip、deflate的壓縮支持。

二. JSon格式的配置文件

2.1 JSON語法特點

JSON的全稱是JavaScript Object Notation,是一種輕量級的數據交換格式。JSON的文本格式獨立於編程語言,採用name:value對存儲名稱和數據,可以保存數字、字元串、邏輯值、數組、對象等數據類型,是理想的數據交換語法格式,簡潔幹練,易於擴展、閱讀和編寫,也便於程序解析和生成。

正是由於JSon語法的簡單和強擴展性、採用可保存各種數據類型的name/value對語法、可嵌套JSON子對象等特性,與配置文件的配置屬性特別吻合,所以,eJet系統使用JSon格式來保存、傳遞、解析系統配置文件。

2.2 eJet配置文件對JSON的擴展

2.2.1 分隔符

eJet系統使用adif中的JSon庫來解析、訪問配置文件信息。JSon語法預設格式以冒號(:)來分隔name和value,以單引號(‘)或雙引號(“)來包含name和value串,以逗號(,)作為name/value對的分隔符,以中括弧[]表示數組,以大括弧{}表示對象。

eJet系統採用JSon作為配置文件語法規範,為了兼容傳統配置文件的編寫習慣,將JSon基礎語法做了一些擴展,即分隔name與value的冒號(:)換成等於號(=),分隔name/value對之間的逗號(,)換成分號(;),其他基礎語法不變。

2.2.2 include指令

由於配置信息數據較大,需要使用不同的文件來保存不同的配置信息,借鑒C語言/PHP語言的include宏指令,eJet系統的JSon語法引入了include指令。擴展語法中將把”include”作為JSon語法的關鍵字,不會被當做對象名稱和值內容來處理,而是作為嵌入另外一個文件到當前位置進行後續處理的特殊指令。其語法規範如下:

include <配置文件名>;

解析JSon內容時,如果遇到include指令,就將include指令後面的文件內容載入到當前指令位置,作為當前文件內容的一部分,進行解析處理。

2.2.3 單行注釋和多行注釋

為了增加配置文件中代碼的可讀性,需要對相關的定義添加詳細說明、註解等內容,方便使用人員快速閱讀和理解。

為支持注釋功能,eJet系統的配置文件對JSON語法做了相應擴展,增加了單行注釋符號#和多行注釋(/* */),其語法規範如下:

# 這是單行注釋,如果井號(#)不在JSon某個Key-Value對的引號裡面,那麼以井號開頭,井號後面的內容都是注釋

/* 注意:多行注釋是以連在一起的/和*開始
         以連在一起的*和/結尾,中間的內容都是注釋
   多行注釋開閉符號,必須不能在Key-Value對的引號裡面
 */

注釋的內容在解析時直接忽略跳過,不會被系統解析和處理。

2.2.4 script語法

使用JSON格式的數據都是由name/value對構成,eJet系統中需要在配置文件中支持Script腳本程序,靈活動態地處理HTTP請求。

eJet配置文件對JSON語法格式擴展了一種固定名稱的script對象,將名稱”script”作為特殊對象的名稱關鍵字,即以script為名稱的對象,其內容不能作為JSON子對象處理,而是作為Script腳本程序內容,存放在對象名為script的對象中。其語法規範如下:

script = {
    if ($request_uri ~* '^/topic/[0-9](*)/(.*)\.mp4$') {
        set $video_flag 1;
    }
};

在同一個JSon對象下,可以有多個script對象,自動構成script對象數組。

另外,使用特殊的開閉標籤<script>和</script>,也可以定義腳本程序。在這兩個開閉標籤中間的內容,即是Script腳本程序,並將這些內容存儲到配置文件定義的任意name名稱對象中,其語法規範如下:

cache file = <script>
       if ($request_uri ~* 'laoooke')
           return "${host_name}_${server_port}${req_path_only}${req_file_only}";
       else if (!-f $root$request_path) {
           return "${host_name}_${server_port}${req_path_only}${index}";
       } else if (!-x $root$request_path) {
           return "$root$request_path is not an executable file";
       } else
           return "${request_header[host]}${req_path_only}else.html";
     </script>;

這樣,”cache file”對象的內容就是一段腳本程序,需要在解釋執行到這裡時,才真正具有實際數據。

三. eJet資源管理架構

3.1 三層資源定位架構

eJet Web伺服器的資源管理結構分成三層:

  • HTTP監聽服務HTTPListen – 對應的是監聽本地IP地址和埠後的TCP連接
  • HTTP虛擬主機HTTPHost – 對應的是請求主機名稱domain
  • HTTP資源位置HTTPLoc – 對應的是主機下的各個資源目錄

一個eJet Web伺服器可以啟動一個到多個監聽服務HTTPListen,一個監聽服務下可以配置一個到多個HTTP虛擬主機,一個虛擬主機下可以配置多個資源位置HTTPLoc。這裡的『多個』沒有數量限制,取決於系統的物理和內核資源限制。

3.2 HTTP監聽服務 – HTTPListen

HTTP監聽服務HTTPListen是指eJet Web伺服器在啟動時,需要綁定本地某個伺服器IP地址和某個埠後,啟動TCP監聽服務,等候接收客戶端發起TCP連接和HTTP請求數據,每個接受的HTTPCon連接一定屬於某個HTTP監聽服務HTTPListen。嚴格來說,HTTPListen負責接受HTTPCon連接,並將請求數據存儲到HTTPCon的接收緩衝區,所以監聽服務對應的是TC連接資源管理,即對應的是請求資源的domain和埠。

HTTP監聽服務的配置信息格式參考如下:

listen = {
    local ip = *; #192.168.1.151
    port = 443;
    forward proxy = on;

    ssl = on;
    ssl certificate = cert.pem;
    ssl private key = cert.key;
    ssl ca certificate = cacert.pem;

    request process library = reqhandle.so

    script = {
        #reply 302 https://ke.test.ejetsrv.com:8443$request_uri;
        addResHeader X-Nat-IP $remote_addr;
    }

    host = {.....}
    host = {.....}
    host = {.....}
}

一台物理伺服器可以安裝多個網卡,每個網卡配置一個獨立IP地址,HTTP監聽服務可以監聽某一個IP地址上的某個埠,也可以監聽所有IP地址上的同一個埠。能啟動監聽服務的埠數量理論上是65536個,其中小於1024的埠需要有root超戶許可權才能監聽。

HTTP監聽服務HTTPListen依賴於底層ePump框架的eptcp_mlisten介面函數,通過該介面,讓每一個epump監聽線程都去監聽指定IP地址和埠上的連接請求和數據請求服務。對於支持REUSEPORT的操作系統內核,大量客戶端發起的並發連接,將會通過內核accept系統調用均衡地分攤到各epump線程處理,對於不支持REUSEPORT的操作系統,ePump框架負責大並發連接在各監聽線程間的負載均衡。

HTTP監聽服務HTTPListen可以設置當前監聽為需要SSL的安全連接,並配置SSL握手所需的私鑰、證書等。配置為SSL安全連接監聽服務後,客戶端發起的HTTP請求都必須是以https://開頭的URL。

在HTTP監聽服務HTTPListen里,可以設置Script腳本程序,執行各種針對請求數據進行預判斷和預處理的指令。這些腳本程序的執行時機是在收到完整的HTTP請求頭後進行的。

eJet系統提供了動態庫回調機制,使用動態庫回調,既可以擴展eJet Web伺服器能力,也可以將小型應用系統附著在eJet Web伺服器上,處理客戶端發起的HTTP請求。

HTTP監聽服務HTTPListen下可管理多個虛擬主機HTTPHost,採用主機名稱為索引主鍵的hashtab來管理下屬的虛擬主機表。噹噹前監聽服務的埠收到TCP請求和數據後,根據Host請求頭的主機名稱,來精確匹配定位出該請求的HTTP虛擬主機HTTPHost。

3.3 HTTP虛擬主機 – HTTPHost

在HTTPListen監聽服務下,可以配置多個虛擬主機,虛擬主機HTTPHost是eJet Web伺服器資源管理體系的第二層,將HTTPCon緩衝區的數據進行解析,創建HTTPMsg來保存解析後的HTTP請求數據,HTTP協議規範中,請求頭Host攜帶的值內容是URL中domain信息,所以HTTP虛擬主機HTTPHost,對應的就是請求域名,或者就是一個網站。一個監聽服務HTTPListen下可以寄宿大量的通過虛擬主機HTTPHost來管理的網站。

HTTP虛擬主機的配置信息格式參考如下:

host = {
    host name = *; #www.ejetsrv.com
    type = server | proxy | fastcgi;
    gzip = on;

    ssl certificate = cert.pem;
    ssl private key = cert.key;
    ssl ca certificate = cacert.pem;

    script = {
        #reply 302 https://ke.test.ejetsrv.com:8443$request_uri;
        addResHeader X-Nat-IP $remote_addr;
    }

    error page = {
        400 = 400.html;
        504 = 504.html;
        root = /opt/ejet/errpage;
    }

    root = /home/hzke/sysdoc;

    location = {...}
    location = {...}
    location = {...}
}

HTTP虛擬主機的名稱一般是域名格式,即多級名稱體系,包含頂級域名、二級域名、三級域名等,通過DNS系統,將該域名解析到當前eJet Web伺服器所在的IP地址上,如果在該IP地址上啟動HTTPListen服務,那麼所有使用該域名的請求都會指向到對應的HTTPHost虛擬主機。

eJet系統根據功能服務形式,對虛擬主機定義了幾種類型:Server、Proxy、FastCGI等,這幾種類型可以同時並存,可或在一起。

虛擬主機HTTPHost下可以設置資源的預設目錄,下屬的資源位置HTTPLoc都可以復用虛擬主機的預設目錄。

如果當前虛擬主機HTTPHost的上級監聽服務是建立在安全連接SSL上,那麼在有多個網站即多個虛擬主機情況下,需要為每個網站配置屬於該網站域名的證書、私鑰等安全身份標識信息,客戶端在向同一個監聽服務發送請求後,採用TLS SNI機制和eJet中實現的SSL域名選擇回調,來完成域名和證書的選擇。

HTTPHost虛擬主機下可以設置Script腳本程序,虛擬主機下的腳本程序被執行時機是在創建HTTPMsg實例,並設置完DocURI後開始執行資源位置實例化流程,在該流程中分別執行HTTPListen的Script腳本、HTTPHost的Script腳本、HTTPLoc的Script腳本。腳本程序的執行按照上述優先順序來進行,使用腳本程序的指令來預處理HTTP請求的各類數據。

一個虛擬主機HTTPHost下可以配置多個資源位置HTTPLoc,代表訪問當前域名下的不同目錄。虛擬主機HTTPHost採用多種方式管理下屬的資源位置HTTPLoc實例,主要包括三種:

  • 精確匹配請求路徑的虛擬主機表 – 以請求路徑名稱為索引的資源位置索引表
  • 對請求路徑前綴匹配的虛擬主機表 – 以請求路徑前綴名稱為索引的資源位置字典樹
  • 對請求路徑進行正則表達式運算的虛擬主機表 – 對正則表達式字元串為索引建立的資源位置列表

進入當前虛擬主機後,到底採用哪個資源位置HTTPLoc,匹配規則和順序是按照上述列表的排序來進行的,首先根據HTTP請求的路徑名在資源位置索引表中精準匹配,如果沒有,則對請求路徑名的前綴在資源位置字典樹中進行匹配檢索,如果還沒有匹配上,最後對資源位置列表中的每個HTTPLoc,利用其正則表達式字元串,去匹配當前請求路徑名,如果還是沒有匹配的資源位置HTTPLoc,那麼使用當前虛擬主機的預設資源位置。

3.4 HTTP資源位置 – HTTPLoc

HTTP資源位置HTTPLoc代表的是請求資源在某個監聽服務下的某個虛擬主機里的目錄位置,HTTPLoc代表的是請求路徑,根據HTTPMsg中的客戶端請求數據,最終基於各種資源匹配規則,找到HTTPListen、HTTPHost、HTTPLoc後,基本確定了當前請求的資源位置、處理方式等。一個網站對應的虛擬主機下,可以有多種功能和資源類別的資源位置HTTPLoc,如圖像文件放置在image為根的目錄下,PHP文件需要採用FastCGI轉發給php-fpm解釋器等。

HTTP資源位置的配置信息格式參考如下:

location = {
    type = server;
    path = [ "\.(h|c|apk|gif|jpg|jpeg|png|bmp|ico|swf|js|css)$", "~*" ];

    root = /opt/ejet/httpdoc;
    index = [ index.html, index.htm ];
    expires = 30D;

    cache_file = <script>
           if ($request_uri ~* 'laoke')
               return "${host_name}_${server_port}${req_path_only}${req_file_only}";
           else if (!-f $root$request_path) {
               return "$root$request_path is not a regular file";
           } else if (!-x $root$request_path) {
               return "$root$request_path is not an executable file";
           } else
               return "${request_header[host]}${req_path_only}else.html";
         </script>;
}

location = {
    path = [ '^/view/([0-9A-Fa-f]{32})$', '~*' ];
    type = proxy;
    passurl = http://cdn.ejetsrv.com/view/$1;

    root = /opt/cache/;
    cache = on;
    cache file = /opt/cache/${request_header[host]}/view/$1;
}

location = {
    type = fastcgi;
    path = [ "\.(php|php?)$", '~*'];

    passurl = fastcgi://localhost:9000;

    index = [ index.php ];
    root = /opt/ejet/php;
}

location = {
    path = [ '/' ];
    type = server;

    script = {
        try_files $uri $uri/ /index.php?$query_string;
    };

    index = [ index.php, index.html, index.htm ];
}

HTTP資源位置HTTPLoc是通過路徑名path和匹配類型matchtype來作為其標識,路徑名為配置中設置的名稱,客戶端請求的路徑名通過匹配類型定義的匹配規則來跟設置的路徑名進行匹配,如果符合匹配,則該請求使用此資源位置HTTPLoc。

匹配規則matchtype一般定義在配置文件中path數組裡的第二項,分為如下幾種:

  • 精準匹配,使用等於號’=’
  • 前綴匹配,使用’^~’這兩個符號
  • 區分大小寫的正則表達式匹配,使用’~’符號
  • 不區分大小寫的正則表達式匹配,使用’~*’這兩個符號
  • 通用匹配,使用’/’符號,如果沒有其他匹配,任何請求都會匹配到

匹配的優先順序順序為: (location =) > (location 完整路徑) > (location ^~ 路徑) > (location ,* 正則順序) > (location 部分起始路徑) > (/)

eJet系統根據功能服務形式,對資源位置HTTPLoc定義了幾種類型:Server、Proxy、FastCGI等,通常情況下,一個資源位置HTTPLoc只屬於一種類型。

HTTP資源位置HTTPLoc都需要一個預設的根目錄,指向當前資源所在的根路徑,客戶端請求的路徑都是相對於當前HTTPLoc下的root跟目錄來定位文件資源的。對於Proxy模式,根目錄一般充當緩存文件的根目錄,即需要對Proxy代理請求回來的內容緩存時,都保存在當前HTTPLoc下的root目錄中。

每個HTTPLoc下都會有預設文件選項,可以配置多個預設文件,一般設置為index.html等。使用預設文件的情形是客戶端發起的請求只有目錄形式,如http://www.xxx.com/,這時該請求訪問的是HTTPLoc的根目錄,eJet系統會自動地依次尋找當前根目錄下的各個預設文件是否存在,如果存在就返回預設文件給客戶端。不過需要注意的是,eJet系統中這個流程是在設置DocURI時處理的。

HTTP資源位置如果是Proxy類型或FastCGI類型,則必須配置轉發地址passurl,轉發地址passurl一般都為絕對URL地址,含有指向其他伺服器的domain域名,passurl的形式取決HTTPLoc資源類型。

反向代理(Reverse Proxy)就是將HTTPLoc的資源類型設置為Proxy模式,通過設置passurl指向要代理的遠程伺服器URL地址,來實現反向代理功能。在反向代理模式下,passurl可以是含有匹配結果變數的URL地址,這個地址指向的是待轉發的下一個Origin伺服器,匹配變數如果為1、1、2等數字變數,即表示基於正則表達式匹配路徑時,把第一個或第二個匹配字元串作為passurl的一部分。當然passurl可以包含任何全局變數或配置變數,使用這些變數可以更靈活方便地處理轉發數據。

在反向代理模式下,HTTPLoc資源位置下有一個cache開關,如果設置cache=on即打開Cache功能,則需要在當前HTTPLoc下設置cachefile緩存文件名。對於不同的請求地址,cachefile必須隨著請求路徑或參數的變化而變化,所以cachefile的取值設置需要採用HTTP變數,或者使用Script腳本來動態計算cachefile的取值。

HTTPLoc下一般都會部署Script腳本程序,包括rewrite、reply、try_files等,根據請求路徑、請求參數、請求頭、源地址等信息,決定當前資源位置是否需要重寫、是否需要轉移到其他地址處理等。

四. HTTP變數

4.1 HTTP變數的定義

HTTP變數是指在eJet Web伺服器運行期間,能動態地訪問HTTP請求、HTTP響應、HTTP全局管理等實例對象中的存儲空間里的數據,或者訪問HTTP配置文件的配置數據等等,針對這些存儲空間的訪問,而抽象出來的名稱叫做HTTP變數。

變數的引用必須以開頭,後跟變數名,如果變數名後面還有連續緊隨的其他字元串,則需用{}來包括住變數名,其基本格式為:開頭,後跟變數名,如果變數名後面還有連續緊隨的其他字元串,則需用來包括住變數名,其基本格式為:變數名稱, {變數名稱},變數名稱,{ 變數名稱 },等等

4.2 HTTP變數的應用

使用HTTP變數的場景主要在JSon格式的配置文件中,給各個配置項目增加動態的可編程介面,就需要基於不同的HTTP請求的信息,做判斷、比較、賦值、拷貝、串接等操作,這些都離不開變數,需要不同的變數名去訪問不同HTTP請求中的不同信息內容,通過配置中使用變數:訪問變數的值,進行條件判斷、比較、匹配、加減乘除、賦值等。變數的使用樣例可參考如下:

access log = {
    log2file = on;
    log file = /var/log/access.log;
    format = [ '$remote_addr', '-', '[$datetime[stamp]]', '"$request"', '"$request_header[host]"',
               '"$request_header[referer]"', '"$http_user_agent"', '$status', '$bytes_recv', '$bytes_sent' ];
}

script = {
    reply 302 https://ke.test.ejetsrv.com:8443$request_uri;
}

cache file = /opt/cache/${request_header[host]}/view/$1;

params = {
    SCRIPT_FILENAME   = $document_root$fastcgi_script_name;
    QUERY_STRING      = $query_string;
    REQUEST_METHOD    = $request_method;
    CONTENT_TYPE      = $content_type;
    CONTENT_LENGTH    = $content_length;
}

script = {
    if ($query[fid])
        cache file = $real_path$query[fid]$req_file_ext;
    else if ($req_file_only)
        cache file = $real_path$req_file_only;
    else if ($query[0])
        cache file = ${real_path}${query[0]}$req_file_ext;
    else
        cache file = ${real_path}index.html;
}

4.3 HTTP變數的類型和使用規則

eJet系統中,共定義了四種HTTP變數類型,分別為:

  • 匹配變數 – 基於資源位置HTTPLoc模式串匹配HTTP請求路徑時匹配串,通過數字變數來訪問,如1,1,2等;
  • 局部變數 – 由script腳本在執行過程中用set指令或賦值符號「=」設置的變數;
  • 配置變數 – 配置文件中Listen、Host、Location下定義的JSon Key變數,以系統會使用到的常量定義為主;
  • 參數變數 – 變數名稱由系統預先定義、但值內容是在HTTPMsg創建後被賦值的變數,參數變數的值是只讀不可寫。

變數的使用規則符合高級語言的約定,對於同名變數,取值時優先順序順序為: 匹配變數 >匹配變數>局部變數 > 配置變數 >配置變數>參數變數

HTTP變數的值類型是弱類型,根據賦值、運算的規則等上下文環境的變化,來確定被使用時變數是數字型、字元型等。除了匹配變數外,其他變數的名稱必須是大小寫字母和下劃線_組合而成,其他字元出現在變數名里則該變數一定是非法無效變數。變數的定義很簡單,前面加上美元符號$,後面使用變數名稱,系統就會認為是HTTP變數。美元符號後的變數名稱也可以通過大括弧{}來跟跟其他字元串區隔。

如果變數的值內容包含多個,那麼該變數是數組變數,數組變數是通過中括弧[]和數字下標序號來訪問數組的各個元素,如$query[1]訪問是請求參數中的第一個參數的值。

匹配變數的名稱為數字,以美元號冠頭,如冠頭,如1,$2…,其數字代表的是使用HTTPLoc定義的路徑模式串,去匹配當前HTTP請求路徑時,被匹配成功的多個子串的數字序號。匹配變數的壽命周期是HTTPMsg實例化成功即準確找到HTTPLoc資源位置實例後開始,到HTTP響應被成功地發送到客戶端後,HTTPMsg消息被銷毀時為止。

局部變數的名稱由字母和下劃線組成,是script腳本在執行過程中用set指令或賦值符號「=」設置的變數,其壽命周期是從變數被創建之後到該HTTPMsg被銷毀這段期間,而HTTPMsg則是用戶HTTP請求到達時創建,成功返回Response後被摧毀。

配置變數是JSon格式的配置文件中定義的Key-Value對中,以Key為名稱的變數,變數的值是設置的Value內容。在配置文件中位於Location、Host、Listen下定義的Key-Value賦值語句對,左側為變數名,右側為變數值,用$符號可以直接引用這些變數定義的內容;在Listen、Host、Location下定義的配置變數,主要是以系統中可能使用到的常量定義為主,這些常量定義也可以使用script腳本來動態定義其常量值,此外,用戶可以額外定義系統配置中非預設常量,我們稱之為動態配置變數。

參數變數是系統預定義的有固定名稱的一種變數類型,參數變數一般指向HTTP請求的各類信息、eJet系統定義的全局變數等。參數變數的名稱是eJet系統預先定義並公布,但大部分變數的內容是跟HTTP請求HTTPMsg相關的,即不同的請求HTTPMsg,參數變數名的值也是隨著變化的。一般要求,參數變數是只讀不可寫變數,即參數變數的值不能被腳本程序改變,只能讀取訪問。

4.4 預定義的參數變數列表和實現原理

相比其他三種變數,參數變數是被使用最多、最有訪問價值的變數,參數變數是系統預先定義的固定名稱變數,變數的值是隨著HTTP請求HTTPMsg的不同而不同。通過參數變數,配置文件中可以根據請求的信息,靈活動態地決定相關配置選項的賦值內容,從而擴展eJet伺服器的能力,減少因額外功能擴展升級eJet系統的定製開銷。

參數變數一般由eJet系統預先定義發布,其變數的值內容是跟隨HTTP請求HTTPMsg的變化而變化,但變數名稱是全局統一通用,所以參數變數也有時稱為全局變數。

eJet系統預定義的參數變數如下:

  • remote_addr – HTTP請求的源IP地址
  • remote_port – HTTP請求的源埠
  • server_addr – HTTP請求的伺服器IP地址
  • server_port – HTTP請求的伺服器埠
  • request_method – HTTP請求的方法,如GET、POST等
  • scheme – HTTP請求的協議,如http、https等
  • host_name – HTTP請求的主機名稱
  • request_path – HTTP請求的路徑
  • query_string – HTTP請求的Query參數串
  • req_path_only – HTTP請求的只含目錄的路徑名
  • req_file_only – HTTP請求路徑中的文件名稱
  • req_file_base – HTTP請求路徑中的文件基本名
  • req_file_ext – HTTP請求路徑中文件擴展名
  • real_file – HTTP請求對應的真實文件路徑名
  • real_path – HTTP請求對應的真實文件所在目錄名
  • bytes_recv – HTTP請求接收到的客戶端位元組數
  • bytes_sent – HTTP響應發送給客戶端的位元組數
  • status – HTTP響應的狀態碼
  • document_root – HTTP請求的資源位置根路徑
  • fastcgi_script_name – HTTP請求中經過腳本運行後的DocURI的路徑名
  • content_type – HTTP請求的內容MIME類型
  • content_length – HTTP請求體的內容長度
  • absuriuri – HTTP請求的絕對URI
  • uri – HTTP請求源URI的路徑名
  • request_uri – HTTP請求源URI內容
  • document_uri – HTTP請求經過腳本運行後的DocURI內容
  • request – HTTP請求行
  • http_user_agent – HTTP請求用戶代理
  • http_cookie – HTTP請求的Cookie串
  • server_protocol – HTTP請求的協議版本
  • ejet_version – eJet系統的版本號
  • request_header – HTTP請求的頭信息數組,通過帶有數字下標或請求頭名稱的中括弧來訪問
  • cookie – HTTP請求的Cookie數組,通過帶有數字下標或Cookie名稱的中括弧來訪問
  • query – HTTP請求的Query參數數組,通過帶有數字下標或參數名稱的中括弧來訪問
  • response_header – HTTP響應的頭信息數組,通過帶有數字下標或響應頭名稱的中括弧來訪問
  • datetime – 系統日期時間數組,不帶中括弧是系統時間,帶createtime或stamp的中括弧則訪問HTTPMsg創建時間和最後時間
  • date – 系統日期數組,同上
  • time – 系統時間,同上

隨著應用場景的擴展,根據需要還可以擴展定義其他名稱的參數變數。總體來說,使用上述參數變數,基本可以訪問HTTP請求相關的所有信息,能滿足絕大部分場景的需求。

系統中預定義的參數變數,都是指向特定的基礎數據結構的某個成員變數,在該數據結構實例化後,其成員變數的地址指針就會被動態地賦值給預定義的參數變數,從而將地址指針指向的內容關聯到參數變數上。

在設置預定義參數變數名時,一般需要設置關聯的數據結構、數據結構的成員變數地址或位置、成員變數類型(字元、短整數、整數、長整數、字元串、字元指針、frame_t)、符號類型、存儲長度等,eJet系統中維持一個這樣的參數變數數組,分別完成參數變數數據的初始化,通過hashtab_t來快速定位和讀寫訪問數組中的參數變數。

獲取參數變數的實際值時,需要傳遞HTTPMsg這個數據結構的實例指針,根據參數變數名快速找到參數變數數組的參數變數實例,根據參數變數的信息,和傳入的實例指針,定位到該實際成員變數的內存指針和大小,從內存中取出該成員變數的值。

五. HTTP Script腳本

5.1 HTTP Script腳本定義

eJet系統在配置文件上擴展了Script腳本語言的語法定義,對JSon語法規範進行擴展,定義了一套符合JavaScript和C語言的編程語法,並提供Script腳本解釋器,實現一定的編程和解釋執行功能。

Script腳本是由一系列符合定義的語法規則而編寫的代碼語句組成,代碼語句風格類似Javascript和C語言,每條語句由一到多條指令構成,並以分號;結尾。

5.2 Script腳本嵌入位置

HTTP Script腳本程序的嵌入位置,共有兩種。第一種嵌入位置是在配置文件的Listen、Host、Location下,通過增加JSon對象script,將腳本程序作為script對象的內容,來實現配置文件中嵌入腳本編程功能。在這種位置中,插入script腳本代碼的語法共定義了三種:

  script = {....};
  script = if()... else...;
  <script> .... </script>

另外一種嵌入Script腳本程序的位置,是在JSon中的Key-Value對中,在Value里增加特殊閉合標籤<script> Script Codes </script>,在標籤裡面嵌入Script腳本代碼,執行完代碼後返回的內容,作為Key的值,這種方式使得JSon規範中Key的值可以動態地由Script腳本程序計算得來。在Listen、Host或Location的常量賦值中,Value內容可以是script腳本,如

  cache file = <script> if ()... return... </script>

對adif 基礎庫中的json.c文件做了修改擴展,使得Json對象都能支持script腳本定義的這幾種語法,如果某個對象下有名稱為script的數據項,就認為該數據項下的Value值為腳本內容。這就將名稱script作為Json的預設常量名稱了,使用時輕易不要使用script作為變數名。

5.3 Script腳本範例

HTTP Script腳本程序示例如下:

 script = {
     if ($query[fid]) "cache file" = $req_path_only$query[fid]$req_file_ext;
     else if ($req_file_only) "cache file" = ${req_path_only}index.html;
     else "cache file" = $req_path_only$req_file_only; 
 }

 cache file = <script> if ($query[fid]) return $req_path_only$query[fid]$req_file_ext;
                        else if ($req_file_only) return ${req_path_only}index.html;
                        else return $req_path_only$req_file_only; 
              </script>

 <script>
     if ($query[fid]) "cache file" = $req_path_only$query[fid]$req_file_ext;
     else if ($req_file_only) "cache file" = ${req_path_only}index.html;
     else "cache file" = $req_path_only$req_file_only; 
 </script>

 <script>
     if ($scheme == "http://") rewrite ^(.*)$  https://$host$1;
 </script>

HTTP Script腳本程序的解釋執行,是在創建HTTPMsg實例並設置完DocURI後,開始執行資源位置實例化流程,在實例化過程中,分別執行HTTPListen的Script腳本、HTTPHost的Script腳本、HTTPLoc的Script腳本。

5.4 Script腳本語句

script腳本是由一系列語句構成的程序,語法類似於JavaScript和C語音,主要包括如下語句:

5.4.1 條件語句

條件語句主要以if、else if、else組成,基本語法為:

if (判斷條件) { ... } else if (判斷條件) { ... } else { ... }

判斷條件至少包含一個變數或常量,通過對一個或多個變數的值進行判斷或比較,取出結果為TRUE或FALSE,來決定執行分支,判斷條件包括如下幾種情況:

  • (a) 判斷條件中只包含一個變數;
  • (b) 判斷條件中包含了兩個變數;
  • (c) 文件或目錄屬性的判斷;

判斷比較操作主要包括:

  • (a) 變數1 == 變數2,判斷是否相等,兩個變數值內容相同為TRUE,否則為FALSE
  • (b) 變數1 != 變數2,判斷不相等,兩個變數值內容不相同為TRUE,否則為FALSE
  • (c) 變數名,判斷變數值,變數定義了、且變數值不為NULL、且變數值不為0,則為TRUE,否則為FALSE
  • (d) !變數名,變數值取反判斷,變數未定義,或變數值為NULL、或變數值為0,則為TRUE,否則為FALSE
  • (e) 變數1 ^~ 變數2,變數1中的起始部分是以變數2開頭,則為TRUE,否則為FALSE
  • (f) 變數1 ~ 變數2,在變數1中查找變數2中的區分大小寫正則表達式,如果匹配則為TRUE,否則為FALSE
  • (g) 變數1 ~* 變數2,在變數1中查找變數2中的不區分大小寫正則表達式,如果匹配則為TRUE,否則為FALSE
  • (h) -f 變數,取變數值字元串對應的文件存在,則為TRUE,否則為FALSE
  • (i) !-f 變數,取變數值字元串對應的文件不存在,則為TRUE,否則為FALSE
  • (j) -d 變數,取變數值字元串對應的目錄存在,則為TRUE,否則為FALSE
  • (k) !-d 變數,取變數值字元串對應的目錄存在,則為TRUE,否則為FALSE
  • (l) -e 變數,取變數值字元串對應的文件、目錄、鏈接文件存在,則為TRUE,否則為FALSE
  • (m) !-e 變數,取變數值字元串對應的文件、目錄、鏈接文件不存在,則為TRUE,否則為FALSE
  • (n) -x 變數,取變數值字元串對應的文件存在並且可執行,則為TRUE,否則為FALSE
  • (o) !-x 變數,取變數值字元串對應的文件不存在或不可執行,則為TRUE,否則為FALSE

5.4.2 賦值語句

賦值語句主要由set語句構成,eJet系統中局部變數的創建和賦值是通過set語句來完成的。其語法如下:

set $變數名  value;

5.4.3 返回語句

返回語句也即是return語句,將script閉合標籤內嵌入的Scirpt腳本代碼執行運算後的結果,或Key-Value對中Value內嵌的腳本程序,解釋執行後的結果返回給Key變數,基本語法為:

return $變數名;
return 常量;

其使用形態如下:

cache file = <script> if ($user_agent ~* "MSIE") return $real_file; </script>;

5.4.4 響應語句

響應語句也就是reply語句,執行該語句後,eJet系統將終止當前HTTP請求HTTPMsg的任何處理,直接返回HTTP響應給客戶端,其語法如下:

reply  狀態碼  [ URL或響應消息體 ];

如果返回的狀態碼是 444,則直接斷開 TCP 連接,不發送任何內容給客戶端。

調用Reply指令時,可以使用的狀態碼有:204,400,402-406,408,410, 411, 413, 416 與 500-504。如果不帶狀態碼直接返回 URL 則被視為 302。其使用形態如下:

  if ($http_user_agent ~ curl) {
      reply 200 'COMMAND USER\n';
  }   
  if ($http_user_agent ~ Mozilla) {
      reply 302 http://www.baidu.com?$args;
  }      
  reply 404;

eJet系統在解釋器解釋執行Script代碼時,先執行Listen下的script腳本、再執行Host下的script腳本,最後再執行Location下的script腳本。在執行下一個腳本之前,先判斷剛剛執行的script腳本是否已經Reply了或者已經關閉當前HTTPMsg了。如果Reply了或關閉當前消息了,則直接返回,無需繼續解析並執行後續的script腳本程序。

5.4.5 rewrite語句

eJet系統中的URL重寫是通過Script腳本來實現的,分別借鑒了Apache和Nginx的成功經驗。

rewrite語句實現URL重寫功能,當客戶HTTP請求到達Web Server並創建HTTPMsg後,分別依次執行Listen、Host、Location下的script腳本程序,rewrite語句位於這些script腳本程序之中,rewrite語句會改變請求DocURL,一旦改變請求DocURL,在依次執行完這些script腳本程序之後,繼續基於新的DocURL去匹配新的Host、Location,並繼續依次執行該Host、Location下的script腳本程序,如此循環,是否繼續循環執行,取決於rewrite的flag標記。

rewrite基本語法如下:

rewrite regex replacement [flag];

執行該語句時是用regex的正則表達式去匹配DocURI,並將匹配到的DocURI替換成新的DocURI(replacement),如果有多個rewrite語句,則用新的DocURI,繼續執行下一條語句。

flag標記可以沿用Nginx設計的4個標記外,還增加了proxy或forward標記。其標記定義如下:

  • (a) last停止所有rewrite相關指令,使用新的URI進行Location匹配。
  • (b) break停止所有rewrite相關指令,不再繼續新的URI進行Location匹配,直接使用當前URI進行HTTP處理。
  • (c) redirect使用replacement中的URI,以302重定向返回給客戶端。
  • (d) permament使用replacement中的URI,以301重定向返回給客戶端。
  • (e) proxy | forward使用replacement中的URI,向Origin伺服器發起Proxy代理請求,並將Origin請求返回的響應結果返回給客戶端。

由於reply語句功能很強大,rewrite中的redirect和permament標記所定義和實現的功能,基本都在reply中實現了,這兩個標記其實沒有多大必要。

rewrite使用案例如下:

rewrite  ^/(.*) https://www.ezops.com/$1 permanent;

rewrite ^/search/(.*)$ /search.php?p=$1?;
請求的URL: http://xxxx.com/search/some-search-keywords
重寫後URL: http://xxxx.com/search.php?p=some-search-keywords

rewrite ^/user/([0-9]+)/(.+)$ /user.php?id=$1&name=$2?;
請求的URL: http://xxxx.com/user/47/dige
重寫後URL: http://xxxx.com/user.php?id=47&name=dige

rewrite ^/index.php/(.*)/(.*)/(.*)$ /index.php?p1=$1&p2=$2&p3=$3?;
請求的URL: http://xxxx.com/index.php/param1/param2/param3
重寫後URL: http://xxxx.com/index.php?p1=param1&p2=param2&p3=param3

rewrite ^/wiki/(.*)$ /wiki/index.php?title=$1?;
請求的URL:http://xxxx.com/wiki/some-keywords
重寫後URL:http://xxxx.com/wiki/index.php?title=some-keywords

rewrite ^/topic-([0-9]+)-([0-9]+)-(.*)\.html$ viewtopic.php?topic=$1&start=$2?;
請求的URL:http://xxxx.com/topic-1234-50-some-keywords.html
重寫後URL:http://xxxx.com/viewtopic.php?topic=1234&start=50

rewrite ^/([0-9]+)/.*$ /aticle.php?id=$1?;
請求的URL:http://xxxx.com/88/future
重寫後URL:http://xxxx.com/atricle.php?id=88

在eJet系統中,replacement後加?和不加?是有差別的,加?意味著query參數沒了,不加則會自動把源URL中的query串(?query)添加到替換後的URL中。

5.4.6 addReqHeader語句

特定情況下,需要對客戶端請求消息添加額外的請求頭,交給後續處理程序,如應用層處理程序、PHP程序、Proxy、Origin伺服器等等,來處理或使用到這些信息。譬如在作為HTTP Proxy功能時,發送給遠程Origin伺服器的請求中都需要添加兩個請求頭:一個是X-Real-IP,另一個是X-Forwarded-For,使用本語句可以很方便地實現了。

其基本語法為:

addReqHeader  <header name>  <header value>;

不能是空格字元,以字母開頭後跟字母、數字和下劃線_的字元串,可以用雙引號圈定起來; 是任意字元串,可以以引號包含起來,字元串中可包含變數。

使用案例如下:

if ($proxied) {
    addReqHeader X-Real-IP $remote_addr;
    addReqHeader X-Forwarded-For $remote_addr;
}

5.4.7 addResHeader語句

其基本語法為:

addResHeader  <header name>  <header value>;

5.4.8 delReqHeader語句

其基本語法為:

delReqHeader  <header name>;

5.4.9 delResHeader語句

其基本語法為:

delResHeader  <header name>;

5.4.10 try_files 語句

try_files 是一個重要的指令,建議位於Location、Host下面。使用該指令,依次測試列表中的文件是否存在,存在就將其設置DocURI,如不不存在,則將最後的URI設置為DocURI,或給客戶端返回狀態碼code。

try_files基本語法如下:

  try_files file ... uri;
或
  try_files file ... =code;

5.4.11 注釋語句

Script腳本程序中,如果一行除去空格字元外,以#號打頭,那麼當前行為注釋行,不被解釋器解釋執行;另外通過C語言代碼塊注釋標記 /* xxx */也被eJet系統採用。

5.5 Script腳本解釋器

eJet系統在處理HTTPMsg的實例化過程中,成功定位到HTTPHost、HTTPLoc等資源位置後,開始解釋執行這三個層級資源管理框架下的腳本程序,執行的順序依次為HTTPListen、HTTPHost、HTTPLoc下的Script腳本程序。

eJet系統的Script解釋器是逐行逐字進行掃描和識別,提取出Token後,分別匹配上述語句指令,再遞歸根據各個語句的掃描、識別和處理。這裡細節不做描述!

六. HTTPMsg的實例化流程

6.1 什麼是HTTPMsg實例化

eJet接受客戶端發起的TCP連接,接收該連接上的HTTP請求數據,解析HTTP請求頭,創建HTTPMsg來保存請求數據後,需要理解客戶端發送HTTP請求的目的,即確定HTTP請求的資源在哪個虛擬主機下的那個存儲位置,這個過程就是HTTPMsg的實例化流程。

如4.1中所述,eJet Web伺服器的資源管理結構分成三層:

  • HTTP監聽服務HTTPListen – 對應的是監聽本地IP地址和埠後的TCP連接
  • HTTP虛擬主機 – 對應的是請求主機名稱domain
  • HTTP資源位置HTTPLoc – 對應的是主機下的各個資源目錄

一個eJet Web伺服器可以監聽本機的一個或多個IP地址、一個或多個埠,等候不同客戶端的TCP連接請求,分別對應到多個監聽服務HTTPListen;在每一個監聽服務下,可以配置一個到多個HTTP虛擬主機HTTPHost,每個虛擬主機對應的是一個網站,管理不同類別的文件資源、網路資源等位置信息HTTPLoc;每個資源位置包含具體的文件存儲路徑,或網路地址等信息。

6.2 匹配虛擬主機和資源位置

HTTPMsg的實例化是以DocURI地址的信息來匹配虛擬主機和資源位置的,DocURI的預設地址是客戶端發起的HTTP請求URI。

HTTPMsg的實例化過程中改變的地址是DocURI,再次匹配虛擬主機和資源位置的也是DocURI的信息。對用戶請求URI進行資源定位過程中,由於補足資源目錄下的預設文件名、或使用rewrite、try_files等指令改變請求地址等操作,都會改變DocURI。

當eJet接受客戶端連接時創建HTTPCon,並綁定監聽服務HTTPListen實例,當接收到請求數據後,創建HTTPMsg,並將該連接上的HTTPListen實例傳遞到的HTTPMsg對象保存。

HTTPMsg根據DocURI的主機名稱,查找當前HTTPListen下的主機表,用Hashtab的精準匹配,找到HTTPHost虛擬主機實例對象。

綁定了HTTPHost後,使用DocURI的路徑名,分別採用路徑名進行精準匹配、前綴匹配、正則表達式匹配三種匹配規則,找到資源位置HTTPLoc,如果三種匹配都沒有匹配到,則採用預設的資源位置HTTPLoc。

6.3 執行腳本程序

HTTPMsg實例對象設置了三個層級的資源對象後,分別讀取各自的腳本程序,解釋並執行這些程序代碼。

執行腳本程序的優先順序是: 首先執行HTTPListen監聽服務下的腳本程序,其次執行HTTPHost虛擬主機下的腳本程序,最後執行HTTPLoc資源位置下的腳本程序。

腳本程序執行過程中,如果調用Reply指令直接給客戶端返迴響應,那麼終止當前所有的Script腳本運行,退出實例化過程,並完成響應的發送後,終止當前請求服務。

如果執行腳本時,調用了rewrite、try_files指令,並且重新改寫了DocURI,則會出現HTTPMsg實例化過程的嵌套執行,即重新執行4.7.2節描述的重新匹配虛擬主機和資源位置,並執行新的腳本程序。需注意的是,eJet系統中HTTPMsg實例化過程中,遞歸嵌套次數不超過16次。

腳本程序執行期間,可根據請求信息(如IP地址、終端類型、特定請求頭、請求目的URL等),利用各種腳本指令,動態設置或改變成員變數值或相關屬性。

七. TLS/SSL

7.1 TLS/SSL、OpenSSL介紹

SSL的全稱為Secure Socket Layer,即安全套接字層,是Netscape於90年代研發,位於TCP協議之上,利用PKI安全加密體系來實現認證和加密傳輸,SSL當前最新版本為3.0。

SSL協議分為兩層:

  • SSL記錄協議(SSL Record Protocol):在TCP之上,為高層協議提供數據封裝、壓縮、加密等功能,定義傳輸格式
  • SSL握手協議(SSL Handshake Protocol):在SSL記錄協議之上,對通訊雙方進行身份認證、協商加密演算法、交換密鑰等。

TLS的全稱是Transport Layer Security,即傳輸層安全協議,當前最新版為TLS 1.3,是IETF(Internet Engineering Task Force,互聯網工程任務組)制定的一種新的協議,建立在SSL 3.0協議規範之上,是SSL 3.0的後續版本。

同樣TLS協議由兩層組成:

  • TLS 記錄協議(TLS Record)
  • TLS 握手協議(TLS Handshake)

SSL/TLS協議提供的服務主要有:

  • 認證。認證客戶端和伺服器,確保數據發送到正確的客戶端和伺服器;
  • 加密。加密數據以防止數據中途被竊取;
  • 一致性。維護數據的完整性,確保數據在傳輸過程中不被改變

實現TLS/SSL協議的開源軟體是OpenSSL,是澳洲人Eric Young、Tim Hudson於90年代開源的SSLeay基礎上演變過來的,採用標準C語言編寫,廣泛用於使用加密和安全的環境。

7.2 eJet集成OpenSSL

客戶端發起HTTP請求,如果scheme是https,則需要建立SSL/TLS over TCP的安全連接到eJet伺服器系統,

eJet系統作為伺服器端接收客戶端HTTP請求和作為客戶端向Origin伺服器發送HTTP請求時,都會使用到SSL連接,調用OpenSSL的方法有一些差別。

eJet作為伺服器端使用SSL時,使用OpenSSL的基本流程共有九個步驟。

  • 初始化OpenSSL庫

系統初始化時,首先調用SSL_library_init初始化OpenSSL庫,調用SSL_add_ssl_algorithms()添加SSL預設演算法,載入錯誤信息定義串,如果根據SSL連接實例能獲取到HTTPCon對象,需創建SSL連接索引,並利用該連接索引,將SSL Socket連接實例和HTTPCon對象關聯。

  • 配置證書和私鑰

在系統配置Listen下,設置HTTPListen監聽服務下是否支持SSL,及預設的證書、私鑰和CA證書,並在每個域名對應的虛擬主機下,配置啟用SSL所需的伺服器證書、私鑰和CA證書。示例如下:

listen = {
    local ip = *; # any IP address
    port = 443;
    forward proxy = off;

    ssl = on;
    ssl certificate = cert.pem;
    ssl private key = cert.key;
    ssl ca certificate = cacert.pem;

    host = {
        host name = www.yunzhai.cn
        type = server;

        ssl certificate = yzcert.pem;
        ssl private key = yzcert.key;
        ssl ca certificate = yzcacert.pem;
...... 
    }
......
}
  • 初始化SSL_Ctx

在系統初始化最後,開始啟動HTTPListen服務前,載入監聽服務和其下各虛擬主機時,分別根據證書、私鑰和CA證書,創建HTTPListen的預設SSL_Ctx實例,或創建各虛擬主機HTTPHost下的SSL_Ctx。

創建SSL_Ctx的過程先調用SSL_CTX_new創建實例,隨後載入證書和私鑰,並校驗證書和私鑰是否匹配,如果存在CA證書,還需載入CA證書。

最後,啟用SNI(Server Name Indication)機制,設置一個回調函數來處理不同域名對應不同的證書和私鑰,在SSL啟動Handshake時,先發送ClientHello請求,其中攜帶了當前連接對應的域名,伺服器端收到ClientHello時,會以域名為參數,調用回調函數,選擇與之相對應的SSL_Ctx。

  • 接受連接並創建SSL Socket

eJet伺服器收到客戶端的TCP連接請求時,創建HTTPCon實例,保存連接信息後,HTTPCon需關聯HTTPListen,並根據HTTPListen中的ssl_link配置選項,來創建SSL Socket連接實例,其過程主要包括:使用SSL_new創建SSL實例,調用SSL_set_fd設置當前連接的文件描述符,調用SSL_set_ex_data將當前SSL對象和HTTPCon實例對象關聯起來。最後,設置當前HTTPCon的ssl_handshaked狀態為未建立握手狀態。

  • 根據域名選擇對應的SSL_Ctx

一個監聽埠下,可以有多個證書,用於不同的主機名,客戶端HTTPS請求到達時,需要使用合適的證書來完成後續SSL握手和加密通信,這是採用TLS規範的SNI機制來實現的。

在創建SSL_Ctx時,需設置多域名選擇的回調函數,SSL握手開始時的ClientHello請求攜帶請求的域名名稱,回調函數根據SSL_get_servername獲取到域名名稱,在當前HTTPListen下查找該名稱對應的虛擬主機HTTPHost,並調用SSL_set_SSL_CTX,將當前HTTPCon中的SSL連接的SSL_Ctx上下文實例設置為該HTTPHost下的sslctx,即可實現證書選擇和切換操作。

  • SSL握手

對於接受客戶端請求的情形,SSL握手過程是在SSL_accept中實現的,由於網路抖動等因素,握手過程中往來的數據需要通過多次讀寫事件來驅動完成,在http_pump處理IOE_READ和IOE_WRITE時,需要判斷當前HTTPCon的ssl_handshaked狀態,如果沒有握手成功,則響應這兩個ePump事件時,都需要調用SSL_accept。

eJet還需要根據SSL_accept的錯誤狀態碼,來添加對當前TCP連接的讀就緒或寫就緒監聽處理,並在http_pump中處理讀寫事件。這是非阻塞通信下建立SSL連接必須要注意的步驟。

如果SSL_accept返回成功,則將HTTPCon的ssl_handshaked設置為已完成握手狀態,並調用http_cli_recv來接收SSL上的數據。

  • 在SSL連接上接收數據

eJet系統封裝了一個針對HTTPCon的數據接收函數,同時兼容有SSL連接和沒有SSL連接這兩種情況,函數定義如下:

int http_con_read (void * vcon, frame_p frm, int * num, int * err);

ePump框架在當前連接有數據可讀時,回調http_pump處理IOE_READ事件,如果完成了握手過程,則調用這個函數來讀取數據。如果是SSL連接,該函數調用SSL_read來讀取數據,如果讀取成功,返回的是解密完成後的數據長度,並將解密後的數據存入緩衝區,注意:這裡有兩次拷貝(從內核拷貝到臨時緩衝區,再從臨時緩衝區拷貝到目標緩衝區),需要優化。

如果SSL_read返回0,則當前連接出現故障,需關閉連接。如果返回值小於0,則調用SSL_get_error來處理錯誤碼,對於SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE兩種情況需要調用ePump介面設置添加讀寫就緒監聽。

  • 在SSL連接上發送數據

eJet系統中發送數據流程一般是用chunk_t數據結構管理數據,調用writev和sendfile將數據通過網路發送給對方,在SSL連接情況下,eJet系統同樣封裝了兩個類似的函數:

int http_con_writev (void * vcon, void * piov, int iovcnt, int * num, int * err);
int http_con_sendfile (void * vcon, int filefd, int64 pos, int64 size, int * num, int * err);

這兩個函數同時兼容有SSL連接和沒有SSL連接這兩種情況,在沒有SSL連接情況下,直接調用tcp_writev和tcp_sendfile。

在有SSL連接情況下,調用SSL_write函數,要寫入的明文數據調用SSL_write後被加密並傳輸給對方。如果發送成功返回的是寫入數據的長度,如果返回0,則當前連接出現故障,需關閉連接。如果返回值小於0,則需要調用SSL_get_error來處理錯誤碼,對於SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE兩種情況需要調用ePump介面設置添加讀寫就緒監聽。

  • 關閉SSL連接

在處理完成數據讀寫操作,或者網路錯誤等情況,當前HTTPCon會被關閉,如果是SSL連接則需釋放SSL實例,分別調用SSL_shutdown和SSL_free來完成資源釋放。

以上九個步驟是eJet系統作為HTTP伺服器時使用SSL連接來傳輸數據的基本流程,對於eJet系統作為HTTP客戶端情形,過程基本類似,這裡不再贅述。

八. Chunk傳輸編碼解析

HTTP 1.1協議增加了Transfer-Encoding: chunked的頭類型,表示消息體的內容長度不能確定,需採用分塊傳輸編碼方式,將消息體發送給對方。

Chunked Transfer Coding分塊傳輸編碼是由多個Chunk塊組成,每個Chunk塊包括兩部分,十六進位的分塊數據長度加上可選的分塊擴展加上\r\n、實際分塊數據加上\r\n,分塊傳輸編碼的結尾是以分塊數據長度為0的分塊組成。

分塊傳輸數據格式如下:

chunked body = chunk-size[; chunk-ext-nanme [= chunk-ext-value]]\r\n
               ...
               0\r\n
               [footer]
               \r\n

chunk size是以16進位表示的長度,footer一般是以\r\n結尾的entity-header,一般都忽略掉。

eJet系統使用HTTPChunk數據結構來解析chunk分塊傳輸編碼的消息體數據,使用chunk_t數據結構來打包分塊傳輸編碼。HTTPChunk數據結構包含chunk_t成員實例,用於存儲解析成功的Chunk數據塊,每一個Chunk數據塊解析狀態和信息用ChunkItem來存儲管理,HTTPChunk中用item_list來管理多個ChunkItem。

採用chunk分塊傳輸編碼的消息體,實際情況是一邊傳輸一邊解析,解析過程要充分考慮到當前接收緩衝區內容的不完整性,這是由HTTPChunk里的http_chunk_add_bufptr來實現的,函數定義如下:

int http_chunk_add_bufptr (void * vchk, void * vbgn, int len, int * rmlen);

vbgn和len指向需解析的消息體數據,rmlen是解析成功後實際用於chunk分塊傳輸編碼的位元組數量。

eJet在遇到chunk分塊傳輸編碼的消息體時,每次收到讀事件,就將數據讀取到緩衝區,將緩衝區所有數據交給這個解析函數解析處理,返回的rmlen值是被解析和處理的位元組數,將處理完的數據從緩衝區移除掉。通過http_chunk_gotall來判斷是否接收到全部chunk分塊傳輸編碼的所有數據,如果沒有,循環地用新接收的數據調用該函數來解析和處理,直至成功接收完畢。

九. 反向代理和正向代理

9.1 判斷是否為代理請求

反向代理是將不同的Origin伺服器代理給客戶端,客戶端不做任何代理配置發起正常的HTTP請求到反向代理伺服器,反向代理伺服器根據配置的路徑規則,代理訪問不同的Origin伺服器並將響應結果返回給客戶端,讓客戶端認為反向代理伺服器就是其訪問的Origin伺服器。

正向代理需要求客戶端設置正向代理伺服器地址,明確給定Origin伺服器地址,要求正向代理伺服器想給定的Origin伺服器轉發請求和響應。

上面描述的反向代理伺服器,在這裡就是eJet Web伺服器,除了充當Web伺服器功能外,還可以充當正向代理伺服器和反向代理伺服器。

eJet系統在HTTPMsg實例化完成後,首先要檢查的是當前請求是否為Proxy代理請求:

  • 是否在rewrite時啟動forward到一個新的Origin伺服器的動作,如果是則代理轉發到新的URL
  • 是否為正向代理,正向代理的請求地址request URI是絕對URI,如果是則代理轉發到絕對URI上
  • 判斷當前資源位置HTTPLoc是否配置了反向代理,以及反向代理指向的Origin伺服器,如果是,根據規則生成訪問Origin伺服器的URL地址

以上三種情況中,第一種和第三種為反向代理,第二種為正向代理,對應的配置樣例如下:

location = { #rewrite ... forward
    type = server;
    path = ['/5g/', '^~' ];
    script = {
        rewrite ^/5g/.*tpl$ http://temple.ejetsrv.com/getres.php forword;
    }
}

# HTTP請求行是絕對URI地址
GET http://cdn.ejetsrv.com/view/23C87F23D909B47E2187A0DB83AF07D3 HTTP/1.1
....

location = { # 反向代理配置
    path = [ '^/view/([0-9A-Fa-f]{32})$', '~*' ];
    type = proxy;
    passurl = http://cdn.ejetsrv.com/view/$1;
......
}

無論是正向代理,還是反向代理,最後轉發請求的操作流程基本類似,即需明確指向新Origin伺服器的URL地址,作為下一步轉發地址,主動建立到Origin伺服器的HTTPCon連接,組裝新的HTTPMsg請求,發送請求並等候響應,將響應結果轉發到源HTTPMsg中,發送給客戶端。

如果是代理請求,包括正向代理或反向代理,eJet需要做Proxy代理轉發處理。

9.2 代理請求的實時轉發

需要重點介紹的是實時轉發源請求到Origin伺服器的流程。代理轉發時先創建一個代理轉發的HTTPMsg實例,將源請求HTTPMsg實例的請求數據複製到代理請求HTTPMsg中,如果HTTP請求含有請求消息體時,代理轉發流程有兩種實現方式:

  • 一種方式是存儲轉發,即接收完所有的HTTP請求消息體後,再複製到代理轉發HTTPMsg中,最後發送出去
  • 另一種方式實時轉發,即接收一部分消息體就發送一部分消息體,直到全部發送完畢

為了確保代理轉發效率和降低存儲消耗,eJet系統採用實時轉發模式。

源請求的消息體內容保存在HTTPCon的rcvstream中,響應IOE_READ事件時將網路內容讀取到該緩衝區後,就要調用http_proxy_srv_send來實時轉發。轉發的數據包括代理請求頭、上次未發送成功的消息體、及當期位於HTTPCon緩衝區中的rcvstream,嚴格按照接收的順序來發送。

每次未發送成功的消息體,將會從HTTPCon的rcvstream中拷貝出來,轉存到代理請求HTTPMsg中的req_body_stream中,作為臨時緩衝區保存累次未能發送的消息體。當從源HTTPCon中接收到新數據、或到Origin伺服器的目的HTTPCon中可寫就緒時,都會啟動http_proxy_srv_send的實時發送流程,而優先發送的消息體就是代理請求中req_body_stream中的內容。

源請求的消息體有三種情況:

  • 沒有消息體內容
  • 存在以Content-Length來標識大小的消息體內容
  • 存在以Transfer-Encoding標識分塊傳輸編碼的消息體內容

實時轉發需要處理這三種情況,最終通過http_con_writev來發送給對方。發送不成功的剩餘內容,需要從源HTTPCon中拷貝到代理請求HTTPMsg中的req_body_stream中。

實時轉發最大問題是擁塞問題,即源HTTPCon上的請求數據發送速度很快,但到Origin伺服器的目的HTTPCon連接的發送速度比較慢,導致大量的數據會堆積到代理消息HTTPMsg中req_body_stream中,消耗大量內存,嚴重時會導致內存消耗過大系統崩潰。

代理消息實時轉發模式的擁塞問題根源在於兩條線路傳輸速度不對等導致,只要發送側速度大於接收側速度,擁塞問題就會出現。解決擁塞問題需從源頭來考慮,判斷是否擁塞的標準是堆積的內存緩衝區超過一定的閾值,一旦內存堆積超過閾值,就斷定為擁塞,需限制客戶端繼續發送任何內容,直到解除擁塞後繼續發送。

9.3 代理響應的實時轉發

代理請求轉發給Origin伺服器後,會返迴響應消息,包括響應頭和響應體,eJet處理響應頭的接收和處理編碼。

和HTTP請求消息的實時轉發類似,代理消息的響應也需要實時轉發給客戶端。

根據代理HTTPMsg內部成員proxiedl連判斷當前消息是否為代理,對Origin返回的響應頭信息進行預處理:

  • 如果是301/302跳轉,當前代理消息是反向代理,並且系統允許自動重定向,則需重新發送重定向請求;
  • 如果需要緩存到本地存儲系統,採用緩存處理流程,見4.20章節
  • 其他情形就按照代理響應來處理

複製所有的響應狀態碼和響應頭到源HTTPMsg中,並將響應HTTPCon的接收緩衝區rcvstream數據實時轉發到源HTTPCon中,同樣地,HTTPCon中沒有發送不成功的數據,轉存到源HTTPMsg中的res_body_stream中臨時緩存起來。每次當源HTTPCon可寫就緒、或代理HTTPCon有數據可讀並讀取成功後,都會調用http_proxy_cli_send,優先發送的是堆積在res_body_stream中的數據。

其他後續流程類似請求消息的實時轉發。

十. FastCGI機制和啟動PHP的流程

10.1 FastCGI基本信息

FastCGI是CGI(Common Gateway Interface)的開放式擴展規範,其技術規範見網址 http://www.mit.edu/~yandros/doc/specs/fcgi-spec.html

對靜態HTML頁面中嵌入動態腳本程序的內容,如PHP、ASP等,需要由特定的腳本解釋器來解釋運行,並動態生成新的頁面,這個過程需要eJet Web伺服器和腳本程序解釋器之間有一個數據交互介面,這個介面就是CGI介面,考慮到性能局限,早期的獨立進程模式的CGI介面發展成FastCGI介面規範。習慣地,我們把解釋器稱之為CGI伺服器。

使用CGI介面規範的頁面腳本程序可以使用任何支持標準輸入STDIN、標準輸出STDOUT、環境變數的編程語言來編寫,如PHP、Perl、Python、TCL等。在傳統CGI規範的fork-and-execute模式中,Web伺服器會為每個HTTP請求,創建一個新進程、解釋執行、返迴響應、銷毀進程,這是個很重的工作流程。

FastCGI對CGI這種重模式進行了簡化,腳本解釋器和Web伺服器之間的交互,通過Unix Socket或TCP協議來實現,Web伺服器收到需要解釋執行的HTTP請求時,建立並維持通信連接到CGI伺服器,按照FastCGI通信規範發送請求,並接收響應,這個流程相比CGI模式,大大提升了性能和並發處理能力。

PHP解釋器名稱為php-fpm(php FastCGI Processor Manager),作為FastCGI通信伺服器監聽來自Web伺服器的連接請求,並接收連接上的數據,進行解析、解釋執行後,返迴響應給Web伺服器端。php-fpm的配置項中,啟動監聽服務:

; The address on which to accept FastCGI requests.
; Valid syntaxes are:
;   'ip.add.re.ss:port'    - to listen on a TCP socket to a specific IPv4 address on
;                            a specific port;
;   '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
;                            a specific port;
;   'port'                 - to listen on a TCP socket to all addresses
;                            (IPv6 and IPv4-mapped) on a specific port;
;   '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
listen = /run/php-fpm/www.sock
;listen = 9000

10.2 eJet如何啟用FastCGI

eJet收到客戶端的HTTP請求並創建HTTPMsg和完成HTTPMsg實例化後,根據資源位置HTTPLoc是否將資源類型設置為FastCGI、並且設置了指向CGI伺服器地址的passurl,如果都設置這兩個參數,則當前請求會被當做FastCGI請求轉發給CGI伺服器。

啟用FastCGI的參數配置如下:

location = {
    type = fastcgi;
    path = [ "\.(php|php?)$", '~*'];

    passurl = fastcgi://127.0.0.1:9000;
    #passurl = unix:/run/php-fpm/www.sock;

    index = [ index.php ];
    root = /data/wwwroot/php;
}

只要是請求DocURL中路徑名稱是以.php或.php5等結尾,當前請求都會被FastCGI轉發。

在獲取轉發URL地址時,是複製配置中的passurl地址,即CGI伺服器地址,不能把HTTP請求中的路徑和query參數信息添加在這個轉發URL後面。轉發地址有兩種形態:

  • 採用TCP協議的CGI伺服器地址,以fastcgi://打頭,後跟IP地址和埠,或域名和埠;
  • 採用Unix Socket的CGI伺服器地址,以unix:打頭,後跟Unix Socket的路徑文件名。

passurl地址指向CGI伺服器,eJet伺服器可以支持很多個CGI伺服器。

eJet獲取到FastCGI轉發地址後,根據該地址創建或打開CGI伺服器FcgiSrv對象實例,建立TCP連接或Unix Socket連接到該伺服器的FcgiCon實例,為當前HTTP請求創建FcgiMsg消息實例,將HTTP請求信息按照FastCGI規範封裝到FcgiMsg中,並啟動發送流程,將請求發送到CGI伺服器。

10.3 FastCGI的通信規範

FastCGI通信依賴於C/S模式的可靠的流式的連接,協議定義了十種通信PDU(Protocol Data Unit)類型,每個PDU都由兩部分組成:一部分是FastCGI Header頭部,另一部分是FastCGI消息體,FastCGI的PDU是嚴格8位元組對齊,PDU總長度不足8的倍數,需要添加Padding補齊8位元組對齊。FastCGI的PDU頭格式如下:

typedef struct fastcgi_header {
    uint8           version;
    uint8           type;
    uint16          reqid;
    uint16          contlen;
    uint8           padding;
    uint8           reserved;
} FcgiHeader, fcgi_header_t;

上面定義的協議頭格式中,version版本號1個位元組,預設值為1,type為PDU類型1個位元組,共計定義了10種類型,reqid為PDU的序號,兩位元組BigEndian整數,contlen是PDU消息體的內容長度,兩位元組BigEndian整數,1位元組的padding是PDU消息體不是8位元組的倍數時,需要補齊8位元組對齊所填充的位元組數,保留1位元組。

其中PDU類型共有十種,分別定義如下:

/* Values for type component of FCGI_Header */
#define FCGI_BEGIN_REQUEST       1
#define FCGI_ABORT_REQUEST       2
#define FCGI_END_REQUEST         3
#define FCGI_PARAMS              4
#define FCGI_STDIN               5
#define FCGI_STDOUT              6
#define FCGI_STDERR              7
#define FCGI_DATA                8
#define FCGI_GET_VALUES          9
#define FCGI_GET_VALUES_RESULT  10
#define FCGI_UNKNOWN_TYPE       11
#define FCGI_MAXTYPE            (FCGI_UNKNOWN_TYPE)

其中從Web伺服器發送給CGI伺服器的PDU類型為:BEGIN_REQUEST、ABORT_REQUEST、PARAMS、STDIN、GET_VALUES等,從CGI伺服器返回給Web伺服器的PDU類型為:END_REQUEST、STDOUT、STDERR、GET_VALUES_RESULT等。

根據PDU type值,PDU消息體格式也都不一樣,分別定義為:

typedef struct {
    uint8  roleB1;
    uint8  roleB0;
    uint8  flags;
    uint8  reserved[5];
} FCGI_BeginRequest;

/* Values for role component of FCGI_BeginRequest */
#define FCGI_RESPONDER           1
#define FCGI_AUTHORIZER          2
#define FCGI_FILTER              3

BEGIN_REQUEST是發送數據到CGI伺服器時,第一個必須發送的PDU。其中的角色role是兩個位元組組成,高位在前、低位在後,一般情況role值為RESPONSER,即要求CGI伺服器充當Responder來處理後續的PARAMS和STDIN請求數據。欄位flags是指當前連接keep-alive還是返回數據後立即關閉。

第二個需發送到CGI伺服器的PDU是PARAMS,其格式是由FcgiHeader加上帶有長度的name/value對組成,PDU消息體格式如下:

typedef struct {
    uint8    namelen;    //namelen < 0x80
    uint32   lnamelen;   //namelen >= 0x80
    uint8    valuelen;   //valuelen < 0x80
    uint32   lvaluelen;  //valuelen >= 0x80
    uint8  * name;       //[namelen];
    uint8  * value;      //[valuelen];
} FCGI_PARAMS;

FastCGI中的PARAMS PDU是將HTTP請求頭信息和預定義的Key-Value頭信息發送給CGI伺服器,這些信息都是Key-Value鍵值對。如果key或value的數據長度在128位元組以內,其長度欄位只需一個位元組即可,如果大於或等於128位元組,則其長度欄位必須用BigEndian格式的4位元組。在對HTTP請求頭和預定義頭Key-Value對信息封裝編碼成PARAMS PDU時,每個Header欄位的編碼格式為:先是Header的name長度,再是value長度,隨後是name長度的name數據內容,最後是value長度的value數據內容。

1位元組namelen或4位元組namelen + 1位元組valuelen或4位元組valuelen + name + value

所有頭信息按照上述編碼格式打包完成後,總長度如果不是8的倍數,計算需不全8位元組對齊的padding數量,將這些數據填充到FcgiHeader中。

第三個要發送到CGI伺服器的PDU是STDIN,STDIN PDU是由FcgiHeader加上實際數據組成。注意的是STDIN數據長度不能大於65535,如果HTTP請求中消息體數據大於65535,需要對消息體拆分成多個STDIN包,使得每個STDIN PDU的消息體長度都在65536位元組以下。需要特別注意的是,所有數據內容拆分成多個STDIN PDU完成後,最後還需要添加一個消息體長度為0的STDIN PDU,表示所有的STDIN數據發送完畢。

當eJet系統收到HTTP請求並需要FastCGI轉發是,按照以上三類數據包協議格式,將HTTP請求打包封裝,並發送成功後,就等等CGI伺服器的處理和響應了。

CGI伺服器返回的PDU一般如下:

如果出現請求格式錯誤或其他錯誤,會返回STDERR數據,其消息體是錯誤內容,將錯誤內容取出來可以直接返回給客戶端。

正常情況下,CGI伺服器會返回一個到多個STDOUT PDU,STDOUT的消息體是實際的數據內容,最大長度小於65536。需要將這些STDOUT的內容整合在一起,作為HTTP響應內容。需注意的是STDOUT內容中,也包含部分HTTP響應頭信息,其格式遵循HTTP規範,每個響應頭有key-value對構成,以\r\n換行符結束,響應頭和響應體之間相隔一個空行\r\n。

全部STDOUT數據結束後,緊接著返回的是END_REQUEST PDU,其格式是8位元組的FcgiHeader,加上8位元組的消息體,其消息體定義如下:

typedef struct {
    uint32     app_status;
    uint8      protocol_status;
    uint8      reserved[3];
} FCGI_EndRequest;

/* Values for protocolStatus component of FCGI_EndRequest */
#define FCGI_REQUEST_COMPLETE    0
#define FCGI_CANT_MPX_CONN       1
#define FCGI_OVERLOADED          2
#define FCGI_UNKNOWN_ROLE        3

eJet伺服器收到END_REQUEST時,就表示CGI伺服器已經返回全部的響應數據了,將這些數據發送給客戶端,即可結束當前處理。

10.4 FastCGI消息的實時轉發

eJet系統將HTTP請求實時轉發給CGI伺服器,基本過程跟Proxy代理轉發類似,包括實時轉發、流量擁塞控制等。

其中在接收CGI伺服器的響應數據時,需要解析以流式返回的STDOUT PDU的數據,但響應數據的總長度並未返回,eJet對這些響應數據的實時轉發是採用Transfer-Encoding分塊傳輸編碼模式。為了減少響應數據的多次拷貝,FcgiCon中每次數據讀就緒時,存入rcvstream緩衝區的數據,連同rcvstream一起移入到發起HTTP請求的源HTTPMsg內的res_rcvs_list列表中,並將解析成功的內容指針存入到res_body_chunk里,類似客戶端訪問本地文件一樣,通過http_cli_send發送給客戶端。

十一. HTTP Cache系統

11.1 HTTP Cache功能設置

HTTP Cache是指Web伺服器充當HTTP Proxy代理伺服器(包括正向代理和反向代理),通過HTTP協議向Origin伺服器下載文件,然後轉發給客戶端,這些文件在轉發給客戶端的同時,緩存在代理伺服器的本地存儲中,下次再有相同請求時,根據相關緩存策略決定本地文件是否被命中,如果命中,則該請求無需向Origin伺服器請求下載,直接將緩存中命中的文件讀取出來返回給客戶端,從而節省網路開銷。

在配置文件中配置正向代理或反向代理的地方,都可以開啟cache功能,並基於配置腳本動態設置緩存文件名等緩存選項。

location = {
    path = [ '^/view/([0-9A-Fa-f]{32})$', '~*' ];
    type = proxy;
    passurl = http://cdn.yunzhai.cn/view/$1;

    # 反向代理配置緩存選項
    root = /opt/cache/;
    cache = on;
    cache file = /opt/cache/${request_header[host]}/view/$1;
}

send request = {
    max header size = 32K;

    /* 正向代理配置的緩存選項 */
    root = /opt/cache/fwpxy;
    cache = on;
    cache file = <script>
                   if ($req_file_only)
                       return "${host_name}_${server_port}${req_path_only}${req_file_only}";
                   else if ($index)
                       return "${host_name}_${server_port}${req_path_only}${index}";
                   else
                       return "${host_name}_${server_port}${req_path_only}index.html";
                 </script>;
}

在配置中啟動了緩存功能後,還要根據Origin伺服器返回的響應頭指定的緩存策略,來決定當前下載文件是否保存在本地、緩存文件保存多長時間等。HTTP響應頭中有幾個頭是負責緩存策略的:

Expires: Wed, 21 Oct 2020 07:28:00 GMT            (Response Header)
Cache-Control: max-age=73202                      (Response Header)
Cache-Control: public, max-age=73202              (Response Header)

Last-Modified: Mon, 18 Dec 2019 12:35:00 GMT      (Response Header)
If-Modified-Since: Fri, 05 Jul 2019 02:14:23 GMT  (Request Header)
 
ETag: 627Af087-27C8-32A9E7B10F                    (Response Header)
If-None-Match: 627Af087-27C8-32A9E7B10F           (Request Header)

Proxy代理伺服器需要處理Origin伺服器返回的響應頭,主要是Expires、Cache-Control、Last-Modified、ETag等。根據Cache-Control的緩存策略決定當前文件是否緩存:如果是no-cache或no-store,或者設定了max-age=0,或者設定了must-revalidate等都不能將當前文件保存到緩存文件中。如果設置了max-age大於0則根據max-age值、Expires值、Last-Modified值、ETag值來判斷下次請求是否使用該緩存文件。

11.2 eJet系統Cache存儲架構

eJet系統是否啟動緩存由配置信息來設定。如果是反向代理,HTTP請求對應的HTTPLoc下的反向代理開關cache是否開啟,即cache=on,cache file項是否設置,來決定是否啟動緩存功能;如果是正向代理,在send request選項中,是否啟動cache,以及cache file命名規則是否設置,決定是否啟動緩存管理。

啟動了cache功能,還需要根據當前請求轉發給Origin後,返回的響應頭中,是否有Cache管理的頭信息,來確定當前返回的響應體是否緩存,以及確定當前緩存的相關信息。

緩存的Raw文件內容存儲在上述配置中以cache file命名的文件中,當文件所有內容全都下載並存儲起來前,文件名後需要增加擴展名.tmp,以表示當前存儲文件正在下載中,還不是一個完整的文件,但已經緩存的內容則可以被命中使用。

cache管理信息則存儲在緩存信息管理文件(Cache Information Management File)中,簡稱為CacheInfo文件,CacheInfo文件的存儲位置在Raw緩存文件所在目錄下建立一個隱藏目錄.cacheinfo,CacheInfo文件就存放該隱藏目錄下,CacheInfo文件名是在Raw存儲文件後增加後綴.cacinf,譬如Raw緩存文件為foo.jpg,則緩存信息管理文件路徑為: .cacheinfo/foo.jpg.cacinf

CacheInfo文件的結構包括三部分:Cache頭信息(96位元組)、Raw存儲碎片管理信息。Cache頭信息是固定的96位元組,其結構如下:

/* 96 bytes header of cache information file */
typedef struct cache_info_s {
    char         * cache_file;
    void         * hcache;
    char         * info_file;
    void         * hinfo;

    uint8          initialized;
 
    uint32         mimeid;
    uint8          body_flag;
    int            header_length;
    int64          body_length;
    int64          body_rcvlen;
 
    /* Cache-Control: max-age=0, private, must-revalidate
       Cache-Control: max-age=7200, public
       Cache-Control: no-cache */
    uint8          directive;     //0-max-age  1-no cache  2-no store
    uint8          revalidate;    //0-none  1-must-revalidate
    uint8          pubattr;       //0-unknonw  1-public  2-private(only browser cache)
 
    time_t         ctime;
    time_t         expire;
    int            maxage;
    time_t         mtime;
    char           etag[36];
 
    FragPack     * frag;
 
} CacheInfo;

在頭信息之後存放的是存儲內容碎片管理信息,每個碎片單元為8位元組:

typedef struct frag_pack {
    int64    offset;
    int64    length;
} FragPack;

內存中採用動態有序數組來管理每一個碎片塊,相鄰塊就需要合併成一個塊,完整文件只有一個塊。將這些碎片塊信息按照8位元組順序存儲在這個區域中。每當文件有新內容寫入時,內存碎片塊數組要完成合併等更新,並將最新結果更新到這個區域。碎片塊信息管理的是Raw存儲文件中從Origin伺服器下載並實際存儲的數據存儲狀態,每塊是以偏移量和長度來唯一標識,相鄰的碎片塊合併,完整文件只有一個碎片塊。

11.3 eJet系統緩存處理流程

eJet系統作為正向代理或反向代理伺服器,實現邊下載邊緩存、完整緩存時無需代理轉發直接返回緩存內容給客戶端等功能,可以實現對大大小小的Origin文件的實時緩存功能,包括碎片存儲、隨機存儲等。

(1)全局管理CacheInfo對象

系統維護一個全局的CacheInfo對象哈希表,以Raw緩存文件名作為唯一標識和索引,如果存在多個用戶請求同一個需要緩存的Origin文件時,只打開或創建一個CacheInfo對象,該對象成員由互斥鎖來保護。而每個對同一Origin文件的HTTP請求,請求位置、偏移量、讀寫Raw緩存文件的句柄等都保存在各自的HTTPMsg實例對象中。

CacheInfo對象是管理和存放Raw緩存文件的各項元信息,對外暴露的主要介面是: cache_info_open, cache_info_create, cache_info_close, cache_info_add_frag等

用戶發起Origin文件請求時,先調用cache_info_open打開CacheInfo對象,如果不存在,則在收到Origin的成功響應後,調用cache_info_create創建CacheInfo對象。每次調用cache_info_open時,如果CacheInfo對象已經在內存中,則將count計數加1,只有count計數為0時才可以刪除釋放CacheInfo對象。當HTTPMsg成功返回給用戶後,需要關閉CacheInfo對象,調用cache_info_close,首先將count計數減1,如果count大於0,直接返回不做資源釋放。

(2)向Origin伺服器轉發Proxy代理請求

eJet收到HTTP客戶請求時,如果是Proxy請求,則調用http_proxy_cache_open檢測並打開緩存,先根據請求URL對應的HTTPLoc配置信息或正向代理對應的send request配置信息,決定當前代理模式下的HTTP請求是否啟用了Cache功能,如果啟用了Cache功能,並且Cache File變數設置了正確的Raw緩存文件名,將該緩存文件名保存在HTTPMsg對象的res_file_name中。

檢查該緩存文件是否存在,如果存在則直接將該緩存文件返回給客戶端即可。註:在沒有收到全部位元組數據之前Raw緩存文件名是實際緩存文件後加.tmp做擴展名。

如果該文件不存在,以該緩存文件名為參數,調用cache_info_open打開CacheInfo對象,如果不存在緩存信息對象CacheInfo,則返回並直接將客戶端請求轉發到Origin伺服器。

如果存在CacheInfo對象,也就是存在以.tmp為擴展名的Raw緩存文件和以.cacinf為擴展名的緩存信息文件,則判斷當前請求的內容(Range規範指定的請求區域)是否全部包含在Raw緩存文件中,如果包含了,則直接將該部分內容返回給客戶端,無需向Origin伺服器發送HTTP下載請求;如果不包含,則需要向Origin伺服器發送請求,但本地緩存中已經有的內容不必重新請求,而是將客戶端請求的區域(Range規範指定的範圍)中尚未緩存到本地的起始位置和長度計算出來,組成新的Range規範,向Origin發送HTTP請求。

(3)處理Origin伺服器返回的響應頭

當HTTP請求轉發到Origin伺服器並返迴響應後,正常情況是將Proxy代理請求HTTPMsg中所有的響應頭全部複製一份到源請求HTTPMsg的響應頭中,包括狀態碼也複製過去。

但對於啟用了Cache=on並且CacheInfo也已經打開的情況,則需要修正源請求HTTPMsg的響應頭,即調用http_cache_response_header來完成:刪除掉不必要的響應頭,修正HTTP響應體的內容傳輸格式,即選擇Content-Length方式還是Transfer-Encoding: chunked方式,並將狀態碼修改成206還是200,修改Content-Range的值內容,因為源請求的Range和向Origin伺服器發起的Proxy代理請求的Range不一定是一致的。並根據CacheInfo信息決定是否增加Expires和Cache-Control等響應頭,等等

隨後,對Origin伺服器返回的HTTP響應頭進行解析,調用http_proxy_cache_parse來完成:分別解析Expires、ETag、Last-Modified、Cache-Control等響應頭,基於這些響應頭信息,再次判斷當前響應內容是否需要緩存Cache=on。

如果不需要緩存:則將Cache設置為off,並關閉已經打開的CacheInfo(甚至刪除掉CacheInfo文件和Raw緩存文件),最主要的是檢查源請求的Range範圍和Proxy代理請求的Range範圍是否一致,如果不一致,則需要重新將源HTTP請求原樣再發送一次,並清除當前Proxy代理請求的所有信息。由於將源HTTP請求HTTPMsg中Cache設置為off了,後續重新發送的Proxy代理請求將不啟用緩存功能,直接使用實時轉發模式。如果兩個請求的Range一致,則直接將當前代理請求的響應體內容採用實時轉發模式,發送給客戶端。

如果需要緩存:解析出響應頭中的Content-Range中的信息,如果之前用cache_info_open打開CacheInfo對象失敗,則此時需調用cache_info_create來創建CacheInfo對象,如果創建失敗(內存不夠、目錄不存在等)則關閉緩存功能,用實時轉發模式發送響應。隨後,提取此次響應的信息,並保存到CacheInfo對象中,打開或創建Raw緩存文件,最重要的幾點是:打開或創建的Raw緩存文件句柄存放在源請求的HTTPMsg中,並將該文件seek寫定位到Range或Content-Range頭中指定的偏移位置上,在此位置上存放Proxy代理請求中的響應體。最後,將CacheInfo對象的最新內容寫入到緩存信息文件中。

(4)存儲Origin伺服器返回的響應體

任何開啟了Cache功能的HTTP請求,只要請求的內容不在本地緩存中,都需要向Origin伺服器以Proxy模式轉發HTTP請求,在處理完代理請求的響應頭後,需要將響應體存儲到Raw緩存文件適當位置,將存儲位置信息更新到緩存信息文件中,並啟動向客戶端發送響應。

存儲Proxy代理請求的響應體是調用http_proxy_srv_cache_store來實現的:先驗證當前源HTTPMsg是否為pipeline後面的請求消息,是否Cache=on等。將代理請求HTTPcon接收緩衝區中的內容作為要存儲的響應體內容,進行簡單解析判斷,

(a)如果響應體是Content-Length格式:計算還剩餘多少內容沒收到,並對比接收緩衝區內容。如果剩餘內容為0,則已經全部收到了請求的內容,關閉當前HTTP代理消息,並將res_body_chunk設置為結束。如果還有很多剩餘內容沒收到,則將接收緩衝區寫入到.tmp的Raw緩存文件中,寫文件句柄在源HTTPMsg對象中,將寫入成功數據塊的文件位置和長度信息,追加到CacheInfo對象中,並更新到緩存信息文件里,將代理請求HTTPCon緩衝區中已經寫入Raw緩存文件的內容刪除掉。最後再判斷,剛才從緩衝區追加寫入到文件的內容是否全部收齊了,如果收齊了,關閉當前HTTP代理消息。

(b)如果響應體是Transfer-Encoding: chunked格式:這種格式並不知道響應體總長度是多少,也不知道剩餘還有多少內容,返回的響應體是以一塊一塊數據塊編碼方式,每個數據塊前面是當前數據塊長度(16進位)加上\r\n,每個數據塊結尾也加上\r\n為結尾。只有收到一個長度為0的數據塊,才知道全部響應體已經結束和收齊了。由於網路傳輸的複雜性,每次接收數據時,並不一定會完整地收齊一個完整的數據塊,所以需要將接收緩衝區的數據交給http_chunk模塊判斷,是否為接續塊、是否收到結尾塊等。

處理接收緩衝區數據前,先判斷是否收齊了全部響應體,如果收齊了,設置res_body_chunk結束狀態,關閉當前代理消息。將接收緩衝區的所有內容添加到http_chunk中解析判斷,得出緩衝區的內容哪些是接續的數據塊,是否收齊等,將接收緩衝區中那些接續數據塊部分寫入到.tmp的Raw緩存文件中,其中寫文件句柄存放在源HTTPMsg對象中,更新總長度,刪除接收緩衝區中已經寫入的內容,並將寫入成功的數據塊的文件位置和長度信息,追加到CacheInfo對象中,並更新到緩存信息文件里。最後判斷,如果全部數據塊都接收齊全了,關閉當前HTTP代理消息,關閉當前HTTP代理消息,同時正式計算並確定當前收齊了所有數據,設置實際的文件長度。

(c)最後啟動發送緩存文件數據到客戶端。

(5)向源HTTPMsg的客戶端發送響應

發送的響應包括響應頭和位於緩存文件中的響應體,調用http_proxy_cli_cache_send來處理:

通過HTTP的承載協議TCP來發送數據前,需要有序地整理待發送的數據內容,一般情況下,待發送的數據內容包括緩衝區數據、文件數據(完整文件內容、部分文件內容等)、未知的需要網路請求的數據等等,這些數據的總長度有可能知道、也可能不知道,這些待發送數據一般情況下,都位於不同存儲位置,譬如在內存中、硬碟上、網路里等,其特點是分散式的、不連續的、碎片化的、甚至內容長度非常大(大到內存都不可能全部容納的極端情況),管理這些不連續的、碎片化、甚至超大塊頭數據,是由數據結構chunk_t來實現的。

chunk_t數據結構提供了各類功能介面,包括添加各種數據(內存塊、文件名、文件描述符、文件指針等)、有序整理、統一輸出、檢索等訪問介面,最主要的功能是該數據結構解決了不同類別數據整合在一起,模擬成為了一個大緩衝區,大大減少了數據讀寫拷貝產生的巨額性能開銷,大大減少了內存消耗。使用該數據結構,只需將要發送的各種數據內容,通過chunk_t的各類數據追加介面,添加到該數據結構的實例對象中,最後通過tcp_writev或tcp_sendfile來實現數據高效、快速、零拷貝方式的傳輸發送。

基於以上邏輯,向客戶端發送數據的主要工作是如何將待發送內容添加到源HTTPMsg中的res_body_chunk中:

(a)首先計算出res_body_chunk中累計存放的響應體數據總長度,加上源HTTP請求文件的起始位置(如果有Range取其起始位置,如果沒有Range,預設為0),得到當前要追加發送給客戶端的數據在緩存文件中的位置偏移量。分別考慮兩種響應體編碼格式的處理情況;

(b)如果響應體是通過Content-Length來標識:

先用HTTP消息響應總長度減去chunk中的響應體總長度,就計算出剩餘的有待添加的數據長度。通過CacheInfo的碎片數據管理介面,查詢出當前Raw緩存文件中,以(a)中計算出的緩存文件偏移量位置,查出可用的數據長度有多少。

如果Raw緩存文件中存在可用數據,對比剩餘數據長度,截取多餘部分。將該Raw緩存文件名、文件偏移位置、截取處理過的可用數據長度等作為參數,調用chunk添加數據介面,添加到res_body_chunk中,如果跟chunk中之前存儲且未發送出去的數據是接續的,合併處理。如果添加到chunk中的數據總長度達到或超過源請求HTTPMsg消息的響應總長度,則將res_body_chunk設置結束狀態,啟動TCP發送流程。

如果Raw緩存文件中不存在可用數據,則判斷是否向Origin伺服器發送HTTP代理請求:當前源HTTP請求中沒有其他的代理請求存在、Raw緩存文件數據不完整、源HTTP請求的數據範圍不在Raw緩存文件中,這三個條件都滿足時,則需要向Origin伺服器發送HTTP代理請求。這個代理請求是HTTP GET請求,可能跟源HTTP請求方法不一樣,只是獲取緩存數據的某一部分內容,其Range值是從源請求起始位置開始,去查找實際Raw緩存文件存儲情況,得出的空缺處偏移位置。該HTTP代理請求,只負責下載數據存儲到本地緩存文件,其響應頭信息並不更新到緩存信息文件中。

(c)如果響應體的編碼格式為Transfer-Encoding: chunked時:

通過CacheInfo的碎片數據管理介面,查詢出當前Raw緩存文件中,以(a)中計算出的緩存文件偏移量位置,查出可用的數據長度有多少。

如果Raw緩存文件中存在可用數據,將可用數據長度截成最多50個1M大小的數據塊,將Raw緩存文件名、1M數據塊起始位置、長度作為參數添加到res_body_chunk中。如果添加到chunk中的數據總長度達到或超過源請求HTTPMsg消息的響應總長度,則將res_body_chunk設置結束狀態,啟動TCP發送流程。

如果Raw緩存文件中不存在可用數據,則與上述(b)流程類似。

(d)如果源HTTPMsg中統計發送給客戶端的響應數據總長度小於res_body_chunk中的總長度,開始發送chunk中的數據。

(6)發送響應給客戶端的流程是標準通用的流程

基於HTTP Proxy的緩存數據存儲、發送、緩存信息管理維護等功能全部實現完成。

十二. HTTP Tunnel

HTTP Tunnel是在客戶端和Origin伺服器之間,通過Tunnel網關,建立傳輸隧道的通信方式,eJet伺服器可以充當HTTP Tunnel網關,分別與客戶端和Origin伺服器之間建立兩個TCP連接,並在這兩個連接之間進行數據的實時轉發。根據RFC 2616規範,HTTP CONNECT請求方法是建立HTTP Tunnel的基本方式。

HTTP Tunnel最常用的場景是HTTP Proxy正向代理伺服器,代理轉發客戶端https的安全連接請求到Origin伺服器,一般情況下,需要採用端到端的TLS/SSL連接,這時,客戶端會嘗試發送CONNECT方法的HTTP請求,建立一條通過Proxy伺服器,到達Origin伺服器的連接隧道,即兩個TCP連接串聯來實時轉發數據,通過這個連接隧道,進行TLS/SSL的安全握手、認證、密鑰交換、數據加密等,從而實現端到端的安全數據傳輸。

十三. eJet的Callback回調機制

13.1 eJet回調機制

eJet系統提供了HTTP請求消息交付給應用程序處理的回調機制,回調機制是事件驅動模型中底層系統非同步調用上層處理函數的編程模式,上層應用系統需事先將函數實現設置到底層系統的回調函數指針中。

eJet系統提供了兩種回調機制,一種是在啟動eJet時,設置的全局回調函數,另一種是在系統配置文件中位於監聽服務下的動態庫配置回調機制。

13.2 eJet全局回調函數

全局回調函數的設置是在啟動eJet系統時,應用層可以實現HTTP消息處理函數,來處理所有HTTP請求的HTTPMsg,這是程序級的回調機制,需要將eJet代碼嵌入到應用系統中來實現回調處理。

設置全局回調的API如下:

int http_set_reqhandler (void * httpmgmt, RequestHandler * reqhandler, void * cbobj);

其中,httpmgmt是eJet系統創建的全局管理入口HTTPMgmt對象實例, reqhandler是應用層實現的回調函數,cbobj是應用層回調函數的第一個回調參數,eJet每次調用回調函數時,必須攜帶的第一個參數就是cbobj。

應用層回調函數的原型如下:

typedef int RequestHandler (void * cbobj, void * vmsg);

其中,cbobj是設置全局回調函數時傳遞迴調參數,vmsg是當前封裝HTTP請求的HTTPMsg實例對象。

應用程序將系統管理所需的數據結構(包括應用層配置、資料庫連接、用戶管理等)封裝好,創建並初始化一個cbobj對象,作為設置回調函數時的回調參數。通過回調參數,已經HTTPMsg請求對象,可以將請求信息和應用程序內的數據對象建立各種關聯關係。

13.3 eJet動態庫回調

eJet系統另外一種回調是使用動態庫的回調方式,這是松耦合型的、修改配置文件就可以完成回調處理的方式。應用程序無需改動eJet的任何代碼,只需在配置中添加含有路徑的動態庫文件名,即可以實現回調功能,其中動態庫必須實現三個固定名稱的函數,且遵循eJet約定的函數原型定義。

配置文件中添加動態庫回調的位置:

listen = {
    local ip = *;
    port = 8181;

    request process library = reqhandle.so app.conf
......

eJet系統啟動期間,載入配置文件後,解析三層資源架構的第一步HTTPListen時,其配置項下的動態庫會被載入,載入過程為:

  • 載入配置項指定動態庫文件;
  • 根據函數名http_handle_init,獲取動態庫中的初始化函數指針;
  • 根據函數名http_handle,獲取動態庫中的回調處理函數指針;
  • 根據函數名http_handle_clean,獲取動態庫中的清除函數指針;
  • 執行動態庫初始化函數,並返回初始化後的回調參數對象。

在eJet系統退出時,會調用http_handle_clean來釋放初始化過程分配的資源。

動態庫在實現回調時,必須含有這三個函數名:http_handle_init、http_handle、http_handle_clean,其函數原型定義如下:

typedef void * HTTPCBInit     (void * httpmgmt, int argc, char ** argv);
typedef void   HTTPCBClean    (void * hcb);
typedef int    RequestHandler (void * cbobj, void * vmsg);

其中回調函數http_handle的第一個參數cbobj是由http_handle_init返回的結果對象,vmsg即是eJet系統的HTTPMsg實例對象。

13.4 回調函數使用HTTPMsg的成員函數

eJet系統通過傳遞HTTPMsg實例對象給回調函數,來處理HTTP請求。HTTP對象封裝了HTTP請求的所有信息,回調函數在處理請求時,可以添加各種響應數據到HTTPMsg中,包括響應狀態、響應頭、響應體等。

訪問請求頭信息或添加響應數據的操作,既可以直接對HTTPMsg的成員變數進行數據讀取或寫入,也可以通過調用HTTPMsg內置的指針函數來進行處理,HTTPMsg中封裝了很多函數調用,通過這些函數,基本可實現eJet系統HTTP請求處理的各種操作。這些例子函數如下:

......
char * (*GetRootPath)     (void * vmsg);
 
int    (*GetPath)         (void * vmsg, char * path, int len);
int    (*GetRealPath)     (void * vmsg, char * path, int len);
int    (*GetRealFile)     (void * vmsg, char * path, int len);
int    (*GetLocFile)      (void * vmsg, char * p, int len, char * f, int flen, char * d, int dlen);
 
int    (*GetQueryP)       (void * vmsg, char ** pquery, int * plen);
int    (*GetQuery)        (void * vmsg, char * query, int len);
int    (*GetQueryValueP)  (void * vmsg, char * key, char ** pval, int * vallen);
int    (*GetQueryValue)   (void * vmsg, char * key, char * val, int vallen);

int    (*GetReqContentP)    (void * vmsg, void ** pform, int * plen);
 
int    (*GetReqFormJsonValueP)  (void * vmsg, char * key, char ** ppval, int * vallen);
int    (*GetReqFormJsonValue)   (void * vmsg, char * key, char * pval, int vallen);

int    (*SetStatus)      (void * vmsg, int code, char * reason);
int    (*AddResHdr)      (void * vmsg, char * na, int nlen, char * val, int vlen);
int    (*DelResHdr)      (void * vmsg, char * name, int namelen);
 
int    (*SetResEtag) (void * vmsg, char * etag, int etaglen);

int    (*SetResContentType)   (void * vmsg, char * type, int typelen);
int    (*SetResContentLength) (void * vmsg, int64 len);

int    (*AddResContent)       (void * vmsg, void * body, int64 bodylen);
int    (*AddResContentPtr)    (void * vmsg, void * body, int64 bodylen);
int    (*AddResFile)          (void * vmsg, char * filename, int64 startpos, int64 len);

int    (*Reply)          (void * vmsg);
int    (*RedirectReply)  (void * vmsg, int status, char * redurl);
......

eJet通過設置回調函數的兩種介面機制,將客戶端的HTTP請求轉交給特定的應用程序來處理,充分利用Web開發的各種前端技術,擴展應用程序與用戶前端的交互能力。

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/294339.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-26 13:43
下一篇 2024-12-26 13:44

相關推薦

發表回復

登錄後才能評論