flask_python_web框架源码阅读(1)

引言

还是老样子,想研究源码,从最新版看没有太大的意义,最好是从第一个版本v0.1开始看起,因为东西最少,但是涉及到的整个框架的设计思路是基本上不会变的···

文章目录

  • 一些概念
    • flask
    • werkzeug
    • wsgi
    • web server /web application
  • 简单了解werkzeug
  • 开始了解flask
    • 快速开发一个hello,world应用
    • 源码分析
      • 实例化过程
      • run源码阅读
      • run_simple源码阅读
      • serve_forever阅读源码
  • 本回合总结

一些概念

flask

这是对flask的最初始的描述

A microframework based on Werkzeug.  It's extensively documented
and follows best practice patterns.
>>> 翻译过来就是
一个基于werkzeug的微型框架,有大量的文档以及遵循最佳设计实践

werkzeug

前面说flask是基于werkzurg进行的实现,那么这个werkzeug是个什么鬼呢?

解释: Werkzeug是一个WSGI工具包,他可以作为一个Web框架的底层库

wsgi

那么什么又是WSGI呢?

解释:Web Server Gateway Interface,它由Python标准定义的一套Web Server与Web Application的接口交互规范,WSGI不是一个应用、框架、模块或者库,而是规范

web server /web application

说到这里又要解释一下什么是web serverweb application
举例来说,例如常见的web application有Django、Flask等,而web server有uWSGI、Gunicorn等。WSGI就是定义了这两端接口交互的规范

简单了解werkzeug

一个原生的基于WSGI 的 “Hello World” 应用看起来是这样的:

# 底层提供两个变量(请求数据需要的环境对象environ,一个可调用的start_response),名字可以改,但是一般不建议
def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return ['Hello World!']

使用werkzeug模块的响应对象,修改如下

from werkzeug.wrappers import Response

 def application(environ, start_response):
    response = Response('Hello World!', mimetype='text/plain')
    return response(environ, start_response)

这里有一个在 URL 中查询字符串的扩展版本(重点是 URL 中的 name 将会替代 World)

from werkzeug.wrappers import Request, Response

def application(environ, start_response):
    request = Request(environ)
    text = 'Hello %s!' % request.args.get('name', 'World')
    response = Response(text, mimetype='text/plain')
    return response(environ, start_response)

这里我们基于Werkzeug启动一个简单的服务器应用,是不是和flask的服务器创建过程比较像了

from werkzeug.wrappers import Request, Response

@Request.application
def application(request):
    return Response('Hello, World!')

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    run_simple('localhost', 8080, application)

开始了解flask

官方的讲,flask是一个轻量级框架, 理解上来说,flask属于跑在web服务器中的一个应用,此应用可以让其和服务器进行交互。

快速开发一个hello,world应用

使用Flask开发一个只返回’hello,world’的简单应用

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World!'

if __name__ == '__main__':
    app.run()

运行脚本,打开网页, 访问"http://127.0.0.1:5000/ ",即可看到"hello,world"出现,是不是很简单

源码分析

实例化过程

那么这个应用是怎么完成简单几行代码启动了一个服务器的?

首先,实例化对象,Flask类只有一个必须指定的参数,即程序主模块或者包的名字,__name__是系统变量,该变量指的是本py文件的文件名

from flask import Flask

app = Flask(__name__)

然后,我们浏览下Flask的 __init__逻辑,有些参数我用中文重新注释了

