首頁>技術>

eJet是一款在GitHub上開源的Web伺服器,下載地址為 https://github.com/kehengzhong/ejet,利用 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 - 對應的是請求主機名稱domainHTTP資源位置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時處理的。

反向代理(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請求的絕對URIuri - 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,否則為FALSE5.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-keywordsrewrite ^/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=digerewrite ^/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=param3rewrite ^/wiki/(.*)$ /wiki/index.php?title=$1?;請求的URL:http://xxxx.com/wiki/some-keywords重寫後URL:http://xxxx.com/wiki/index.php?title=some-keywordsrewrite ^/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=50rewrite ^/([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虛擬主機 - 對應的是請求主機名稱domainHTTP資源位置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/SSL7.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伺服器。

上面描述的反向代理伺服器,在這裡就是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中,傳送給客戶端。

每次未傳送成功的訊息體,將會從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中。

根據代理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轉發。

採用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資料傳送完畢。

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伺服器已經返回全部的響應資料了,將這些資料傳送給客戶端,即可結束當前處理。

其中在接收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命名規則是否設定,決定是否啟動快取管理。

快取的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伺服器下載並實際儲存的資料儲存狀態,每塊是以偏移量和長度來唯一標識,相鄰的碎片塊合併,完整檔案只有一個碎片塊。

(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,直接返回不做資源釋放。

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

檢查該快取檔案是否存在,如果存在則直接將該快取檔案返回給客戶端即可。注:在沒有收到全部位元組資料之前Raw快取檔名是實際快取檔案後加.tmp做副檔名。

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

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

但對於啟用了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伺服器返回的響應體

儲存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開發的各種前端技術,擴充套件應用程式與使用者前端的互動能力。

86
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 安卓vs鴻蒙第三方件切換寶典V1.0(附下載)