美文网首页
Flask 请求处理流程(一):WSGI 和 路由

Flask 请求处理流程(一):WSGI 和 路由

作者: Uchen | 来源:发表于2018-12-22 15:01 被阅读10次

flask 一个基于 python 实现的Web开发微框架,主要依赖:

  • Werkzeug:一个 python 的 WSGI 工具包,也可以作为一个 Web 框架的底层库。
  • Jinja2:为 python 提供的一个功能齐全的模板引擎

flask 只建立 WerkezugJinja2 的桥梁,前者实现一个合适的 WSGI 应用,后者处理模板。 Flask 也绑定了一些通用的标准库包,比如 logging 。其它所有一切取决于扩展。

一、WSGI


WSGI(Web Server Gateway Interface)的本质是一种约定,是 Python web 开发中 web 服务器与 web 应用程序之间数据交互的约定。它封装了接受 HTTP 请求解析 HTTP 请求发送 HTTP,响应等等的这些底层的代码和操作,使开发者可以高效的编写Web应用。

网关协议的本质是为了解耦,实现 web 服务器(提供 web 服务)和 web 应用程序(资源处理)的分离,WSGI 就是一个支持 WSGI 的 web 服务器与 Python web 应用程序之间的约定。

WSGI

WSGI 服务器

一个 WSGI 服务器需要实现两个函数

1、解析 http 请求,为应用程序提供 environ 字典
def get_environ(self):
    env = {}
    env['wsgi.version']      = (1, 0)
    env['wsgi.url_scheme']   = 'http'
    env['wsgi.input']        = StringIO.StringIO(self.request_data)
    env['wsgi.errors']       = sys.stderr
    env['wsgi.multithread']  = False
    env['wsgi.multiprocess'] = False
    env['wsgi.run_once']     = False
    env['REQUEST_METHOD']    = self.request_method    # GET
    env['PATH_INFO']         = self.path              # /hello
    env['SERVER_NAME']       = self.server_name       # localhost
    env['SERVER_PORT']       = str(self.server_port)  # 8888
    return env
2、实现 start_response 函数
def start_response(self, status, response_headers, exc_info=None):
    # Add necessary server headers
    server_headers = [
        ('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'),
        ('Server', 'WSGIServer 0.2'),
    ]
    self.headers_set = [status, response_headers + server_headers]
    # To adhere to WSGI specification the start_response must return
    # a 'write' callable. We simplicity's sake we'll ignore that detail
    # for now.
WSGI 服务器

服务器与应用程序交互过程

主要过程是:

服务器从客户端获取到请求,然后通过 get_env 获得 env 变量。再调用应用程序,传入 env 变量(字典) 和 start_response 函数, 并获得响应。最后将响应返回给客户端。

服务器与应用程序交互过程

代码如下:

def handle_one_request(self):
    self.request_data = request_data = self.client_connection.recv(1024)
    print(''.join(
        '< {line}\n'.format(line=line)
        for line in request_data.splitlines()
    ))
    self.parse_request(request_data)
    env = self.get_environ()   #获取 environ
    result = self.application(env, self.start_response)  #调用应用程序
    self.finish_response(result)

在上述这个过程中,Python 应用程序主要工作就是根据输入的 environ 字典信息生成相应的 http 报文返回给服务器

下面是一个简单的例子:

from wsgiref.simple_server import make_server

def simple_app(environ, start_response):
  status = '200 OK'
  response_headers = [('Content-type', 'text/plain')]
  start_response(status, response_headers)
  return [u"This is hello wsgi app".encode('utf8')]

httpd = make_server('', 8000, simple_app)
print "Serving on port 8000..."
httpd.serve_forever()

其中:

  • environ: 一个包含全部 HTTP 请求信息的字典,由 WSGI Server 解包 HTTP 请求生成。
  • start_response: 一个 WSGI Server 提供的函数,调用可以发送响应的状态码和 HTTP 报文头, 函数在返回前必须调用一次 start_response()
  • simple_app() 应当返回一个可以迭代的对象(HTTP正文)。
  • simple_app() 函数由 WSGI Server 直接调用和提供参数。
  • Python 内置了一个 WSGIREFWSGI Server,不过性能不是很好,一般只用在开发环境。可以选择其他的如 Gunicorn

总结

WSGI Server 和 App交互图

flask 中实现 WSGI 接口


1.通过 __call__ 方法将 Flask 对象变为可调用

def __call__(self, environ, start_response):
    """Shortcut for :attr:`wsgi_app`."""
    return self.wsgi_app(environ, start_response)

