Learning Objectives
- 說明一次 HTTP 請求與回應包含哪些部分,以及 200、201、404 這些狀態碼的意思
- 用 urequests.get 取得 JSON 資料並轉成字典,用 urequests.post 上傳感測值
- 用 try/except 與狀態碼處理網路錯誤,用完記得 close()
- 用 socket 在 ESP32 上寫一個簡單的網頁伺服器,依網址路徑控制 LED
和網路服務說同一種語言
上一課 ESP32 連上了 Wi-Fi,但只是「連上」還不夠,還要能和網路上的服務交換資料。 瀏覽器打開網頁、手機 App 查天氣,背後大多是 HTTP:一方送出請求(request),另一方回傳回應(response)。
這一課 ESP32 兩種角色都當一次:
- 用戶端:向天氣服務要資料、把感測值上傳到伺服器(用
urequests) - 伺服器:讓同一個網路裡的手機用瀏覽器開關 LED(用
socket)
HTTP 長什麼樣子
HTTP 是純文字的格式。瀏覽器打開 http://192.168.1.42/led/on 時,送出的請求大致是:
GET /led/on HTTP/1.1 ← 請求行:方法、路徑、版本
Host: 192.168.1.42 ← 標頭(header),一行一個
← 空行:標頭結束
伺服器的回應:
HTTP/1.1 200 OK ← 狀態行:版本、狀態碼、說明
Content-Type: text/html ← 標頭:內容是 HTML
← 空行
<h1>LED: ON</h1> ← 內容(body)
每一行用 \r\n 結束,標頭和內容之間隔一個空行。
| 常見的方法 | 用途 |
|---|---|
| GET | 取得資料(查天氣、打開網頁) |
| POST | 送出資料(上傳感測值、送出表單) |
| 常見的狀態碼 | 意思 |
|---|---|
| 200 OK | 成功 |
| 201 Created | 成功,而且建立了一筆新資料 |
| 404 Not Found | 找不到這個路徑 |
| 500 Internal Server Error | 伺服器自己出錯 |
本課的
api.example.com是虛構的服務。example.com是 RFC 2606 保留給文件與範例用的網域,不屬於任何真實服務, 模擬器裡的回應是事先設定好的。
JSON:網路服務常用的資料格式
網路服務回傳的資料大多是 JSON(JavaScript Object Notation),和 Python 的字典、串列長得幾乎一樣:
{"city": "Taoyuan", "temp": 27.5, "hourly": [{"hour": 15, "temp": 28}, {"hour": 18, "temp": 26}]}
| JSON | Python |
|---|---|
{ } 物件 |
字典 dict |
[ ] 陣列 |
串列 list |
"文字" |
字串 str |
27.5、28 |
浮點數、整數 |
true/false/null |
True/False/None |
MicroPython 用 urequests 模組送 HTTP 請求,回應物件 r 有這些常用的東西:
| 寫法 | 得到 |
|---|---|
r.status_code |
狀態碼,例如 200 |
r.text |
回應內容(字串) |
r.json() |
把 JSON 內容轉成字典或串列 |
r.close() |
關閉連線、釋放記憶體 |
ESP32 的記憶體不多,每次用完都要 close(),不然連續請求幾次之後可能會記憶體不足。
這個模組在 micropython-lib 裡已經改名為
requests,舊名稱urequests保留下來讓舊程式繼續能用。 本課用urequests,網路上的範例兩種寫法都會看到,用法相同。
逐步看:取得天氣資料
先連上 Wi-Fi(上一課的寫法),再送出 GET 請求。電路畫面下方的網路紀錄會出現送出的請求; 注意第 11 行花了一段時間:請求要經過網路送到伺服器,再等回應回來。
逐步看:上傳感測值,處理網路錯誤
upload() 用 POST 把一筆讀值送到伺服器。程式故意先在還沒連網時呼叫一次,看 try/except 怎麼接住錯誤;連網之後再呼叫一次。
換個角色:ESP32 當網頁伺服器
反過來,讓 ESP32 等別人連進來。這要用比較底層的 socket(通訊端),步驟固定是:
| 步驟 | 程式 | 意思 |
|---|---|---|
| 1 | socket.getaddrinfo("0.0.0.0", 80)[0][-1] |
取得「本機所有網路介面、第 80 埠」的位址;80 是 HTTP 預設的連接埠 |
| 2 | s.bind(addr)、s.listen(1) |
綁定位址,開始等待連線 |
| 3 | cl, remote = s.accept() |
停在這裡,直到有瀏覽器連進來,得到這次連線的 cl |
| 4 | cl.recv(1024) |
讀取請求(bytes) |
| 5 | cl.send(...) |
送出狀態行、標頭、空行、內容 |
| 6 | cl.close() |
關閉這次連線,回到第 3 步等下一個 |
MicroPython 官方教學的網頁伺服器範例也是這個結構。請求的第一行是 GET /led/on HTTP/1.1,
用空白切開,索引 1 就是路徑,程式依路徑決定要做什麼。
真的開發板上,你在同一個 Wi-Fi 的手機瀏覽器輸入 ESP32 的 IP 位址(例如 http://192.168.1.42/led/on)就能連進來。
模擬器裡,瀏覽器的請求依電路規格的時間表送來:1 秒開燈、2 秒看首頁、3 秒關燈。
這種伺服器一次只服務一個連線,也沒有任何密碼保護,只適合在自己的區域網路裡做實驗,不要讓它暴露在公開的網際網路上。
逐步看:用網址開關 LED
看程式停在第 20 行 accept():時間直接跳到下一個請求到達的時刻。每處理完一個請求,網路紀錄會多一筆「回應」。最後一個請求處理完,程式又停在 accept() 等待,直到模擬時間結束。
小測驗:HTTP 與 JSON
伺服器回應的第一行是 HTTP/1.1 404 Not Found,代表什麼?
下面的程式會印出什麼?(r.json() 回傳的就是這樣的字典)
import json
text = '{"city": "Taoyuan", "hourly": [{"hour": 15, "temp": 28}, {"hour": 18, "temp": 26}]}'
data = json.loads(text)
print(data["hourly"][1]["temp"], len(data["hourly"]))
要把感測器讀值送到伺服器存起來,通常用哪一種方法?
小測驗:網頁伺服器
下面的程式會印出什麼?
request = "GET /led/off HTTP/1.1\r\nHost: 192.168.1.42\r\n\r\n"
path = request.split(" ")[1]
print(path, path.startswith("/led"))
網頁伺服器程式執行到 cl, remote = s.accept(),還沒有人連進來,這時程式在做什麼?
回應要寫成 "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" 再接內容。最後為什麼有兩組 \r\n?
重點整理
- HTTP 是一來一回的文字格式:請求行(方法、路徑)+標頭+空行+內容;回應用狀態碼表示結果(200、201、404…)。
r = urequests.get(網址)、urequests.post(網址, json=字典);r.status_code、r.text、r.json(),用完r.close()。- 網路操作包在
try/except OSError裡,失敗時程式不會停,可以稍後重試。 - 網頁伺服器:
bind→listen→ 迴圈裡accept(會等待)→recv讀請求 → 依路徑處理 →send回應 →close。 - 這種簡單的伺服器沒有密碼保護,只在自己的區域網路裡實驗。
下一課介紹物聯網常用的 MQTT:不用一直問伺服器,有新訊息時會主動送到。