一次請求與一次回覆,全是純文字
在上一篇你打開了一個通訊端,看到了主從式的安排:一端聆聽,另一端連線,位元組流經作業系統替你撐開的那道門。但當那道門開了之後,兩端到底對彼此說了什麼?在網頁世界裡,答案是 HTTP,超文字傳輸協定。HTTP 活在應用層,也就是整個堆疊的最頂端,騎乘在底下 TCP 所提供的可靠位元組串流之上。
關於 HTTP,第一件令人意外的事是它有多麼樸素。它是一種文字協定:瀏覽器送出的訊息只是一行行普通、可讀的字元,那種你大可以手打出來的字。一次請求以一行開頭,寫明方法與路徑,接著是一疊標頭、一個空行,再加上可選的本文。回覆的骨架相同:一行狀態行、標頭、一個空行,然後是本文——通常就是你索取的那份 HTML、圖片或 JSON。整個網頁世界就靠這一個整齊的形狀撐起。
REQUEST (browser -> server) RESPONSE (server -> browser)
----------------------------------- -----------------------------------
GET /index.html HTTP/1.1 HTTP/1.1 200 OK
Host: example.com Content-Type: text/html
User-Agent: Firefox/123 Content-Length: 1270
Accept: text/html Cache-Control: max-age=3600
<blank line>
<blank line> <!doctype html> ...the page...
^ method ^ path ^ version ^ version ^ code ^ phraseURL:對某個東西的精確地址
在瀏覽器能送出任何東西之前,它得先知道要送到哪裡、要索取什麼。這就是 URL的工作——你輸入或點擊的那個網址。一個 URL 把好幾層的定址壓進一個字串裡,由左讀到右,就像從國家一路讀到公寓的郵政地址。
以 https://example.com:443/photos/cat.jpg 為例。配置方案 https 告訴瀏覽器要用哪個協定、以及是否要用 TLS 把它包起來。主機 example.com 是一個網域名稱,前一階提過的網際網路電話簿 DNS 會把它變成一個 IP 位址。連接埠 443 是那台機器上的門牌號(通常會省略,因為 https 就隱含了 443)。而路徑 /photos/cat.jpg 則指名伺服器該抓取或產生的那個特定資源。同樣的方案、同樣的主機,但兩條不同的路徑,就是兩個完全不同的請求。
方法與狀態碼:動詞與裁決
請求的第一個字 GET 就是方法,是句子裡的動詞。HTTP 請求方法是一套刻意設計的小詞彙。GET 意思是「把這個資源給我」,而且不該改動伺服器上的任何東西。POST 意思是「這裡有些資料,拿去做點什麼」,例如送出表單。PUT 意思是「把這個存到這裡」,DELETE 意思是「移除它」,HEAD 則是「只把標頭送我,不要本文」。GET 只讀、而 POST 可能改動的這種規矩稱為安全性;而像 PUT 或 DELETE 那樣可以重複而不產生額外效果的方法,則稱為冪等。這些是網頁世界約定的慣例,不是 TCP 強制執行的規則。
回覆以一個三位數的狀態碼開場,那是伺服器對發生了什麼事的裁決。第一個數字把它們分成幾個家族。1xx 是資訊性的,很少見。2xx 代表成功,而 200 OK 就是你想要的那個。3xx 代表重新導向,去別處看看;301 說的是「已永久搬遷」。4xx 代表你這個用戶端出了錯,著名的 404 找不到與 403 禁止存取都住在這裡。5xx 代表伺服器壞了,像是 500 內部伺服器錯誤或 503 服務無法使用。光是知道第一個數字,通常就能告訴你問題出在誰身上。
標頭:讓一切運轉的後設資料
第一行與空行之間的那幾行,就是 HTTP 標頭,一疊「名稱─值」對,攜帶著關於這則訊息的一切,唯獨不含內容本身。它們是寫在包裹外面的標籤與指示。Content-Type 說明本文是哪種東西(text/html、image/jpeg、application/json),好讓瀏覽器知道該畫一個頁面還是顯示一張圖。Content-Length 說明本文有幾個位元組,好讓接收方知道何時已讀完整份東西。
其他標頭則負責協商與指示。Accept 讓用戶端說「我比較想要 HTML,但 JSON 也可以」。Cache-Control 告訴各個快取它們可以保留副本多久,這呼應了先前提過的網頁快取與 CDN 概念。Authorization 攜帶憑證。標頭的妙處在於它是開放式的:可以加入新的標頭而不必更動核心協定,這正是 HTTP 能持續好用超過三十年的一大原因。標頭區塊正是 HTTP 大部分彈性的所在。
一條連線,多次請求
一個現代網頁不是單一檔案。它是一份 HTML 文件,外加數十張圖片、樣式表、指令稿與字型,每一個都是位於各自路徑上的各自資源。最早的 HTTP 會為其中每一個單獨開一條全新的 TCP 連線,一次又一次付出三向交握的成本,回覆完一次就拆掉。在一個有五十項資產的頁面上,那就是五十次交握;而且請記得前一階說過,一次交握要花掉一整個來回時間,那是你買再多頻寬也加速不了的,因為你贏不過光速。
解法是持久連線,自 HTTP/1.1 起成為預設。客戶端與伺服器不再回覆一次就甩門,而是讓同一條 TCP 連線保持開啟、一次又一次地重複使用它。一次交握,接著是同一條管子裡一連串的請求與回覆。光是這個改動就大幅縮短了頁面載入時間,因為昂貴的建立成本只付一次,而不是每個資源都付一次。
HTTP/1.1 甚至讓客戶端可以連珠炮似地一口氣發出好幾個請求,不必等每個回覆,這個想法叫做管線化。但有個陷阱會困擾本階的下一篇:回覆必須以請求送出的確切順序回來,所以一個慢吞吞的回應會卡住排在它後面的一切。這就是隊首阻塞,而擊敗它正是 HTTP/2 與 HTTP/3 的故事。眼下先記住這幅畫面:持久連線讓 HTTP 變快了,但還沒讓它變得平行。
HTTP 什麼都不記得
這裡有個塑造了一切建於 HTTP 之上事物的設計選擇:它是一個無狀態協定。每個請求都被獨立處理,彷彿伺服器從沒見過你。伺服器本身不會記得你兩個請求之前登入過,也不記得你的購物車裡有什麼。當這個請求結束時,就 HTTP 而言,你又變回了一個陌生人。
這聽起來像個缺陷,其實是個刻意而強大的取捨。正因為沒有任何請求依賴前一個請求的記憶,一整群伺服器裡的任何一台都能回應任何請求——這正是讓 內容傳遞網路與負載平衡器能把數百萬名使用者分散到許多機器上的關鍵,就像一連串相同的在地倉庫,任何一座都能替你出貨。代價是真正的連續性——保持登入、維持購物車——必須在其上重新建立,而緊接著的下一篇就會說明 cookie 如何做到這件事。無狀態不是 HTTP 不小心忘了;而是 HTTP 故意拒絕記憶,並以此換來可擴展性。