5. 練習:實現簡單的Web伺服器

實現一個簡單的Web伺服器myhttpd。伺服器程序啟動時要讀取配置檔案/etc/myhttpd.conf,其中需要指定伺服器監聽的連接埠號和服務目錄,例如:

Port=80
Directory=/var/www

注意,1024以下的連接埠號需要超級用戶才能開啟服務。如果你的系統中已經安裝了某種Web伺服器(例如Apache),應該為myhttpd選擇一個不同的連接埠號。當瀏覽器向伺服器請求檔案時,伺服器就從服務目錄(例如/var/www)中找出這個檔案,加上HTTP協議頭一起發給瀏覽器。但是,如果瀏覽器請求的檔案是可執行的則稱為CGI程序,伺服器並不是將這個檔案發給瀏覽器,而是在伺服器端執行這個程序,將它的標準輸出發給瀏覽器,伺服器不發送完整的HTTP協議頭,CGI程序自己負責輸出一部分HTTP協議頭。

5.1. 基本HTTP協議

打開瀏覽器,輸入伺服器IP,例如 http://192.168.0.3 ,如果連接埠號不是80,例如是8000,則輸入 http://192.168.0.3:8000 。這時瀏覽器向伺服器發送的HTTP協議頭如下:

GET / HTTP/1.1
Host: 192.168.0.3:8000
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.6) Gecko/20061201 Firefox/2.0.0.6 (Ubuntu-feisty)
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

注意,其中每一行的末尾都是回車加換行(C語言的"\r\n"),第一行是GET請求和協議版本,其餘幾行選項欄位我們不討論,HTTP協議頭的最後有一個空行,也是回車加換行

我們實現的Web伺服器只要能正確解析第一行就行了,這是一個GET請求,請求的是服務目錄的根目錄/(在本例中實際上是/var/www),Web伺服器應該把該目錄下的索引頁(預設是index.html)發給瀏覽器,也就是把/var/www/index.html發給瀏覽器。假如該檔案的內容如下(HTML檔案沒必要以"\r\n"換行,以"\n"換行就可以了):

<html>
<head><title>Test Page</title></head>
<body>
	<p>Test OK</p>
	<img src='mypic.jpg'>
</body>
</html>

顯示一行字和一幅圖片,圖片的相對路徑(相對當前的index.html檔案的路徑)是mypic.jpg,也就是/var/www/mypic.jpg,如果用絶對路徑表示應該是:

<img src='/mypic.jpg'>

伺服器應按如下格式應答瀏覽器:

HTTP/1.1 200 OK
Content-Type: text/html

<html>
<head><title>Test Page</title></head>
<body>
	<p>Test OK</p>
	<img src='mypic.jpg'>
</body>
</html>

伺服器應答的HTTP頭也是每行末尾以回車加換行結束,最後跟一個空行的回車加換行。

HTTP頭的第一行是協議版本和應答碼,200表示成功,後面的消息OK其實可以隨意寫,瀏覽器是不關心的,主要是為了調試時給開發人員看的。雖然網絡協議最終是程序與程序之間的對話,但是在開發過程中卻是人與程序之間的對話,一個設計透明的網絡協議可以提供很多直觀的信息給開發人員,因此,很多應用層網絡協議,如HTTP、FTP、SMTP、POP3等都是基于文本的協議,為的是透明性(transparency)。

HTTP頭的第二行表示即將發送的檔案的類型(稱為MIME類型),這裡是text/html,純文字檔案檔案是text/plain,圖片則是image/jpg、image/png等。

然後就發送檔案的內容,發送完畢之後主動關閉連接,這樣瀏覽器就知道檔案發送完了。這一點比較特殊:通常網絡通信都是客戶端主動發起連接,主動發起請求,主動關閉連接,伺服器只是被動地處理各種情況,而HTTP協議規定伺服器主動關閉連接(有些Web伺服器可以配置成Keep-Alive的,我們不討論這種情況)。

瀏覽器收到index.html之後,發現其中有一個圖片檔案,就會再發一個GET請求(HTTP協議頭其餘部分略):

GET /mypic.jpg HTTP/1.1

一個較大的網頁中可能有很多圖片,瀏覽器可能在下載網頁的同時就開很多綫程下載圖片,因此,'''伺服器即使對同一個客戶端也需要提供並行服務的能力'''。伺服器收到這個請求應該把圖片發過去然後關閉連接:

HTTP/1.1 200 OK
Content-Type: image/jpg

(這裡是mypic.jpg的二進制數據)

這時瀏覽器就應該顯示出完整的網頁了。

如果瀏覽器請求的檔案在伺服器上找不到,要應答一個404錯誤頁面,例如:

HTTP/1.1 404 Not Found
Content-Type: text/html

<html><body>request file not found</body></html>

5.2. 執行CGI程序

如果瀏覽器請求的是一個執行檔(不管是什麼樣的執行檔,即使是shell腳本也一樣),那麼伺服器並不把這個檔案本身發給瀏覽器,而是把它的執行結果標準輸出發給瀏覽器。例如一個shell腳本/var/www/myscript.sh(注意一定要加可執行權限):

#!/bin/sh
echo "Content-Type: text/html"
echo
echo "<html><body>Hello world!</body></html>"

這樣瀏覽器收到的是:

HTTP/1.1 200 OK
Content-Type: text/html

<html><body>Hello world!</body></html>

總結一下伺服器的處理步驟:

  1. 解析瀏覽器的請求,在服務目錄中查找相應的檔案,如果找不到該檔案就返回404錯誤頁面

  2. 如果找到了瀏覽器請求的檔案,用stat(2)檢查它是否可執行

  3. 如果該檔案可執行:

    1. 發送HTTP/1.1 200 OK給客戶端

    2. fork(2),然後用dup2(2)重定向子進程的標準輸出到客戶端socket

    3. 在子進程中exec(3)該CGI程序

    4. 關閉連接

  4. 如果該檔案不可執行:

    1. 發送HTTP/1.1 200 OK給客戶端

    2. 如果是一個圖片檔案,根據圖片的副檔名發送相應的Content-Type給客戶端

    3. 如果不是圖片檔案,這裡我們簡化處理,都當作Content-Type: text/html

    4. 簡單的HTTP協議頭有這兩行就足夠了,再發一個空行表示結束

    5. 讀取檔案的內容發送到客戶端

    6. 關閉連接