uhttpd是一個簡單的web服務器程序,以前沒怎么接觸過,所以這里主要是對web服務器設計的一些學習總結。Openwrt系統中,真正用到的(需要了解的),其實不多,主要就是cgi的處理,包括與cgi程序的信息交互等,最后一節詳細描述一下。
1.HTTP協議概述
HTTP協議是目前互聯網使用最廣泛的應用層協議。其協議框架很簡單,在一個TCP連接中,以一問一答的方式進行信息交互。具體講,就是客戶端(如常見的瀏覽器)connect服務端的知名端口(通常是80),建立一個TCP連接,然后發送一個request;服務器端對該request解析后,發回相應的response應答,並關閉TCP連接。這就是一次交互,之后客戶端再有請求,則重復上面的過程。
交互報文格式如下圖所示:
Request報文首行為request-line,其中type有GET、POST、HEAD三種方式,然后最重要的是url,它告訴服務器所請求的資源。Response報文首行為response-line,其中最重要的是code,它告知客戶端響應情況(found、redirect、error等),然后跟一個簡單的可讀的短語。
兩種報文后面具體的內容格式差不多,都是一些headers(其中冒號前的str指明header類型),然后以一個空行標識header結束,后面是數據。對於request,只有POST類型的請求需要提交數據,其它類型的是沒有數據的。Response報文的數據就是url所指定的資源文件(html、doc、gif等)。
2.服務器架構
Uhttpd作為一個簡單的web服務器,其代碼量並不多,而且組織結構比較清楚。和其它網絡服務器差不多,其main函數進行一些初始化(首先parse config-file,然后parse argv),然后進入一個循環,不斷地監聽,每當有一個客戶請求到達時,則對它進行處理。
對於web服務器,所要做的處理主要就是分析url,判斷出是file-request、cgi-request或lua-request,這主要是根據url的最前面的字符串(稱為前綴prefix)得出的;然后就用相應的形式進行處理。如下圖所示:
3.cig-response流程
前面已提到,openwrt系統中使用的uhttpd服務,主要是用cgi方式來回應客戶請求的,下面就對這種方式詳細闡述。
3.1url解析
由上圖紅色字所示,uh_cgi_request需要兩個二外的參數pathinfo和interpreter,其中pin是一個struct,包含了路徑中各種有用信息;ipr指明所用的cgi程序,因為一個服務器中可以有多個cgi程序。
如圖所示,docroot是服務器的資源目錄,是為了os准確定位資源位置,由uhttpd的config文件設定,如openwrt中為/www。后面的是client傳來的url,開頭的為cgi-prefix,也是有uhttpd的config文件設定的,它指明serv端采用cgi處理方式,如openwrt中的為/www/cgi-bin;緊接着的是cgi的程序名,它指明了使用哪個cgi程序;再后面就是實際的path信息了,在cgi方式中,它會被當成參數供cgi程序使用。
3.2cgi處理框架
要運行cgi程序,首先意味着需fork出一個子進程,並通過execl函數替換進程空間為cgi程序;其次,數據傳遞,子進程替換了進程空間后,怎么獲得原信息,有怎么把回饋數據傳輸給父進程(即uhttpd),父進程又怎么接收這些數據。
首先創建了兩個pipe,這實際上是利用AF_UNIX協議域,創建兩個相連的socket_unix,那么它們映射的文件描述符(即這里的fd[0]、fd[1])就構成了一個pipe,且這種關系即使fork后也仍然存在,因為fork僅是增加文件的引用次數,而os維護的file結構和socket結構都沒變,這就是父子進程間傳遞數據的方式。然后fork出一個子進程。
子進程中首先把兩個管道的一端close,注意這僅是使得文件引用次數變為1。由於子進程待會要excel替換,替換后rfd、wfd就不存在了,因此先把它們dup2給知名的stdin、stdout,這樣即使execl替換后,ipt->extu程序可以以此來和父進程傳遞數據。另外,execl替換后,cgi程序仍需要之前的一些參數信息,如PATH_INFO等,這種情況下,最簡單的辦法就是setenv,把需要的參數設為環境變量。
為什么要兩個pipe,因為子進程向父進程傳遞回饋數據需要一個out-pipe,而若有post數據,子進程還需要一個in-pipe,從父進程讀取post數據。
父進程中首先也是close,同上所述。若有post數據,先從httprequest-header中得到content-length,為后面傳遞給子進程做准備。然后進入一個循環(為什么要循環,什么時候退出,后面講),通過select輪詢io,超時、中斷的情況就不看了,輪詢的io一個是reader,即從子進程讀取回饋數據,而若有post數據的話,還要另一個io,writer,向子進程寫post數據。主要的處理就是上圖中紅色字所示,具體如下: