(四)服务器把请求交给 Python 应用
1. 最小 WSGI 调用
先写一个只回显方法与路径的程序,观察服务器怎样调用函数:
# 项目文件:demos/wsgi_echo.py
# 服务器构造environ并交入回调;应用返回字节片段,服务器把它们写进HTTP响应。
import json
from wsgiref.simple_server import make_server
def application(environ, start_response):
value = {"method": environ["REQUEST_METHOD"], "path": environ["PATH_INFO"]}
body = json.dumps(value).encode("utf-8")
start_response(
"200 OK",
[("Content-Type", "application/json"), ("Content-Length", str(len(body)))],
)
return [body]
if __name__ == "__main__":
with make_server("127.0.0.1", 8770, application) as server:
print("打开 http://127.0.0.1:8770/hello", flush=True)
server.serve_forever()make_server(主机,端口,应用) 建立服务器,serve_forever 持续接收请求。浏览器打开 /hello 时,服务器调用 application(environ,start_response)。
environ 是请求信息字典,REQUEST_METHOD 为 GET,PATH_INFO 为 /hello;start_response 是服务器交进来的回调,应用用它提供状态行与响应头,随后返回可迭代的字节片段。服务器再把这些内容写回连接。
这套服务器与 Python 应用之间的调用约定叫 WSGI。HTTP 是浏览器与服务器间的网络协议,WSGI 是服务器调用 Python 应用的接口,二者负责不同的一段连接。WSGI 规范
2. 让业务函数收到一致的请求对象
把 environ 的读取集中在 http.py,后面的处理函数就不必反复处理头字段与字节长度:
# 项目文件:web_project/src/webbook/http.py
# 请求先保留字节,按Content-Type解释;响应先编码,再计算真正的字节长度。
import json
from dataclasses import dataclass, field
from http import HTTPStatus
from urllib.parse import parse_qs
class Problem(Exception):
def __init__(self, status, message, fields=None, headers=None):
self.status, self.message = status, message
self.fields, self.headers = fields or {}, headers or []
super().__init__(message)
@dataclass
class Request:
method: str
path: str
query: dict
headers: dict
body: bytes
@classmethod
def read(cls, environ):
try:
length = int(environ.get("CONTENT_LENGTH") or "0")
except ValueError as error:
raise Problem(400, "Content-Length 不是整数") from error
if length < 0:
raise Problem(400, "Content-Length 不能为负")
if length > 16384:
raise Problem(413, "书籍请求体超过16384字节")
body = environ["wsgi.input"].read(length)
if len(body) != length:
raise Problem(400, "请求体没有完整到达")
headers = {
key[5:].lower().replace("_", "-"): value
for key, value in environ.items()
if key.startswith("HTTP_")
}
headers["content-type"] = environ.get("CONTENT_TYPE", "")
return cls(
environ["REQUEST_METHOD"],
environ["PATH_INFO"],
parse_qs(environ.get("QUERY_STRING", ""), keep_blank_values=True),
headers,
body,
)Problem 沿用第五章自定义异常的做法,保存状态、说明、字段错误与需要交回的响应头。Request 的数据类保存方法、路径、查询、头和原始正文。
wsgi.input 是服务器提供的输入流,按 Content-Length 读取本次正文。中文可能占多个 UTF-8 字节,因此长度按字节核对。以 HTTP_ 开头的环境项转成统一的小写头名称;查询串则通过 parse_qs 解释。