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) 撰寫。
- HTML 頁面含 tags 指示 browser 如何呈現文字與圖形,例如
<b> Make me bold! </b>。 - HTML 真正的威力在 hyperlink:
<a href="http://www.cmu.edu/index.html">Carnegie Mellon</a>可指向任何 Internet host 上的內容。
沒有 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):
- 沒有標準規則判斷 URL 是 static 還是 dynamic — 每個 server 自訂;古典作法是規定所有可執行檔放在特定目錄(如
cgi-bin)。 - Suffix 開頭的
/不是 Linux 根目錄,而是該類內容的 home directory(如 static 放/usr/httpd/html、dynamic 放/usr/httpd/cgi-bin)。 - 最小 suffix 是
/,所有 server 都會展開成預設首頁(如/index.html)— 這就是只打網域名稱也能開首頁的原因(browser 補上/,server 再展開)。 - Port 為選填,預設是 HTTP 的 well-known port 80。
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 + 空行。
- Request line 格式:
method URI version - Method 包含 GET, POST, OPTIONS, HEAD, PUT, DELETE, TRACE;GET 佔絕大多數,指示 server 產生並回傳 URI (uniform resource identifier) 所指的內容。
- URI 是 URL 的 suffix(檔名 + 選擇性參數)。例外:proxy server 請求內容時,URI 必須是完整 URL(p.988 註 3)。
- Header 格式:
header-name: header-data。 - Host header 在 HTTP/1.1 為必填、HTTP/1.0 不需要;它讓 proxy chain 中間的 proxy cache 能判斷自己是否有本地快取的內容副本。
- HTTP/1.1 (RFC 2616) 較 HTTP/1.0 (1996) 增加快取、安全等 headers,並支援同一 persistent connection 上執行多次交易;兩版相容,因為雙方會忽略不認識的 header。
HTTP Responses:response line + 零或多個 response headers + 空行 + response body。
- Response line 格式:
version status-code status-message - status-code 是三位數正整數;最重要的兩個 response headers:Content-Type(body 的 MIME type)與 Content-Length(body 的 byte 大小)。
常見 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 產生的內容型別與大小,所以 CGI 程式自己必須產生 Content-type、Content-length 兩個 response headers 以及終止 headers 的空行。(Practice Problem 11.5 即考此觀念:CGI 內容寫到 stdout,而 stdout 已被 dup2 重導到 connected descriptor,故直達 client。)
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 → 依序 printf 出 Connection: close、Content-length、Content-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-Metadata 的 rio_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 OK 與 Server 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
- Child 執行
execve後,CGI 程式繼承 execve 之前已開啟的檔案與環境變數,故其 stdout 輸出不經 parent 直達 client。 - Tiny 只設
QUERY_STRING;真實 server 會設定全部 CGI 環境變數。 - 不夠強健之處:Tiny 先送出
200 OK才執行 CGI 程式,沒有考慮 CGI 程式執行失敗的可能。
若 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)
- 每個網路應用都基於 client-server model:server 管理資源、為 client 提供服務;基本操作是 client-server transaction(request → response)。
- 程式設計師眼中的 Internet:(1) 每台 host 有唯一 32-bit IP address;(2) IP address 集合映射到 Internet domain names;(3) 不同 host 上的 processes 可經由 connections 通訊(見 11-Network-Programming/02-Global-IP-Internet)。
- Socket 是連線端點,以 file descriptor 形式呈現給應用程式;client/server 透過讀寫 descriptor 通訊(見 11-Network-Programming/03-Sockets-Interface)。
- Web server 與 browser 用 HTTP 溝通:static 內容 = 讀磁碟檔回傳;dynamic 內容 = 在 child process 中執行程式並回傳其輸出;CGI 規範參數傳遞與輸出路徑。
- 權威資訊來源:RFC 文件(HTTP/1.1 = RFC 2616);MIME types 官方清單由 IANA 維護。
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_static 中 Close(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 章) |
Related Notes
- 11-Network-Programming/01-Client-Server-Model-and-Networks
- 11-Network-Programming/02-Global-IP-Internet
- 11-Network-Programming/03-Sockets-Interface
- 10-System-IO/02-Rio-Package-and-File-Metadata
- 10-System-IO/03-File-Sharing-Redirection-and-Standard-IO
- 09-Virtual-Memory/04-Memory-Mapping
- 08-Exceptional-Control-Flow/04-Process-Control
- 08-Exceptional-Control-Flow/05-Signals
- 12-Concurrent-Programming/01-Process-and-Event-Based-Concurrency