class Flask(object):
    """The flask object implements a WSGI application and acts as the central
    object.  It is passed the name of the module or package of the
    application.  Once it is created it will act as a central registry for
    the view functions, the URL rules, template configuration and much more.

    The name of the package is used to resolve resources from inside the
    package or the folder the module is contained in depending on if the
    package parameter resolves to an actual python package (a folder with
    an `__init__.py` file inside) or a standard module (just a `.py` file).

    For more information about resource loading, see :func:`open_resource`.

    Usually you create a :class:`Flask` instance in your main module or
    in the `__init__.py` file of your package like this::

        from flask import Flask
        app = Flask(__name__)
    """

    #: the class that is used for request objects.  See :class:`~flask.request`
    #: for more information.
    request_class = Request

    #: the class that is used for response objects.  See
    #: :class:`~flask.Response` for more information.
    response_class = Response

    #: path for the static files.  If you don't want to use static files
    #: you can set this value to `None` in which case no URL rule is added
    #: and the development server will no longer serve any static files.
    static_path = '/static'

    #: if a secret key is set, cryptographic components can use this to
    #: sign cookies and other things.  Set this to a complex random value
    #: when you want to use the secure cookie for instance.
    secret_key = None

    #: The secure cookie uses this for the name of the session cookie
    session_cookie_name = 'session'

    #: options that are passed directly to the Jinja2 environment
    jinja_options = dict(
        autoescape=True,
        extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_']
    )

    def __init__(self, package_name):
      
        # 是否开启debug模式
        self.debug = False

        # 包名或者模块名,唯一需要的必传参数
        self.package_name = package_name

        #: 获取app所在目录
        self.root_path = _get_package_path(self.package_name)
        
        # 存储视图函数的字典,键为函数别名,值为函数对象
        self.view_functions = {}

        #: a dictionary of all registered error handlers.  The key is
        #: be the error code as integer, the value the function that
        #: should handle that error.
        #: To register a error handler, use the :meth:`errorhandler`
        #: decorator.
        self.error_handlers = {}

 				# 处理请求前执行的函数列表,使用@before_request装饰器进行注册
        self.before_request_funcs = []

 				# 处理请求前执行的函数列表,使用@after_request装饰器进行注册
        self.after_request_funcs = []

        #: a list of functions that are called without arguments
        #: to populate the template context.  Each returns a dictionary
        #: that the template context is updated with.
        #: To register a function here, use the :meth:`context_processor`
        #: decorator.
        # 上下文相关
        self.template_context_processors = [_default_template_ctx_processor]
				
        # 存储路由和视图函数的映射关系: Map([' (OPTIONS, HEAD, GET) -> 						static>]) 这种格式
        self.url_map = Map()
				
        # 将静态文件路径添加到self.url_map中
        if self.static_path is not None:
            self.url_map.add(Rule(self.static_path + '/',
                                  build_only=True, endpoint='static'))
            if pkg_resources is not None:
                target = (self.package_name, 'static')
            else:
                target = os.path.join(self.root_path, 'static')
            self.wsgi_app = SharedDataMiddleware(self.wsgi_app, {
                self.static_path: target
            })

        #: the Jinja2 environment.  It is created from the
        #: :attr:`jinja_options` and the loader that is returned
        #: by the :meth:`create_jinja_loader` function.
        # 初始化 Jinja2 模版环境
        self.jinja_env = Environment(loader=self.create_jinja_loader(),
                                     **self.jinja_options)
        self.jinja_env.globals.update(
            url_for=url_for,
            get_flashed_messages=get_flashed_messages
        )

总结来看,初始化通过传入的包名,解析包内部资源,并注册路由到映射表中,初始化jinja2环境信息,初始化一些可能会用到的额外参数,比如请求钱需要使用的函数列表等等

run源码阅读

这里先不说路由是怎么回事,先来看看这段代码做了什么事情

if __name__ == '__main__':
    app.run()

这段代码就是第一个版本的run函数,行数非常之少,下面的英文解释一下:

开启本地开发服务器,如果设置debug参数为True,则服务器会在检测到代码变动时自动重启服务器,如果检测到有异常发生,那么将会抛出异常

host,port就是服务器地址和端口号,options是给werkzeug的run_simple使用的,调用底层库实现交互

def run(self, host='localhost', port=5000, **options):
    """Runs the application on a local development server.  If the
    :attr:`debug` flag is set the server will automatically reload
    for code changes and show a debugger in case an exception happened.

    :param host: the hostname to listen on.  set this to ``'0.0.0.0'``
                 to have the server available externally as well.
    :param port: the port of the webserver
    :param options: the options to be forwarded to the underlying
                    Werkzeug server.  See :func:`werkzeug.run_simple`
                    for more information.
    """
    from werkzeug import run_simple
    if 'debug' in options:
        self.debug = options.pop('debug')
    options.setdefault('use_reloader', self.debug)
    options.setdefault('use_debugger', self.debug)
    return run_simple(host, port, self, **options)

run_simple源码阅读

这个flask的run封装了run_simple函数,所以我们要研究下run_simple函数的实现,参数大多数都是给内部核心函数inner.make_server()准备的

