Please enable Javascript to view the contents

阅读Bottle Web框架小记

 ·  ☕  6 分钟

起因 🤔

因为好奇一个 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

  • environstart_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 做了两件事情:

  1. 被服务器程序(有可能是其他 middleware)调用,返回结果回去
  2. 调用应用程序(有可能是其他 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

路由就简单看到这里,其中还有起者重要作用的 requestresponse上下文。

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 如何创建一个多线程下会不干扰的全局变量

相关链接

目录