2. 实现 wsgi_app 函数处理 web 服务器转发的请求

def wsgi_app(self, environ, start_response):
    """
    The actual WSGI application.
    :param environ: a WSGI environment
    :param start_response: a callable accepting a status code,
                       a list of headers and 
                       an optional exception context to start the responseresponse
    """
    ctx = self.request_context(environ)
    error = None
    try:
        try:
            ctx.push()
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        except:
            error = sys.exc_info()[1]
            raise
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        ctx.auto_pop(error)

二、路由


Flask 类中支持路由功能的数据结构,在 __init__ 函数中初始化:

url_rule_class = Rule
self.url_map = Map()
self.view_functions = {}

Rule 和 Map 是 werkzeug 中实现的 路由类映射类

>>> m = Map([
  ...     # Static URLs
  ...     Rule('/', endpoint='static/index'),
  ...     Rule('/about', endpoint='static/about'),
  ...     Rule('/help', endpoint='static/help'),
  ...     # Knowledge Base
  ...     Subdomain('something', [
  ...         Rule('/', endpoint='something/index'),
  ...         Rule('/browse/', endpoint='something/browse'),
  ...         Rule('/browse/<int:id>/', endpoint='something/browse'),
  ...         Rule('/browse/<int:id>/<int:page>', endpoint='something/browse')
  ...     ])
  ... ], default_subdomain='www')

通过 Map 我们就可以实现动态路由的功能。这里我们注意到 Map 类先建立了 url 到 endpoint 的映射,而不是直接映射到函数,这是为什么呢?

主要是两个原因:

  • 一是为了实现动态路由功能,
  • 二是为不同的 url 映射到同一个视图函数提供了便利。

view_functions 是一个字典,它负责建立 endpoint 和视图函数之间的映射关系。

下面是一个小实验,证明我们所说的映射关系:

>>> from flask import Flask
>>> app = Flask(__name__)
>>> @app.route('/')
... def index():
...     return "hello world"
... 
>>> app.url_map
Map([<Rule '/' (HEAD, GET, OPTIONS) -> index>,
<Rule '/static/<filename>' (HEAD, GET, OPTIONS) -> static>])

>>> app.view_functions
{'index': <function index at 0x7f6ced14c840>, 'static': <bound method _PackageBoundObject.send_static_file of <Flask '__main__'>>}

这里我们可以看到

<Rule '/' (HEAD, GET, OPTIONS) -> index>,'index': <function index at 0x7f6ced14c840>

通过 endpoint 这个中间量,我们让把 路由函数 建立了映射关系。

要注意一下,为什么会有 '/static/<filename>' 这个路由呢,这是因为在初始化时 flask 调用了 add_url_rule 函数做了如下绑定:

if self.has_static_folder:
    assert bool(static_host) == host_matching, 'Invalid static_host/host_matching combination'
    self.add_url_rule(
        self.static_url_path + '/<path:filename>',
        endpoint='static',
        host=static_host,
        view_func=self.send_static_file
    )

总结

Flask 理由

注册路由

在 flask 中注册路由有两种方式,

  • 一种是用 route 装饰器,如上所示,@app.route()
  • 另一种是直接调用 add_url_rule 函数绑定视图类,

但是本质上二者都是调用 add_url_rule 函数,下面我们来看一下 add_url_rule 函数的实现。

在 Flask 的 add_url_rule 函数很长,但是核心的代码为以下几行:

self.url_map.add(rule)
rule = self.url_rule_class(rule, methods=methods, **options)
self.view_functions[endpoint] = view_func
1. 装饰器
def route(self, rule, **options):
    def decorator(f):
        endpoint = options.pop('endpoint', None)
        self.add_url_rule(rule, endpoint, f, **options)
        return f
    return decorator
2. 视图类
class CounterAPI(MethodView):
    def get(self):
        return session.get('counter', 0)
    def post(self):
        session['counter'] = session.get('counter', 0) + 1
        return 'OK'
    app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter'))

注册路由之后,flask 就需要分发路由,调用相应的视图函数。

def dispatch_request(self):
    req = _request_ctx_stack.top.request
    if req.routing_exception is not None:
        self.raise_routing_exception(req)
    rule = req.url_rule
    if getattr(rule, 'provide_automatic_options', False) \
       and req.method == 'OPTIONS':
        return self.make_default_options_response()
    return self.view_functions[rule.endpoint](**req.view_args)

相关文章

网友评论

      本文标题:Flask 请求处理流程(一):WSGI 和 路由

      本文链接:https://www.haomeiwen.com/subject/euvikqtx.html