Web 伺服器與 Tiny (Web Servers & the Tiny Server)

Overview Table

主題 核心內容 書頁
Web 基礎 HTTP 是文字式應用層協定;Web 內容用 HTML 撰寫,可含 hyperlinks p.984
Web 內容 內容 = byte 序列 + MIME type;分 static(讀檔)與 dynamic(執行程式)兩種服務方式 p.985
URL 結構 http://host:port/path?arg1&arg2;client 用前綴、server 用後綴 p.985-986
HTTP transactions request = request line + headers + 空行;response = response line + headers + 空行 + body p.986-989
CGI 動態內容的 de facto 標準:fork + execve + QUERY_STRING + dup2 重導 stdout p.989-991
Tiny Web server 約 250 行的迭代式 (iterative) HTTP/1.0 伺服器,只支援 GET,可服務 static 與 dynamic 內容 p.992-999
章末總結 client-server 模型 + Internet + sockets + HTTP + CGI 的整合 p.1000-1001

11.5.1 Web 基礎 (Web Basics, p.984)

Web client(browser)與 server 以文字式應用層協定 HTTP (hypertext transfer protocol) 互動:browser 開啟連線並請求內容,server 回傳內容後關閉連線,browser 讀取並顯示。Web 服務與傳統檔案傳輸(如 FTP)的關鍵差異在於內容可用 HTML (hypertext markup language) 撰寫。

EOF 在連線上的意義 (Aside, p.984)

沒有 EOF 字元這種東西。EOF 是由 kernel 偵測的「條件」,應用程式透過 read 回傳 0 得知。磁碟檔案:目前檔案位置超過檔案長度時發生;Internet 連線:對端 process 關閉它那一端的連線時發生,本端在試圖讀取 stream 最後一個 byte 之後偵測到 EOF。與 10-System-IO/01-Unix-IO-and-Files 的 EOF 概念一致。

11.5.2 Web 內容 (Web Content, p.985-986)

對 Web client 與 server 而言,內容 (content) 就是「一段 byte 序列 + 相關聯的 MIME (multipurpose internet mail extensions) type」。伺服器提供內容有兩種方式,每個內容都對應伺服器管理的某個檔案,以唯一的 URL (universal resource locator) 命名。

服務方式 做法 名稱
讀取磁碟檔案並回傳其內容 fetch a disk file serving static content
執行可執行檔並回傳其執行期輸出 run an executable serving dynamic content

常見 MIME types(Fig. 11.23):

MIME type 說明
text/html HTML 頁面
text/plain 未格式化文字
application/postscript Postscript 文件
image/gif / image/png / image/jpeg GIF / PNG / JPEG 編碼的二進位影像

