Learning Objectives
- 說明瀏覽器與伺服器各自做什麼,以及一次請求與回應裡有哪些部分
- 拆解一個 URL:通訊協定、主機、連接埠、路徑、查詢字串、片段
- 說出常見 HTTP 方法的語意,並從狀態碼判斷問題出在哪一邊
- 說明 HTTP 無狀態的意思,以及整頁回應與 AJAX 部分更新的差別
這一軌要做什麼
學完這一軌,你可以自己寫出有登入、有資料庫、有 API 的小網站,並且知道怎麼部署。工具是 Python 的 Flask,只要學過 Python 軌 前 15 課就能開始。
寫程式之前,先弄清楚你的程式要放在什麼位置。上網時有兩個角色:
- 瀏覽器(client,用戶端):在你的電腦或手機上,負責發出請求,並把收到的內容畫成畫面。
- 伺服器(server):在遠端的電腦上,一直等著請求進來,處理後送回回應。
瀏覽器 ── 請求 ──▶ 伺服器 ◀─▶ 資料庫
瀏覽器 ◀─ 回應 ─── 伺服器
後端開發寫的就是伺服器這一邊的程式:收到請求,查資料、做判斷,然後決定回什麼。瀏覽器和伺服器之間說的語言叫 HTTP,這一課先把它看清楚,後面每一課都會用到。
URL 的組成
URL 是網路上資源的位址。https://notes.example.com:8443/notices/12?tag=exam&page=2#comments 這個網址可以拆成六段:
https:// 通訊協定
notes.example.com 主機
:8443 連接埠
/notices/12 路徑
?tag=exam&page=2 查詢字串
#comments 片段
| 部分 | 用途 |
|---|---|
| 通訊協定 | 用什麼方式連線。https 會加密,http 不會 |
| 主機 | 要連到哪一台伺服器 |
| 連接埠 | 同一台電腦上的哪一個服務。https 預設 443、http 預設 80,用預設值時可以省略 |
| 路徑 | 要伺服器上的什麼資源。Flask 主要靠它決定交給哪個函數處理 |
| 查詢字串 | ? 後面的 名稱=值,用 & 隔開,常用來傳篩選條件、頁碼 |
| 片段 | # 後面的部分,瀏覽器用來捲到頁面的某個位置,不會送到伺服器 |
逐步看:把網址拆開
Python 標準函式庫的 urlsplit 可以拆網址。對照上面的表格,看每一段的值。
一次請求與回應裡有什麼
HTTP/1.1 的訊息就是一段照格式寫的文字。瀏覽器送出的請求長這樣:
POST /feedback HTTP/1.1
Host: notes.example.com
Content-Type: application/x-www-form-urlencoded
Cookie: theme=dark
name=amy&text=hello
- 第一行:方法、路徑、版本
- 接下來是標頭(header),一行一個「名稱: 值」,放關於這則訊息的資訊,例如內容的類型、Cookie、瀏覽器的種類
- 一個空行
- 內容(body),不一定有
伺服器回的回應結構相同,只有第一行換成版本、狀態碼與說明:
HTTP/1.1 201 Created
Content-Type: text/html; charset=utf-8
Content-Length: 18
<h1>已收到</h1>
內容可以是網頁、圖片或 JSON,由 Content-Type 標頭說明是哪一種。
HTTP/2 之後,傳輸時改成二進位格式,但方法、路徑、標頭、狀態碼這些概念都一樣。
逐步看:HTTP 訊息只是文字
自己把一段請求文字拆開。Flask 不用你做這件事,但知道它只是文字之後,後面看到 request.headers、request.form 就知道資料從哪裡來。(實際傳輸的換行是 \r\n,這裡為了好讀用 \n。)
HTTP 方法與狀態碼
方法說明這個請求想做什麼。瀏覽器開網頁、點連結用的是 GET,送表單通常是 POST;其他方法在第 11 課設計 API 時會用到。
| 方法 | 語意 | 常見用途 |
|---|---|---|
| GET | 取得資料,不應該改變伺服器上的資料 | 開網頁、搜尋 |
| POST | 送出資料,通常會造成改變 | 送出表單、新增留言 |
| PUT | 用送來的內容取代整筆資料 | 更新一筆資料 |
| PATCH | 只改資料的一部分 | 更新部分欄位 |
| DELETE | 刪除資料 | API 的刪除 |
狀態碼是回應第一行的三位數字,第一個數字代表類別。看到錯誤時先看它,就知道該往哪邊找:
| 範圍 | 類別 | 常見的例子 |
|---|---|---|
| 1xx | 資訊 | 很少直接碰到 |
| 2xx | 成功 | 200 OK、201 Created |
| 3xx | 轉址 | 301、302:資源在別的網址,瀏覽器會自動再請求一次 |
| 4xx | 用戶端錯誤:請求本身有問題 | 400 格式錯誤、401 還沒登入、403 沒有權限、404 找不到、405 不支援這個方法 |
| 5xx | 伺服器錯誤:程式或伺服器出問題 | 500 程式發生未處理的錯誤、503 暫時無法服務 |
4xx 和 5xx 的分別很實用:4xx 通常要看請求送了什麼,5xx 就要回頭看伺服器的程式與紀錄。
HTTP 是無狀態的
無狀態(stateless)的意思是:同一個瀏覽器連續送兩個請求,HTTP 本身不會把它們連在一起。伺服器處理第二個請求時,不會因為協定而知道你剛剛做過什麼。
但是網站明明記得你登入了。這是因為回應可以叫瀏覽器存一小段資料(Cookie),瀏覽器之後每次請求都把它放進標頭帶回去,伺服器再從裡面認出你。下面的範例用函數模擬這件事,第 14 課會實際操作。
逐步看:伺服器不記得上一個請求
把 server 想成伺服器,每次呼叫就是一個請求。注意第二次呼叫時它能看到什麼。
整頁回應與部分更新
伺服器回應的內容有兩種常見的架構:
| 整頁回應 | AJAX | |
|---|---|---|
| 誰送出請求 | 瀏覽器(點連結、送表單) | 網頁裡的 JavaScript |
| 伺服器回什麼 | 完整的 HTML | JSON 或部分 HTML |
| 畫面的變化 | 整個換掉 | 只更新一小塊 |
點連結、送表單是整頁回應,畫面會整個重新畫過。AJAX 則由網頁裡的 JavaScript 在背景送請求,現在通常用 fetch,拿到資料後只改畫面的一部分,例如按讚數、搜尋建議。第 2 到 10 課先做整頁回應,第 11 到 13 課再做 API 與 fetch。
JSON 是一種文字的資料格式,寫法和 Python 的字典、串列很像,例如 {"seats": 12}。
動手做:判斷狀態碼、拆第一行
完成兩個函數。status_class 依狀態碼回傳類別名稱;parse_request_line 拆開請求的第一行,回傳方法、路徑、查詢字串,沒有查詢字串時是空字串。程式最下面的測試不要改。
想一想
網址 https://example.com/a?x=1#top 裡,哪一段不會送到伺服器?
下面的程式會印出什麼?
from urllib.parse import urlsplit
parts = urlsplit('https://a.example.com/x/y?k=1#top')
print(parts.path)
使用者回報網站出現 500 Internal Server Error,第一步應該看哪裡?
「HTTP 是無狀態的」是什麼意思?
按下「按讚」後,畫面沒有整個重新載入,只有數字變了。比較可能的做法是?
重點整理
- 瀏覽器發出請求,伺服器送回回應;後端程式寫在伺服器這一邊
- URL 分成通訊協定、主機、連接埠、路徑、查詢字串、片段;片段不會送到伺服器
- 請求與回應都是「第一行+標頭+空行+內容」
- GET 取得資料,POST 送出資料;4xx 先看請求,5xx 先看伺服器
- HTTP 無狀態,網站靠 Cookie 認得你
- 整頁回應換掉整個畫面,AJAX 只更新一部分
下一課在自己的電腦上裝好 Flask,寫出第一個網站。