起因 🤔
因为好奇一个 Python Wsgi Web Framework 内部结构,就想“解剖”个看看,挑一个代码量不是那么多的 8,
就它了-> Bottle。
What is wsgi?
WSGI 全称是 Web Server Gateway Interface,其主要作用是 Web 服务器与 Python Web 应用程序或框架之间的建议标准接口,以促进跨各种 Web 服务器的 Web 应用程序可移植性。
WSGI 并不是框架而只是一种协议,我们可以将 WSGI 协议分成三个组件 Application,Server,Middleware 和协议中传输的内容。
将这三个组件对映射到我们具体使用的组件是:
- Server: 常用的有 uWSGI,gunicorn 等
- Application: Django,Flask 等
- Middleware: route
Application
WSGI 规定每个 python 程序(Application)必须是一个可调用的对象(实现了call 函数的方法或者类),接受两个参数 environ
(WSGI 的环境信息) 和 start_response
(开始响应请求的函数),并且返回 iterable
。
environ
和 start_response
由 http server 提供并实现
environ
变量是包含了请求和环境信息的字典
- Application 内部在返回前调用
start_response
start_response
也是一个 callable,接受两个必须的参数,status
(HTTP 状态)和 response_headers
(响应消息的头)
- 可调用对象要返回一个值,这个值是可迭代的。
看看 Bottle 中怎么实现的吧(为了阅读方便,删除了部分代码):
查看 Bottle.__call__
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# Bottle
...
def __call__(self, environ, start_response): # app 可回调
''' Each instance of :class:'Bottle' is a WSGI application. '''
return self.wsgi(environ, start_response)
def wsgi(self, environ, start_response):
""" The bottle WSGI-interface. """
try:
out = self._cast(self._handle(environ)) # 相关处理
start_response(response._status_line, response.headerlist) # 调用服务器程序提供的 start_response,填入两个参数
return out # 返回必须是 iterable
except Exception:
# 错误处理 ...
environ['wsgi.errors'].write(err)
headers = [('Content-Type', 'text/html; charset=UTF-8')]
start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info())
return [tob(err)] # tob: str -> bytes
...
|
中间层 Middleware
from https://cizixs.com/2014/11/08/understand-wsgi/
有些程序可能处于服务器端和程序端两者之间:对于服务器程序,它就是应用程序;而对于应用程序,它就是服务器程序。这就是中间层 middleware。middleware 对服务器程序和应用是透明的,它像一个代理/管道一样,把接收到的请求进行一些处理,然后往后传递,一直传递到客户端程序,最后把程序的客户端处理的结果再返回。
middleware 做了两件事情:
- 被服务器程序(有可能是其他 middleware)调用,返回结果回去
- 调用应用程序(有可能是其他 middleware),把参数传递过去
PEP 333
上面给出了 middleware 的可能使用场景:
- 根据 url 把请求给到不同的客户端程序(url routing)
- 允许多个客户端程序/web 框架同时运行,就是把接到的同一个请求传递给多个程序。
- 负载均衡和远程处理:把请求在网络上传输
- 应答的过滤处理
WSGI application 非常重要的特点是:它是可以嵌套的。
换句话说,可以写个 application,它做的事情就是调用另外一个 application,然后再返回(类似一个 proxy)。
一般来说,嵌套的最后一层是业务应用,中间就是 middleware。
这样的好处是,可以解耦业务逻辑和其他功能,比如限流、认证、序列化等都实现成不同的中间层,不同的中间层和业务逻辑是不相关的,可以独立维护;而且用户也可以动态地组合不同的中间层来满足不同的需求。(from https://cizixs.com/2017/01/11/flask-insight-start-process/)
Example: “Hello World” in a bottle
1
2
3
4
5
6
7
|
from bottle import route, run, template
@route('/hello/<name>')
def index(name):
return template('<b>Hello {{name}}</b>!', name=name)
run(host='localhost', port=8080)
|
看到上面代码就会有个疑问?app 在哪里:
1
2
3
4
5
|
# 初始化
app = default_app = AppStack() # AppStack 并实现类似于堆栈的 API
app.push() # push 方法中压入了 Bottle 的实例
app() # 使用堆栈顶部作为默认应用程序
|
启动流程
启动代码是 run
函数,这个代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
def run(app=None, server='wsgiref', host='127.0.0.1', port=8080,
interval=1, reloader=False, quiet=False, plugins=None,
debug=None, **kargs):
""" Start a server instance. This method blocks until the server terminates.
"""
try:
app = app or default_app() # 获取默认的 app
for plugin in plugins or []: # 安装插件
app.install(plugin)
if server in server_names: # default get the `WSGIRefServer` class.
server = server_names.get(server)
if isinstance(server, type):
server = server(host=host, port=port, **kargs)
server.quiet = server.quiet or quiet
if not server.quiet:
_stderr("Bottle v%s server starting up (using %s)...\n" % (__version__, repr(server)))
_stderr("Listening on http://%s:%d/\n" % (server.host, server.port))
_stderr("Hit Ctrl-C to quit.\n\n")
server.run(app) # run
except KeyboardInterrupt:
pass
|
Bottle
中的 server_names
支持如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
server_names = {
'cgi': CGIServer,
'flup': FlupFCGIServer,
'wsgiref': WSGIRefServer,
'waitress': WaitressServer,
'cherrypy': CherryPyServer,
'paste': PasteServer,
'fapws3': FapwsServer,
'tornado': TornadoServer,
'gae': AppEngineServer,
'twisted': TwistedServer,
'diesel': DieselServer,
'meinheld': MeinheldServer,
'gunicorn': GunicornServer,
'eventlet': EventletServer,
'gevent': GeventServer,
'geventSocketIO':GeventSocketIOServer,
'rocket': RocketServer,
'bjoern' : BjoernServer,
'auto': AutoServer,
}
|
再去看 Bottle.wsgi
的方法,处理语句 self._cast(self._handle(environ))
。
Bottle._handle
处理请求方法的核心内容就是:请求的 hooks 处理、路由处理以及错误处理,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# Bottle
def _handle(self, environ):
# environ :WSGI SERVER 的环境信息
path = environ['bottle.raw_path'] = environ['PATH_INFO']
environ['bottle.app'] = self
request.bind(environ)
response.bind()
try:
self.trigger_hook('`before_request')
route, args = self.router.match(environ)
environ['route.handle'] = route
environ['bottle.route'] = route
environ['route.url_args'] = args
return route.call(**args)
finally:
self.trigger_hook('after_request')
|
暂停下,先去看看路由。
路由
一个 web 应用不同的路径会有不同的处理函数,路由就是根据请求的 URL 找到对应处理函数的过程。
构建路由规则
在执行查找之前,需要有一个规则列表,它存储了 url 和处理函数的对应关系。
Bottle
中如何构建路由规则,有两种写法:
1
2
3
4
5
6
7
8
9
|
# 1.
@route('/hello')
def hello():
return "hello"
# 2.
def world():
return "world"
route('/world', callback=world)
|
查看 Bottle.route
方法的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# Bottle
def route(self, path=None, method='GET', callback=None, name=None,
apply=None, skip=None, **config):
""" A decorator to bind a function to a request URL.
"""
if callable(path): path, callback = None, path
plugins = makelist(apply)
skiplist = makelist(skip)
def decorator(callback):
# TODO: Documentation and tests
if isinstance(callback, basestring): callback = load(callback)
for rule in makelist(path) or yieldroutes(callback):
for verb in makelist(method):
verb = verb.upper()
# 创建路由规则
route = Route(self, rule, verb, callback, name=name,
plugins=plugins, skiplist=skiplist, **config)
# 添加路由规则
self.add_route(route)
return callback
return decorator(callback) if callback else decorator
|
添加路由 Bottle.add_route
:
1
2
3
4
5
|
# Bottle
def add_route(self, route):
''' Add a route object, but do not change the :data:`Route.app`
attribute.'''
self.router.add(route.rule, route.method, route, name=route.name)
|
Match 实现
Router.match
方法返回: Route
实例和 url 相关参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# Router
def match(self, environ):
''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). '''
verb = environ['REQUEST_METHOD'].upper()
path = environ['PATH_INFO'] or '/'
target = None
methods = ['PROXY', verb, 'ANY']
for method in methods:
for combined, rules in self.dyna_regexes[method]:
match = combined(path)
if match:
target, getargs = rules[match.lastindex - 1]
return target, getargs(path) if getargs else {}
# No matching route and no alternative method found. We give up
raise HTTPError(404, "Not found: " + repr(path))
|
Bottle._handle
中匹配到路由后就返回 Route
的实例执行 call
方法。
1
2
3
4
5
6
|
# Route
@cached_property
def call(self):
''' The route callback with all plugins applied. This property is
created on demand and then cached to speed up subsequent requests.'''
return self._make_callback()
|
这里想提一下 cached_property
属性缓存,这里装饰路由规则 call
方法的作用就显而易见了。
下次调用 call 方法就不需要再去执行 Route._make_callback
方法了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class cached_property(object):
''' A property that is only computed once per instance and then replaces
itself with an ordinary attribute. Deleting the attribute resets the
property. '''
def __init__(self, func):
self.__doc__ = getattr(func, '__doc__')
self.func = func
def __get__(self, obj, cls):
if obj is None:
return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value
|
路由就简单看到这里,其中还有起者重要作用的 request
和 response
上下文。
Request and Response
Bottle
中把这些作为类似全局变量的东西,它们必须是动态的,因为在多线程或者多协程的情况下,每个线程或者协程获取的都是自己独特的对象,不会互相干扰。
1
2
3
4
5
6
7
8
|
#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a
#: request callback, this instance always refers to the *current* request
#: (even on a multithreaded server).
request = LocalRequest()
#: A thread-safe instance of :class:`LocalResponse`. It is used to change the
#: HTTP response for the *current* request.
response = LocalResponse()
|
探究下如何做到多线程时不会互相干扰
Bottle._handle
方法中的:
1
2
|
request.bind(environ)
response.bind()
|
查看 LocalRequest.bind
方法就是创建一个新的 BaseRequest 实例:
1
2
3
|
class LocalRequest(BaseRequest):
bind = BaseRequest.__init__
environ = local_property()
|
BaseRequest.__init__
方法对 environ
属性进行初始化:
1
2
3
4
5
6
7
8
9
10
|
class BaseRequest(object):
__slots__ = ('environ') # 限制实例的属性只有 `environ`
def __init__(self, environ=None):
""" Wrap a WSGI environ dictionary. """
#: The wrapped WSGI environ dictionary. This is the only real attribute.
#: All other attributes actually are read-only properties.
self.environ = {} if environ is None else environ
self.environ['bottle.request'] = self
...
|
local_property
函数装饰了 environ
属性,使用 threading.local
本地存储 environ
的值。
1
2
3
4
5
6
7
8
9
10
|
def local_property(name=None):
if name: depr('local_property() is deprecated and will be removed.') #0.12
ls = threading.local()
def fget(self):
try: return ls.var
except AttributeError:
raise RuntimeError("Request context not initialized.")
def fset(self, value): ls.var = value
def fdel(self): del ls.var
return property(fget, fset, fdel, 'Thread-local property')
|
可能看着有点复杂,简单的例子:
1
2
3
4
5
6
7
8
9
|
class A:
local = local_property() # 初始化 local 属性
def init(self):
self.local = {"hello": "c"} # fset
b = A()
b.init()
print(b.local) # fget
|
到这里一个简单 web 框架的启动到请求的路由和响应的基本流程就理清了。
其实还有 plugins, hook 等功能没有看。。😭
Bottle.route
一个小的技巧让 route
有两种使用方式
cached_property
属性缓存
LocalRequest
如何创建一个多线程下会不干扰的全局变量
相关链接