URL 解剖(以 http://bluefish.ics.cs.cmu.edu:8000/cgi-bin/adder?15000&213 為例):

http://www.google.com:80/index.html
└──┬──┘└──────┬──────┘└┬┘└───┬────┘
 scheme     host      port  suffix(檔名+參數)
├──────── client 使用 ─────┤├─ server 使用 ─┤
  (決定聯絡哪種 server、        (在檔案系統找檔、
   在哪裡、哪個 port)            判斷 static/dynamic)

可執行檔 URL:/cgi-bin/adder?15000&213
                          ↑檔名與參數以 '?' 分隔
                           參數之間以 '&' 分隔

Server 解讀 URL suffix 的三個要點(p.986):

11.5.3 HTTP Transactions (p.986-989)

HTTP 建立在「經由 Internet 連線傳輸的文字行」之上,因此可用 telnet 直接對任何 Web server 進行交易除錯。HTTP 標準要求每個文字行以 CRLF(\r\n)結尾

Client                                     Server
  │ ── GET / HTTP/1.1        (request line) ──▶│
  │ ── Host: www.aol.com    (request header) ─▶│
  │ ── (空行:終止 headers) ─────────────────▶│
  │                                            │
  │◀── HTTP/1.0 200 OK       (response line) ──│
  │◀── Content-Type: text/html  (headers) ─────│
  │◀── Content-Length: 42092 ──────────────────│
  │◀── (空行:終止 headers) ───────────────────│
  │◀── <html> ... </html>    (response body) ──│
  │◀── server 關閉連線 ────────────────────────│

HTTP Requests:request line + 零或多個 request headers + 空行。

HTTP Responses:response line + 零或多個 response headers + 空行 + response body。

常見 status codes(Fig. 11.25):

Status code Status message 意義
200 OK 請求處理無誤
301 Moved permanently 內容移到 Location header 所指的 hostname
400 Bad request server 無法理解請求
403 Forbidden server 沒有權限存取被請求的檔案
404 Not found server 找不到被請求的檔案
501 Not implemented server 不支援該 request method
505 HTTP version not supported server 不支援請求中的版本

11.5.4 服務動態內容 (Serving Dynamic Content — CGI, p.989-991)

動態內容引出四個問題,由 de facto 標準 CGI (common gateway interface) 一次回答:

問題 CGI 的答案
client 如何把程式參數傳給 server? GET 的參數放在 URI 內:? 分隔檔名與參數、& 分隔各參數;空白須編碼為 %20(其他特殊字元也有類似編碼)
server 如何把參數傳給 child? server fork child,child 在 execve 前設定環境變數 QUERY_STRING(如 15000&213),CGI 程式用 getenv 讀取
server 如何傳其他資訊給 child? CGI 定義一組環境變數(見下表)
child 的輸出送到哪? CGI 程式寫到 standard output;child 在載入 CGI 程式前先用 dup2 把 stdout 重導到 connected descriptor,輸出因此直達 client

CGI 環境變數(Fig. 11.26):

環境變數 說明
QUERY_STRING 程式參數
SERVER_PORT 父行程監聽的 port
REQUEST_METHOD GET 或 POST
REMOTE_HOST client 的網域名稱
REMOTE_ADDR client 的 dotted-decimal IP 位址
CONTENT_TYPE 僅 POST:request body 的 MIME type
CONTENT_LENGTH 僅 POST:request body 的 byte 大小
Child 負責的 headers

父行程不知道 child 產生的內容型別與大小,所以 CGI 程式自己必須產生 Content-typeContent-length 兩個 response headers 以及終止 headers 的空行。(Practice Problem 11.5 即考此觀念:CGI 內容寫到 stdout,而 stdout 已被 dup2 重導到 connected descriptor,故直達 client。)

POST 的例外 (Asides, p.989/991)

POST 的參數放在 request body、不在 URI。若要支援 POST,child 還需把 standard input 也重導到 connected descriptor,CGI 程式再從 stdin 讀取 request body 中的參數。

範例 adder.c(Fig. 11.27)流程:getenv("QUERY_STRING") → 以 strchr(buf,'&') 切出兩參數 → atoi → 組出 HTML body → 依序 printfConnection: closeContent-lengthContent-type + 空行、body,最後 fflush(stdout)。對應交易(Fig. 11.28):GET /cgi-bin/adder?15000&213 HTTP/1.0 → 回應 15000 + 213 = 15213

11.6 Tiny Web Server (p.992-1000)

Tiny 是約 250 行 C 程式碼的迭代式 (iterative) HTTP/1.0 伺服器,整合本書的 process control(08-Exceptional-Control-Flow/04-Process-Control)、Unix I/O、sockets 介面與 HTTP,只支援 GET,能對真實 browser 服務 static 與 dynamic 內容,但缺乏真實伺服器的功能、強健性與安全性。

main:  Open_listenfd(port)
         │
         ▼         ┌────────────────────────────────────┐
       while(1) ──▶│ Accept → Getnameinfo → doit → Close │──┐
         ▲         └────────────────────────────────────┘  │
         └─────────────────(迭代式:一次一個連線)◀─────────┘

doit(fd):
  Rio_readinitb / Rio_readlineb  ── 讀 request line
  sscanf "%s %s %s" → method, uri, version
  method != "GET"? ──▶ clienterror 501 "Not implemented" → return
  read_requesthdrs  ── 讀掉並忽略所有 request headers
  is_static = parse_uri(uri, filename, cgiargs)
  stat(filename) 失敗? ──▶ clienterror 404 "Not found" → return
  ┌─ static:  非 regular file 或無讀權限(S_ISREG/S_IRUSR)?
  │             ──▶ 403 "Forbidden";否則 serve_static
  └─ dynamic: 非 regular file 或無執行權限(S_ISREG/S_IXUSR)?
                ──▶ 403 "Forbidden";否則 serve_dynamic

各函式重點:

函式 作用 關鍵細節
main 開 listening socket 後進入無窮迴圈:accept → doit → close iterative server:一次只服務一個 client;port 由命令列傳入
doit 處理一筆 HTTP 交易 10-System-IO/02-Rio-Package-and-File-Metadatario_readlineb 讀 request line;strcasecmp 過濾非 GET
clienterror 送出含 status code/message 的錯誤回應 + HTML body body 先組成單一字串以便算出 Content-length;所有輸出用強健的 rio_writen
read_requesthdrs 讀取並忽略所有 request headers 迴圈直到讀到只含 \r\n 的空行(strcmp(buf, "\r\n"))
parse_uri 把 URI 解析成 filename 與(可能為空的)CGI 參數字串 URI 含 cgi-bin ⇒ dynamic(回傳 0);否則 static(回傳 1)。static home dir = 目前目錄 .,dynamic home dir = ./cgi-bin,預設檔名 ./home.html(URI 以 / 結尾時附加);dynamic 用 index(uri,'?') 切出 cgiargs
serve_static 送 response line/headers 後,以 mmap + rio_writen 送檔案內容 見下方詳解
get_filetype 由副檔名判斷 MIME type 支援 .html.gif.png.jpg,其餘視為 text/plain(共五種 static 類型)
serve_dynamic 先送 HTTP/1.0 200 OKServer header,再 fork child 執行 CGI 程式 見下方詳解

serve_static 的資料流(Fig. 11.34, p.997-999):

get_filetype → 組 headers(200 OK / Server / Connection: close /
               Content-length / Content-type + 空行)→ Rio_writen

srcfd = Open(filename, O_RDONLY, 0)
srcp  = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0)
        │  把檔案前 filesize bytes 映射到私有唯讀虛擬記憶體區
