<menu id="ycqsw"></menu><nav id="ycqsw"><code id="ycqsw"></code></nav>
<dd id="ycqsw"><menu id="ycqsw"></menu></dd>
  • <nav id="ycqsw"></nav>
    <menu id="ycqsw"><strong id="ycqsw"></strong></menu>
    <xmp id="ycqsw"><nav id="ycqsw"></nav>
  • 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 – 系統時間,同上

    隨著應用場景的擴展,根據需要還可以擴展定義其他名稱的參數變量??傮w來說,使用上述參數變量,基本可以訪問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開發的各種前端技術,擴展應用程序與用戶前端的交互能力。

    版權聲明:本文內容由互聯網用戶自發貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如發現本站有涉嫌抄襲侵權/違法違規的內容, 請發送郵件至 舉報,一經查實,本站將立刻刪除。

    發表評論

    登錄后才能評論
    国产精品区一区二区免费