Learning Objectives
- 說明瀏覽器與網頁伺服器之間的 HTTP 請求與回應
- 用 Flask 的路由把網址對應到 Python 函式,控制 GPIO 上的 LED
- 用網址變數與查詢字串接收參數,回傳文字、狀態碼與 JSON
- 說明 host="0.0.0.0"、連接埠與開發伺服器的安全限制
用手機控制樹莓派
到目前為止,要控制樹莓派得連上終端機、執行程式。如果讓樹莓派當一台網頁伺服器, 同一個網路上的任何手機或電腦,打開瀏覽器輸入網址,就能看到感測器的數值、按按鈕開燈。
這一課用 Flask:一個輕量的 Python 網頁框架,幾行程式就能做出網頁伺服器。
瀏覽器和伺服器在說什麼
瀏覽器每打開一個網址,就送出一個 HTTP 請求;伺服器處理之後送回一個 HTTP 回應:
瀏覽器 → 伺服器:GET /led/on HTTP/1.1 (方法、路徑、版本)
伺服器 → 瀏覽器:HTTP/1.1 200 OK (狀態碼)
LED 已打開 (內容)
| 狀態碼 | 意思 |
|---|---|
| 200 | 成功 |
| 400 | 請求有問題(例如參數不對) |
| 404 | 找不到這個路徑 |
| 500 | 伺服器的程式出錯 |
「計算機概論」軌的 HTTP 課有更完整的介紹。
Flask 的路由
Flask 官方文件裡最小的程式:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
app = Flask(__name__)建立應用程式物件。@app.route("/")是裝飾器:把下面的函式登記成「網址/的處理函式」。這叫路由(route)。- 函式回傳的字串就是回應的內容(預設當成 HTML)。
在樹莓派上執行:
| 寫法 | 誰連得到 |
|---|---|
app.run() |
只有樹莓派自己(127.0.0.1),預設連接埠 5000 |
app.run(host="0.0.0.0", port=5000) |
同一個網路上的其他裝置,網址是 http://樹莓派的IP:5000/ |
網址裡可以有變數:@app.route("/led/<state>") 讓 /led/on、/led/off 都進到同一個函式,state 參數收到 "on" 或 "off"。
模擬器怎麼運作:模擬的 Flask 不會真的開網路連線。電路規格裡有一張「誰在什麼時間送來什麼請求」的時間表,
app.run()依時間收到請求、呼叫你的函式,電路畫面下方會列出收到的請求與送出的回應。
逐步看:網址控制 LED
程式先登記兩個路由,再在第 21 行開始等待。請求的時間表是:0.6 秒 GET /、1.5 秒 GET /led/on、2.5 秒 GET /led/blink(不認得)、3.5 秒 GET /led/off。
看電路畫面下方的請求與回應,以及每次請求執行了哪幾行。
給程式看的網頁:JSON
給人看的網頁回傳 HTML;給其他程式(手機 App、另一台電腦的資料分析程式)用的網址,通常回傳 JSON。 在 Flask 裡,處理函式回傳一個字典,Flask 就會自動轉成 JSON 回應:
@app.route("/api/sensor")
def sensor():
return {"temp": 26.1, "unit": "C"} # 回應:{"temp": 26.1, "unit": "C"}
網址後面 ? 開始的部分叫查詢字串(query string),例如 /api/sensor?unit=F。
Flask 用 request.args.get("unit", "C") 取值;第二個引數是沒有傳時的預設值,這樣網址少了參數也不會出錯。
下一個範例的溫度感測器在 4 秒內從 25°C 升到 29°C,每次有請求時才讀一次。
逐步看:感測器 API 與查詢字串
請求時間表:0.8 秒 GET /api/sensor、2 秒 GET /api/sensor?unit=F、3 秒 GET /api/sensors(多打了一個 s)。看第三個請求:沒有任何一行程式執行,Flask 直接回 404。
小測驗:路由與回應
有路由 @app.route("/led/<state>"),瀏覽器請求 /led/on/now,會怎樣?
處理函式寫 return "不認得的指令", 400,瀏覽器收到的狀態碼是多少?
下面的程式模擬 Flask 處理 /api/sensor?unit=F 時的計算,會印出什麼?
args = {"unit": "F"}
temp = 25.0
unit = args.get("unit", "C")
if unit == "F":
print({"temp": round(temp * 9 / 5 + 32, 1), "unit": "F"})
else:
print({"temp": temp, "unit": "C"})
安全:控制面板只放在自己的網路
能控制硬體的網頁,被陌生人連上就是真實世界的問題(燈被亂開、馬達被亂轉)。Flask 官方文件特別提醒:
flask run或app.run()啟動的是開發用的伺服器,適合測試,不適合正式上線;正式部署要換成專門的伺服器(官方文件的「Deploying to Production」一章)。- 除錯模式(debug)出錯時會在瀏覽器顯示互動式除錯器,可以執行任意 Python 程式碼;絕對不要在別人連得到的網路上開啟。
- 只有在關閉除錯器、或信任同一個網路上的使用者時,才用
host="0.0.0.0"對外開放。
實務上的原則:
- 控制面板只放在家裡或實驗室的區域網路,不要直接開放到網際網路(不要在路由器上設定連接埠轉送)。
- 會改變狀態的動作(開燈、關門)在正式的系統裡要用 POST 請求並加上登入;本課為了簡單用 GET。
- 使用者送來的文字放回網頁前要跳脫(Flask 的範本會自動做),或者像範例一樣不要放回去。
下一課的 MQTT 是另一種讓裝置交換資料的方式,適合很多台裝置一起工作。
小測驗:部署與安全
關於 Flask 的開發伺服器與除錯模式,下面哪一個說法正確?
同學想在宿舍外也能用手機開關房間的燈,最不建議哪一種做法?
重點整理
- 瀏覽器送 HTTP 請求(方法+路徑),伺服器送回應(狀態碼+內容)。
app = Flask(__name__)建立應用程式;@app.route("網址")把網址對應到處理函式。<state>是網址變數;request.args.get("key", 預設值)讀查詢字串。- 回傳字串=HTML/文字;回傳
(內容, 狀態碼)指定狀態碼;回傳字典=JSON。 app.run(host="0.0.0.0", port=5000)讓區域網路上的裝置連線;開發伺服器只用於開發,除錯模式不要對外,控制面板不要直接放上網際網路。