def run_simple(
    hostname,
    port,
    application,
    use_reloader=False,
    use_debugger=False,
    use_evalex=True,
    extra_files=None,
    reloader_interval=1,
    reloader_type="auto",
    threaded=False,
    processes=1,
    request_handler=None,
    static_files=None,
    passthrough_errors=False,
    ssl_context=None,
):
    """
    :param hostname: The host to bind to, for example ``'localhost'``.
        If the value is a path that starts with ``unix://`` it will bind
        to a Unix socket instead of a TCP socket..
    :param port: The port for the server.  eg: ``8080``
    :param application: the WSGI application to execute
    :param use_reloader: should the server automatically restart the python
                         process if modules were changed?
    :param use_debugger: should the werkzeug debugging system be used?
    :param use_evalex: should the exception evaluation feature be enabled?
    :param extra_files: a list of files the reloader should watch
                        additionally to the modules.  For example configuration
                        files.
    :param reloader_interval: the interval for the reloader in seconds.
    :param reloader_type: the type of reloader to use.  The default is
                          auto detection.  Valid values are ``'stat'`` and
                          ``'watchdog'``. See :ref:`reloader` for more
                          information.
    :param threaded: should the process handle each request in a separate
                     thread?
    :param processes: if greater than 1 then handle each request in a new process
                      up to this maximum number of concurrent processes.
    :param request_handler: optional parameter that can be used to replace
                            the default one.  You can use this to replace it
                            with a different
                            :class:`~BaseHTTPServer.BaseHTTPRequestHandler`
                            subclass.
    :param static_files: a list or dict of paths for static files.  This works
                         exactly like :class:`SharedDataMiddleware`, it's actually
                         just wrapping the application in that middleware before
                         serving.
    :param passthrough_errors: set this to `True` to disable the error catching.
                               This means that the server will die on errors but
                               it can be useful to hook debuggers in (pdb etc.)
    :param ssl_context: an SSL context for the connection. Either an
                        :class:`ssl.SSLContext`, a tuple in the form
                        ``(cert_file, pkey_file)``, the string ``'adhoc'`` if
                        the server should automatically create one, or ``None``
                        to disable SSL (which is the default).
    """
    if not isinstance(port, int):
        raise TypeError("port must be an integer")
    if use_debugger:
        from .debug import DebuggedApplication

        application = DebuggedApplication(application, use_evalex)
    if static_files:
        from .middleware.shared_data import SharedDataMiddleware

        application = SharedDataMiddleware(application, static_files)

    def log_startup(sock):
        display_hostname = hostname if hostname not in ("", "*") else "localhost"
        quit_msg = "(Press CTRL+C to quit)"
        if sock.family == af_unix:
            _log("info", " * Running on %s %s", display_hostname, quit_msg)
        else:
            if ":" in display_hostname:
                display_hostname = "[%s]" % display_hostname
            port = sock.getsockname()[1]
            _log(
                "info",
                " * Running on %s://%s:%d/ %s",
                "http" if ssl_context is None else "https",
                display_hostname,
                port,
                quit_msg,
            )

    def inner():
        try:
            fd = int(os.environ["WERKZEUG_SERVER_FD"])
        except (LookupError, ValueError):
            fd = None
        srv = make_server(
            hostname,
            port,
            application,
            threaded,
            processes,
            request_handler,
            passthrough_errors,
            ssl_context,
            fd=fd,
        )
        if fd is None:
            log_startup(srv.socket)
        srv.serve_forever()

    if use_reloader:
        # If we're not running already in the subprocess that is the
        # reloader we want to open up a socket early to make sure the
        # port is actually available.
        if not is_running_from_reloader():
            if port == 0 and not can_open_by_fd:
                raise ValueError(
                    "Cannot bind to a random port with enabled "
                    "reloader if the Python interpreter does "
                    "not support socket opening by fd."
                )

            # Create and destroy a socket so that any exceptions are
            # raised before we spawn a separate Python interpreter and
            # lose this ability.
            address_family = select_address_family(hostname, port)
            server_address = get_sockaddr(hostname, port, address_family)
            s = socket.socket(address_family, socket.SOCK_STREAM)
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            s.bind(server_address)
            if hasattr(s, "set_inheritable"):
                s.set_inheritable(True)

            # If we can open the socket by file descriptor, then we can just
            # reuse this one and our socket will survive the restarts.
            if can_open_by_fd:
                os.environ["WERKZEUG_SERVER_FD"] = str(s.fileno())
                s.listen(LISTEN_QUEUE)
                log_startup(s)
            else:
                s.close()
                if address_family == af_unix:
                    _log("info", "Unlinking %s" % server_address)
                    os.unlink(server_address)

        # Do not use relative imports, otherwise "python -m werkzeug.serving"
        # breaks.
        from ._reloader import run_with_reloader

        run_with_reloader(inner, extra_files, reloader_interval, reloader_type)
    else:
        inner()

