feat(web基础): 新增零基础Web开发课程
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""第5-6课标准示例:启动本地HTTP服务并自动调用图书接口。"""
|
||||
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from threading import Thread
|
||||
from urllib.error import HTTPError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
class BookApiHandler(BaseHTTPRequestHandler):
|
||||
"""处理本课图书查询和新增请求的最小HTTP处理器。"""
|
||||
|
||||
# 本课不连接数据库,使用类属性保存当前程序运行期间的图书。
|
||||
# 程序退出后这份列表会消失,再次运行时会重新从一条图书开始。
|
||||
books = [{"id": 1, "title": "Python入门"}]
|
||||
|
||||
def send_json(
|
||||
self,
|
||||
status: HTTPStatus,
|
||||
data: dict | list,
|
||||
extra_headers: dict | None = None,
|
||||
) -> None:
|
||||
"""把Python对象转换为UTF-8 JSON响应。"""
|
||||
|
||||
# HTTP连接传输的是字节,因此先生成JSON字符串,再编码成UTF-8字节。
|
||||
response_body = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
# Content-Length必须填写字节数量,不能直接计算中文字符串的字符数。
|
||||
self.send_header("Content-Length", str(len(response_body)))
|
||||
if extra_headers is not None:
|
||||
for header_name, header_value in extra_headers.items():
|
||||
self.send_header(header_name, header_value)
|
||||
self.end_headers()
|
||||
self.wfile.write(response_body)
|
||||
|
||||
def do_GET(self) -> None:
|
||||
"""处理GET /books,并拒绝未知路径。"""
|
||||
|
||||
# 标准库收到GET请求后会自动调用本方法,self.path保存请求目标。
|
||||
if self.path == "/books":
|
||||
self.send_json(HTTPStatus.OK, {"items": self.books})
|
||||
return
|
||||
self.send_json(HTTPStatus.NOT_FOUND, {"message": "资源不存在"})
|
||||
|
||||
def do_POST(self) -> None:
|
||||
"""处理POST /books,校验JSON并创建图书。"""
|
||||
|
||||
if self.path != "/books":
|
||||
self.send_json(HTTPStatus.NOT_FOUND, {"message": "资源不存在"})
|
||||
return
|
||||
|
||||
try:
|
||||
# Content-Length告诉服务器本次请求体一共有多少字节。
|
||||
content_length = int(self.headers.get("Content-Length", "0"))
|
||||
request_body = self.rfile.read(content_length).decode("utf-8")
|
||||
request_data = json.loads(request_body)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
self.send_json(HTTPStatus.BAD_REQUEST, {"message": "JSON格式错误"})
|
||||
return
|
||||
|
||||
title = request_data.get("title")
|
||||
if not isinstance(title, str) or not title.strip():
|
||||
self.send_json(
|
||||
HTTPStatus.UNPROCESSABLE_CONTENT,
|
||||
{"message": "书名不能为空"},
|
||||
)
|
||||
return
|
||||
|
||||
# 本课用列表长度模拟生成编号;真实项目通常由数据库生成主键。
|
||||
book = {"id": len(self.books) + 1, "title": title.strip()}
|
||||
self.books.append(book)
|
||||
self.send_json(
|
||||
HTTPStatus.CREATED,
|
||||
book,
|
||||
{"Location": f"/books/{book['id']}"},
|
||||
)
|
||||
|
||||
def log_message(self, format_text: str, *args: object) -> None:
|
||||
"""关闭标准库默认访问日志,让教学输出保持清晰。"""
|
||||
|
||||
|
||||
def send_request(
|
||||
url: str,
|
||||
method: str = "GET",
|
||||
data: dict | None = None,
|
||||
) -> None:
|
||||
"""作为客户端发送HTTP请求并输出响应。"""
|
||||
|
||||
request_body = None
|
||||
headers = {"Accept": "application/json"}
|
||||
if data is not None:
|
||||
request_body = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
|
||||
# Request只负责描述请求;urlopen()执行时才真正连接服务器并发送请求。
|
||||
request = Request(url, data=request_body, headers=headers, method=method)
|
||||
try:
|
||||
with urlopen(request, timeout=3) as response:
|
||||
response_text = response.read().decode("utf-8")
|
||||
print(f"{method} {request.full_url} -> {response.status}")
|
||||
print(f"响应体:{response_text}")
|
||||
except HTTPError as error:
|
||||
response_text = error.read().decode("utf-8")
|
||||
print(f"{method} {request.full_url} -> {error.code}")
|
||||
print(f"响应体:{response_text}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""在随机本地端口启动服务,完成调用后安全关闭。"""
|
||||
|
||||
# 127.0.0.1限制为本机访问;端口0表示让操作系统选择可用端口。
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), BookApiHandler)
|
||||
# 服务器需要持续等待请求,因此放入后台线程;主线程继续充当客户端。
|
||||
server_thread = Thread(target=server.serve_forever, daemon=True)
|
||||
server_thread.start()
|
||||
host, port = server.server_address
|
||||
base_url = f"http://{host}:{port}"
|
||||
print(f"本地服务已启动:{base_url}")
|
||||
|
||||
try:
|
||||
send_request(f"{base_url}/books")
|
||||
send_request(
|
||||
f"{base_url}/books",
|
||||
method="POST",
|
||||
data={"title": "HTTP接口实践"},
|
||||
)
|
||||
send_request(f"{base_url}/books")
|
||||
send_request(f"{base_url}/missing")
|
||||
finally:
|
||||
# 即使某次请求失败,也必须释放监听端口并等待后台线程退出。
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
server_thread.join()
|
||||
print("本地服务已关闭。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user