Python 課程 – 網路與 API(Networking & APIs)
主題:HTTP 伺服器(http.server)
簡介
在現代的程式開發中,網路服務是不可或缺的基礎。即使是最簡單的原型、測試工具或是教學示範,都需要一個能夠接受 HTTP 請求、回傳回應的伺服器。Python 內建的 http.server 模組提供了零安裝、即時啟動的 HTTP 伺服器,讓開發者不必額外安裝第三方套件,就能在本機或局域網上快速建立靜態或簡易動態的服務。
本篇文章將從 核心概念、實作範例、常見陷阱與最佳實踐、以及 實際應用場景 四個面向,深入說明 http.server 的使用方式,協助讀者從「能跑起來」到「寫得安全、可維護」的階段。
核心概念
1. http.server 的基本結構
http.server 其實是由兩個主要類別組成:
| 類別 | 功能說明 |
|---|---|
http.server.HTTPServer |
繼承自 socketserver.TCPServer,負責監聽 TCP 連線、接受請求並將其交給處理器(Handler)。 |
http.server.BaseHTTPRequestHandler |
抽象基底類別,定義了 do_GET()、do_POST() 等方法,負責解析請求、產生回應。 |
當我們直接使用 python -m http.server 時,Python 會自動建立一個 SimpleHTTPRequestHandler(繼承自 BaseHTTPRequestHandler),並將當前目錄的檔案以 靜態檔案伺服器 的方式提供。
2. 為什麼要自訂 Handler?
內建的 SimpleHTTPRequestHandler 已足以服務靜態檔案,但在以下情況下,我們往往需要自行撰寫 Handler:
- 動態產生內容(例如 JSON API、表單回應)
- 自訂路由(不同 URL 對應不同的處理邏輯)
- 加入認證、日誌、CORS 等額外功能
自訂 Handler 的方式很簡單:繼承 BaseHTTPRequestHandler,覆寫 do_GET()、do_POST() 等方法,然後把它傳給 HTTPServer。
3. HTTPServer 的執行流程
- 建立 socket:綁定 IP(預設
0.0.0.0)與埠號(預設8000)。 - 等待連線:
serve_forever()會持續阻塞,直到收到中斷訊號(Ctrl+C)或程式自行結束。 - 分派請求:每當有新連線進來,
HTTPServer會產生一個Handler實例,呼叫對應的do_*方法。
⚠️ 小提醒:
HTTPServer預設是 單執行緒(single‑threaded),同時只能處理一個請求。若需要同時處理多個連線,請改用ThreadingHTTPServer或ForkingHTTPServer(Linux 專用)。
4. 常用屬性與方法
| 屬性/方法 | 說明 |
|---|---|
self.path |
請求的 URL 路徑(包含 query string)。 |
self.headers |
http.client.HTTPMessage 物件,存放所有請求標頭。 |
self.send_response(code, message=None) |
發送狀態碼(例如 200、404)。 |
self.send_header(key, value) |
加入單一回應標頭。必須在 end_headers() 前呼叫。 |
self.end_headers() |
結束標頭區塊,之後才能寫入內容。 |
self.wfile.write(bytes) |
實際寫入回應內容(必須是 bytes)。 |
self.rfile.read(n) |
讀取請求主體(例如 POST 的表單資料)。 |
程式碼範例
以下示範 5 個實用範例,從最簡單的靜態伺服器到支援 JSON API、檔案上傳、跨來源資源共享(CORS)與多執行緒。
範例 1:最簡單的靜態檔案伺服器(單行指令)
# 在終端機執行以下指令,即可在當前目錄啟動 8000 埠的 HTTP 伺服器
python -m http.server 8000
說明:等同於
python -c "from http.server import SimpleHTTPRequestHandler, HTTPServer; HTTPServer(('0.0.0.0',8000), SimpleHTTPRequestHandler).serve_forever()",不需要寫任何程式碼。
範例 2:自訂 Handler,回傳簡易的 HTML 頁面
# file: simple_server.py
from http.server import HTTPServer, BaseHTTPRequestHandler
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
# 只處理根目錄 /
if self.path == '/':
self.send_response(200) # 200 OK
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.end_headers()
html = """<!DOCTYPE html>
<html><head><title>Hello</title></head>
<body><h1>歡迎使用自訂 HTTP 伺服器</h1></body></html>"""
self.wfile.write(html.encode('utf-8')) # 必須是 bytes
else:
self.send_error(404, "找不到頁面")
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 8080), MyHandler)
print('伺服器啟動於 http://0.0.0.0:8080')
server.serve_forever()
重點:使用
send_error()可自動產生錯誤頁面,省去自行寫 404 HTML。
範例 3:提供 JSON API(GET & POST)
# file: json_api.py
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
class APIHandler(BaseHTTPRequestHandler):
# 讓瀏覽器或其他客戶端可以跨域呼叫
def _set_cors_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
def do_OPTIONS(self):
self.send_response(204) # No Content
self._set_cors_headers()
self.end_headers()
def do_GET(self):
if self.path == '/api/time':
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self._set_cors_headers()
self.end_headers()
data = {'time': self.date_time_string()}
self.wfile.write(json.dumps(data).encode('utf-8'))
else:
self.send_error(404, "API not found")
def do_POST(self):
if self.path == '/api/echo':
content_len = int(self.headers.get('Content-Length', 0))
raw_body = self.rfile.read(content_len)
try:
payload = json.loads(raw_body)
except json.JSONDecodeError:
self.send_error(400, "Invalid JSON")
return
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self._set_cors_headers()
self.end_headers()
# 回傳相同內容,示範 echo 功能
self.wfile.write(json.dumps(payload).encode('utf-8'))
else:
self.send_error(404, "API not found")
if __name__ == '__main__':
httpd = HTTPServer(('127.0.0.1', 5000), APIHandler)
print('JSON API 伺服器啟動於 http://127.0.0.1:5000')
httpd.serve_forever()
說明:
do_OPTIONS處理 CORS 先行請求(pre‑flight)。self.date_time_string()取得符合 HTTP 標準的日期字串。- 使用
json.dumps()與encode('utf-8')產生回傳的 bytes。
範例 4:支援檔案上傳(multipart/form-data)
# file: upload_server.py
import cgi
import os
from http.server import HTTPServer, SimpleHTTPRequestHandler
UPLOAD_DIR = 'uploads'
os.makedirs(UPLOAD_DIR, exist_ok=True)
class UploadHandler(SimpleHTTPRequestHandler):
def do_POST(self):
# 只接受 /upload 路徑
if self.path != '/upload':
self.send_error(404, "Not Found")
return
# 解析 multipart/form-data
ctype, pdict = cgi.parse_header(self.headers.get('Content-Type'))
if ctype != 'multipart/form-data':
self.send_error(400, "Invalid Content-Type")
return
pdict['boundary'] = bytes(pdict['boundary'], "utf-8")
fields = cgi.parse_multipart(self.rfile, pdict)
# 取得上傳的檔案(假設表單欄位名稱是 file)
uploaded_files = fields.get('file')
if not uploaded_files:
self.send_error(400, "No file uploaded")
return
for i, file_data in enumerate(uploaded_files):
filename = f'upload_{i}.bin'
filepath = os.path.join(UPLOAD_DIR, filename)
with open(filepath, 'wb') as f:
f.write(file_data)
# 回傳成功訊息
self.send_response(200)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.end_headers()
self.wfile.write(b'File uploaded successfully!')
if __name__ == '__main__':
httpd = HTTPServer(('0.0.0.0', 8081), UploadHandler)
print('檔案上傳伺服器啟動於 http://0.0.0.0:8081')
httpd.serve_forever()
重點:
- 透過
cgi.parse_multipart解析 multipart 表單。- 為避免檔名衝突,示範用編號方式儲存。實務上應使用
uuid或原始檔名並做好 安全檢查(避免目錄穿越)。
範例 5:多執行緒 HTTP 伺服器(同時處理多個請求)
# file: threaded_server.py
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
# 直接使用內建的 SimpleHTTPRequestHandler,支援靜態檔案
if __name__ == '__main__':
server = ThreadingHTTPServer(('0.0.0.0', 9000), SimpleHTTPRequestHandler)
print('Threaded HTTP 伺服器啟動於 http://0.0.0.0:9000')
try:
server.serve_forever()
except KeyboardInterrupt:
print('\n伺服器關閉')
server.server_close()
說明:
ThreadingHTTPServer為HTTPServer的子類別,會為每個連線建立一個新執行緒,讓多個使用者能同時取得檔案或 API 回應。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 建議的解決方式 |
|---|---|---|
使用單執行緒 HTTPServer |
同時多個客戶端請求會排隊,導致延遲甚至超時。 | 在開發階段使用 ThreadingHTTPServer;正式上線則考慮使用 WSGI(如 gunicorn)或 反向代理(Nginx) |
| 直接回傳使用者上傳的檔名 | 可能造成 目錄穿越(../../etc/passwd)或檔名衝突。 |
只接受白名單字元、使用 os.path.basename 或 uuid.uuid4() 產生安全檔名 |
未設定 Content-Type |
瀏覽器無法正確解析回傳資料,API 使用者會收到錯誤格式。 | 始終使用 self.send_header('Content-Type', ...),對 JSON 使用 application/json |
| 缺乏 CORS 設定 | 前端跨域請求被瀏覽器阻擋。 | 在 API 回應中加入 Access-Control-Allow-Origin: *(或限定來源) |
忘記 end_headers() |
回傳的 HTTP 標頭不完整,客戶端會收到錯誤或無法解析。 | 每次在寫入 wfile 前呼叫 self.end_headers() |
使用 print 輸出除錯訊息 |
會與 HTTP 回應混在一起,導致協定錯誤。 | 使用 logging 模組,或將除錯訊息寫入檔案/標準錯誤 |
| 未捕捉例外 | 程式崩潰導致伺服器停止服務。 | 在 do_* 方法內部使用 try/except,並回傳適當的 HTTP 錯誤碼(如 500) |
其他最佳實踐
- 使用
with管理檔案:確保檔案資源正確關閉。 - 限制請求大小:對於上傳或 POST 請求,檢查
Content-Length,防止 DoS 攻擊。 - 啟用 HTTPS:
http.server本身不支援 TLS,開發測試時可搭配ssl.wrap_socket,正式環境建議使用反向代理(Nginx/Apache)提供 TLS。 - 日誌紀錄:
BaseHTTPRequestHandler.log_message()可自訂輸出格式,或直接使用logging。 - 分離路由與處理邏輯:即使是簡易伺服器,也建議把路由表寫成字典,讓程式易於擴充。
實際應用場景
| 場景 | 為何選擇 http.server |
可能的擴充方式 |
|---|---|---|
| 前端開發時的靜態資源伺服器 | 零設定、即時啟動、可直接瀏覽目錄結構。 | 加入 ThreadingHTTPServer、使用 --directory 指定根目錄。 |
| 快速原型 API | 只需要幾行程式碼即可提供 JSON 端點,方便前端測試。 | 加入 CORS、簡易驗證(API key)或使用 uvicorn、FastAPI 進一步擴充。 |
| 教育訓練或實驗室 | 學生可直接在本機建立自己的伺服器,了解 HTTP 流程。 | 結合 socket、asyncio 示範非同步 I/O。 |
| IoT 裝置的本地管理介面 | 裝置資源受限,無法安裝完整的 Web 框架。 | 以 ThreadingHTTPServer 提供簡易設定頁面,配合 cgi 處理表單。 |
| 自動化測試的 Mock Server | 測試程式需要一個可預測回應的 HTTP 端點。 | 在 do_GET / do_POST 中回傳固定 JSON,或根據 URL 讀取本機檔案做回應。 |
小結:
http.server不是用來建置大型商業系統的解決方案,但在 開發階段、測試環境、教學示範 中,它的「即開即用」特性可以大幅提升工作效率。
總結
http.server是 Python 標準函式庫中最輕量的 HTTP 伺服器,適合 快速測試、原型開發與教學。- 透過 自訂 Handler,我們可以實作靜態檔案、JSON API、檔案上傳、CORS 等常見需求。
- 為了避免 單執行緒瓶頸、安全漏洞,建議使用
ThreadingHTTPServer、做好檔名過濾、限制請求大小、加入日誌與錯誤處理。 - 在實務上,
http.server常被用作 本機開發伺服器、Mock API、IoT 裝置管理介面,配合反向代理或更完整的 WSGI 框架即可滿足生產環境需求。
掌握了這些概念與技巧後,你就能在任何需要臨時提供 HTTP 服務的情境下,快速部署一個 安全、可維護 的伺服器,為後續的更高階開發奠定堅實基礎。祝你玩得開心、寫得順利! 🎉