Close(srcfd)      ← 映射後 descriptor 不再需要;不關會造成致命的洩漏
Rio_writen(fd, srcp, filesize)   ← 實際把檔案內容傳給 client
Munmap(srcp, filesize)           ← 釋放映射區;不釋放會造成致命的記憶體洩漏

mmap 的語意詳見 09-Virtual-Memory/04-Memory-Mapping

serve_dynamic 的控制流(Fig. 11.35, p.999-1000):

Parent (Tiny)                          Child
  │ Rio_writen: "HTTP/1.0 200 OK"
  │ Rio_writen: "Server: Tiny Web Server"
  │ Fork() ──────────────────────────▶ setenv("QUERY_STRING", cgiargs, 1)
  │                                    Dup2(fd, STDOUT_FILENO)  ← stdout→client
  │                                    Execve(filename, emptylist, environ)
  │ Wait(NULL)  ← 阻塞等待並回收 child      │
  │                                    CGI 程式的 printf 直達 client
過早關閉的連線與 SIGPIPE (Aside, p.1000)

若 client 已關閉連線(例如按下瀏覽器 Stop),server 對該連線的第一次 write 正常返回,但第二次 write 會觸發 SIGPIPE,其預設行為是終止 process。若捕捉或忽略 SIGPIPE,第二次 write 改為回傳 −1 且 errno = EPIPE("Broken pipe")。強健的 server 必須捕捉 SIGPIPE 並檢查 write 的 EPIPE 錯誤。相關訊號機制見 08-Exceptional-Control-Flow/05-Signals

11.7 本章總結 (Summary, p.1000-1001)

Exam/Test Patterns

情境 / 關鍵字 答案
連線上的 EOF 是什麼? 不是字元,是 kernel 偵測的條件;對端關閉連線後,本端 read 回傳 0
static vs dynamic content 的差別 static:讀磁碟檔回傳;dynamic:執行程式、回傳其執行期輸出
URL ?& 的作用 ? 分隔檔名與參數;& 分隔各參數;空白編碼為 %20
URL suffix 開頭的 / 代表? 不是 Linux 根目錄,是該類內容的 home directory
只輸入網域名稱為何能開首頁? browser 補 /,server 把最小 suffix / 展開為預設檔(如 /index.html)
HTTP request line / response line 格式 method URI version / version status-code status-message
HTTP/1.1 必填而 HTTP/1.0 不用的 header Host(供 proxy chain 中的 proxy cache 判斷是否有本地副本)
HTTP 文字行的行尾 CRLF(\r\n)
404 / 403 / 501 / 505 / 301 Not found / Forbidden / Not implemented / HTTP version not supported / Moved permanently
CGI 程式如何取得 GET 參數? server 的 child 設 QUERY_STRING 環境變數,CGI 程式用 getenv 讀取
CGI 輸出如何到達 client?(PP 11.5) child 先 dup2(fd, STDOUT_FILENO)execve;CGI 寫 stdout 即直達 client
誰產生動態內容的 Content-type/length? CGI 程式(child)自己,因 parent 不知道內容型別與大小
POST 參數放哪?CGI 如何讀? 放 request body;child 需再把 stdin 重導到 connected descriptor,CGI 從 stdin 讀
Tiny 判斷 static/dynamic 的規則 URI 含字串 cgi-bin 即視為 dynamic,否則 static
Tiny 對非 GET method 的回應 501 Not implemented
Tiny 對 request headers 的處理 read_requesthdrs 讀掉並忽略,直到空行 \r\n
serve_staticClose(srcfd)Munmap 的目的 避免致命的洩漏:mmap 後 descriptor 不再需要;傳完後必須 munmap 釋放映射區
serve_dynamic 中 parent 的 Wait(NULL) 阻塞等待並回收 (reap) child,避免 zombie
對已關閉連線第二次 write 觸發 SIGPIPE(預設終止 process);若捕捉/忽略則回傳 −1、errno=EPIPE(Broken pipe)
Tiny 是哪種伺服器架構? iterative server:一次只處理一個連線(併發版見第 12 章)