make_server源码阅读

在这个函数中,make_server()会根据线程或者进程的数量创建对应的WSGI服务器。Flask在默认情况下是创建BaseWSGIServer服务器

def make_server(
    host=None,
    port=None,
    app=None,
    threaded=False,
    processes=1,
    request_handler=None,
    passthrough_errors=False,
    ssl_context=None,
    fd=None,
):
    """Create a new server instance that is either threaded, or forks
    or just processes one request after another.
    """
    if threaded and processes > 1:
        raise ValueError("cannot have a multithreaded and multi process server.")
    elif threaded:
        return ThreadedWSGIServer(
            host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd
        )
    elif processes > 1:
        return ForkingWSGIServer(
            host,
            port,
            app,
            processes,
            request_handler,
            passthrough_errors,
            ssl_context,
            fd=fd,
        )
    else:
        return BaseWSGIServer(
            host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd
        )

从源码可以看得出来,一共有几种WSGI服务器,基类是BaseWSGIServer,其余服务器类继承自基类,(而基类则是封装了HTTPServer类,)并且同时继承不同的混入类,增加其需要的方法

class BaseWSGIServer(HTTPServer, object):

    """Simple single-threaded, single-process WSGI server."""

    multithread = False
    multiprocess = False
    request_queue_size = LISTEN_QUEUE

    def __init__(
        self,
        host,
        port,
        app,
        handler=None,
        passthrough_errors=False,
        ssl_context=None,
        fd=None,
    ):
        if handler is None:
            handler = WSGIRequestHandler

        self.address_family = select_address_family(host, port)

        if fd is not None:
            real_sock = socket.fromfd(fd, self.address_family, socket.SOCK_STREAM)
            port = 0

        server_address = get_sockaddr(host, int(port), self.address_family)

        # remove socket file if it already exists
        if self.address_family == af_unix and os.path.exists(server_address):
            os.unlink(server_address)
        HTTPServer.__init__(self, server_address, handler)

        self.app = app
        self.passthrough_errors = passthrough_errors
        self.shutdown_signal = False
        self.host = host
        self.port = self.socket.getsockname()[1]

        # Patch in the original socket.
        if fd is not None:
            self.socket.close()
            self.socket = real_sock
            self.server_address = self.socket.getsockname()

        if ssl_context is not None:
            if isinstance(ssl_context, tuple):
                ssl_context = load_ssl_context(*ssl_context)
            if ssl_context == "adhoc":
                ssl_context = generate_adhoc_ssl_context()

            # If we are on Python 2 the return value from socket.fromfd
            # is an internal socket object but what we need for ssl wrap
            # is the wrapper around it :(
            sock = self.socket
            if PY2 and not isinstance(sock, socket.socket):
                sock = socket.socket(sock.family, sock.type, sock.proto, sock)
            self.socket = ssl_context.wrap_socket(sock, server_side=True)
            self.ssl_context = ssl_context
        else:
            self.ssl_context = None
    def ···

class ThreadedWSGIServer(ThreadingMixIn, BaseWSGIServer):

    """A WSGI server that does threading."""

    multithread = True
    daemon_threads = True

class ForkingWSGIServer(ForkingMixIn, BaseWSGIServer):

    """A WSGI server that does forking."""

    multiprocess = True

    def __init__(
        self,
        host,
        port,
        app,
        processes=40,
        handler=None,
        passthrough_errors=False,
        ssl_context=None,
        fd=None,
    ):
        if not can_fork:
            raise ValueError("Your platform does not support forking.")
        BaseWSGIServer.__init__(
            self, host, port, app, handler, passthrough_errors, ssl_context, fd
        )
        self.max_children = processes

serve_forever阅读源码

inner中根据不同情况下返回的服务器类,进行服务器的启动函数,可以看到最终是使用HTTPServer中的启动服务的方法。而HTTPServerPython标准类库中的接口

def serve_forever(self):
    self.shutdown_signal = False
    try:
        HTTPServer.serve_forever(self)
    except KeyboardInterrupt:
        pass
    finally:
        self.server_close()

本回合总结

初步了解了Flask框架所涉及到的术语及基本的网路协议

通过阅读源码一步步的了解了Flask的服务器启动过程

你可能感兴趣的:(web开发)