1.WSGI
Python 的很多知名的Web框架(例如Flask, Django)实际上都是遵从了这个wsgi模型。
WSGI Server作用
- 监听HTTP服务端口(TCPServer, 80端口)
- 接收浏览器端HTTP请求并解析封装成environ环境数据
- 负责调用应用程序,将environ和start_response方法传入
- 将应用程序响应的正文封装成HTTP响应报文返回浏览器端
WSGI APP要求
- 应用程序是一个可调用对象(函数、类都可以)
- 这个可调用对象应该接收两个参数(environ, start_response)
- 调用start_response
- 最后必须返回一个可迭代对象
2.简单Web框架实现
首先说明这篇文章的目的是尽可能用最少的库代码去搭建简易的web框架,是笔者在探索web框架背后原理时自己实现的demo,有什么不对之处,欢迎指正。
让我们从一个简单的例子开始。这个例子是将environ里的key, value打印出来,后面在编写Request的时候,要知道这里面都有什么内容。这个例子比较典型了,很多地方都有实现,能帮助我们看看最简易模型。
# coding=utf-8
from werkzeug.serving import run_simple
def application(environ,start_response):
from io import StringIO
stdout = StringIO()
print("Hello world!", file=stdout)
print(file=stdout)
for k, v in sorted(environ.items()):
print(k, '=', repr(v), file=stdout)
start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
return [stdout.getvalue().encode("utf-8")]
if __name__ == '__main__':
run_simple("127.0.0.1", 8000, application)
让我们访问http://127.0.0.1:8000?user=admin&music=Rock看看返回什么。记住几个非常有用的字段,如REQUEST_METHOD
,PATH_INFO
,QUERY_STRING
,解析这些字段可以知道用户请求的资源、路由是什么。
因此一个简单的Request类诞生了。
class Request:
def __init__(self, environ):
self.method = environ.get("REQUEST_METHOD", "GET")
self.path = environ.get("PATH_INFO", "")
self.query = environ.get("QUERY_STRING", "")
self.args = self.parse(self.query)
@staticmethod
def parse(data: str, sep="&"):
if not data:
return {}
return dict(map(lambda arg: arg.partition("=")[::2], [arg for arg in data.split(sep)]))
另外,让我们看看响应类该如何写,很简单,它只需要在返回内容前调用start_response, 第一个参数是状态字(包含状态码和状态消息),第二个参数是headers内容,headers是一个列表,列表里面是一个元组。返回的正文内容是一个列表,列表里的内容是bytes。
CODE_MAP = {
200: "OK",
404: "Not Found",
}
UNKNOWN = "Unknown"
class Response:
default_charset = "utf-8"
default_status = 200
default_mimetype = "text/plain"
def __init__(
self,
response=None,
status=None,
headers=None,
mimetype=None,
content_type=None,
):
if status is None:
status = self.default_status
if isinstance(status, int):
self.status = "{} {}".format(status, CODE_MAP.get(status, UNKNOWN))
else:
self.status = status
if mimetype is None:
mimetype = self.default_mimetype
if content_type is None:
content_type = ";".join([mimetype, "charset={}".format(self.default_charset)])
if headers is not None:
self.headers = headers
else:
self.headers = {}
if "Content-Type" not in self.headers:
self.headers["Content-Type"] = content_type
if response is None:
self.response = []
elif isinstance(response, (bytes, bytearray, str)):
self.response = self.set_data(response)
else:
self.response = self.set_data(str(response))
def __call__(self, environ, start_response):
start_response(self.status, self._headers)
return self.response
@property
def _headers(self):
return [(k, v) for k, v in self.headers.items()]
def set_data(self, text):
if isinstance(text, str):
value = text.encode(self.default_charset)
else:
value = bytes(text)
return [value]
注意response设置成了可调用(定义了__call__函数),先调用start_response, 再返回响应正文。至此,application函数可以修改为:
def application(environ,start_response):
request = Request(environ)
msg = "I am {user}, I love {music} music".format(**request.args)
return Response(msg)(environ, start_response)
请还是用http://127.0.0.1:8000?user=admin&music=Rock访问,不然会报错,效果如下。
Web框架一个很重要的功能就是路由,那我们该怎么去实现呢,我们知道flask框架所采用的是装饰器来实现路由,这里也想用装饰器来实现, 而且从外观上想和flask保持一致,其他功能暂不实现,务求简单。
NOT_FOUND = Response("Not Found, 404", 404)
NOT_AUTH = Response("No Auth, please connect to administrator", 403)
class WebFramework:
default_host = "127.0.0.1"
default_port = 8000
def __init__(self, name=None):
self.name = name if name else __name__
self.map = {}
def application(self, environ, start_response):
request = Request(environ)
method, path = request.method, request.path
if not (method in self.map and path in self.map[method]):
return NOT_FOUND(environ, start_response)
response = self.map[method][path](request)
if isinstance(response, Response):
return response(environ, start_response)
return Response(response)(environ, start_response)
def run(self, host=None, port=None, debug=False):
if host is None:
host = self.default_host
if port is None:
port = self.default_port
debug = bool(debug)
run_simple(host, port, self.application, use_debugger=debug)
def route(self, path, methods=("GET",)):
def decorator(func):
for method in methods:
self.map.setdefault(method, {})[path] = func
return func
return decorator
app = WebFramework()
@app.route("/")
def index(request):
return "Hello, I am {user}, I love {music} music".format(**request.args)
if __name__ == '__main__':
app.run()
可以看到,这样就实现了路由功能。
在实际web应用中,常用的功能是登录,由于http是无状态的,而登录后跳转到其它页面,服务器还能知道我是哪个用户么,于是出现了会话管理,未完待续。。。
3.会话管理
- HTTP 是无状态的协议
每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次请求的发送者是不是同一个客户端。服务器与浏览器为了进行会话跟踪,就必须主动的去维护一个状态,这个状态用于告诉服务端前后两个请求是否来自同一个浏览器。这个状态的实现方式可以通过 cookie 、session或token来实现(请参考Cookie、Session、Token、JWT 傻傻分不清楚可不行)
- Cookie
先让我们从最简单的cookie开始,cookie的解析实际上是从HTTP_COOKIE
字段获取而来, 为了实现登录功能,我们还特意实现了解析form表单方法。
class Request:
def __init__(self, environ):
self.method = environ.get("REQUEST_METHOD", "GET")
self.path = environ.get("PATH_INFO", "")
self.query = environ.get("QUERY_STRING", "")
self.args = self.parse(self.query)
self.form = self.parse_form(environ)
self.cookie = self.parse(environ.get('HTTP_COOKIE'))
@staticmethod
def parse(data: str, sep="&"):
if not data:
return {}
return dict(map(lambda arg: arg.partition("=")[::2], [arg for arg in data.split(sep)]))
def parse_form(self, environ):
buffer_size = int(environ.get("CONTENT_LENGTH", 0))
data = environ['wsgi.input'].read(buffer_size).decode("utf-8") if buffer_size else ""
return self.parse(data)
除了解析,就是生成cookie, 我们可以在Response里实现。
from werkzeug.http import dump_cookie
class Response:
"""省略之前代码"""
def set_cookie(self, key, value, max_age=None, expires=None):
self.headers["Set-Cookie"] = dump_cookie(key, value, max_age=max_age, expires=expires)
def delete_cookie(self, key):
self.set_cookie(key, "", max_age=0, expires=0)
另外让我们一起写写简单的视图页面,并为了验证登录,创建了一个假的数据库字典。主页视图需要验证cookie字段是否有username字段,如果没有,则会跳转到登录页面。登录页面判断请求方式,如果是POST请求,验证用户输入的用户名和密码,如果验证通过,保存Cookie并跳转到首页,首页再次访问的时候已经有了Cookie, 验证ok就可以显示欢迎页。登录页面如果是GET请求,则展示登录框页面,让用户输入用户名密码。
fake_db = {
"users": {
"admin": "admin",
"huge": "huge",
}
}
def redirect(location, code=302, response=None):
if response is None:
response = Response("redirect page", status=code)
if not isinstance(location, str):
raise ValueError
response.headers["Location"] = location
return response
@app.route("/")
def index(request):
if request.cookie.get("username"):
return show_index(request)
return redirect("/login")
@app.route("/login", methods=["POST", "GET"])
def login(request):
if request.method == "POST":
return do_login(request)
return show_login()
def do_login(request):
username = request.form.get("username")
password = request.form.get("password")
if username in fake_db["users"] and password == fake_db["users"][username]:
response = Response(status=302)
response.set_cookie("username", username)
return redirect("/", response=response)
return NOT_AUTH
def show_login():
return Response("""
登录
""", mimetype="text/html")
@app.route("/logout")
def do_logout(request):
response = Response("logout success")
response.delete_cookie("username")
return response
def show_index(request):
username = request.cookie.get("username")
return Response("""
主页
你好,{user}
""".format(user=username), mimetype="text/html")
注意这里实现了一个简单的页面跳转函数,其实原理也很简单,就是将返回响应的headers的Location字段设置值为跳转路由,响应状态码设置为302。
让我们看看这个Cookie实现的效果,再进行简单的登录后,页面跳回首页欢迎页,重复刷新页面,已经不需要验证登录了。可是从这个实现方式,我们也看出了Cookie的弊病,那就是信息存储在浏览器端,数据透明,容易被伪造。
- Session
Session一般是配合Cookie一起使用,服务器响应浏览器端的请求,并返回唯一标识信息 sessionid 给浏览器,浏览器把返回的 sessionid 存储到 cookie 中,同时 cookie 记录 sessionid 属于哪个域名,当用户第二次访问服务器的时候,请求自动判断此域名下是否存在 cookie 信息,如果存在就将 cookie 信息发送给服务端,服务端会从 cookie 中获取 sessionid,再根据 sessionid 查找对应的 session 信息,如果找到 session 证明用户已经登录,可以执行后面操作,如果没有找到,说明用户没有登录或者失败。