引言
还是老样子,想研究源码,从最新版看没有太大的意义,最好是从第一个版本v0.1开始看起,因为东西最少,但是涉及到的整个框架的设计思路是基本上不会变的···
这是对flask的最初始的描述
A microframework based on Werkzeug. It's extensively documented
and follows best practice patterns.
>>> 翻译过来就是
一个基于werkzeug的微型框架,有大量的文档以及遵循最佳设计实践
前面说flask是基于werkzurg进行的实现,那么这个werkzeug是个什么鬼呢?
解释: Werkzeug是一个WSGI工具包,他可以作为一个Web框架的底层库
那么什么又是WSGI呢?
解释:Web Server Gateway Interface,它由Python标准定义的一套Web Server与Web Application的接口交互规范,WSGI不是一个应用、框架、模块或者库,而是规范
说到这里又要解释一下什么是web server 和 web application了
举例来说,例如常见的web application有Django、Flask等,而web server有uWSGI、Gunicorn等。WSGI就是定义了这两端接口交互的规范
一个原生的基于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属于跑在web服务器中的一个应用,此应用可以让其和服务器进行交互。
使用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环境信息,初始化一些可能会用到的额外参数,比如请求钱需要使用的函数列表等等
这里先不说路由是怎么回事,先来看看这段代码做了什么事情
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)
这个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
inner
中根据不同情况下返回的服务器类,进行服务器的启动函数,可以看到最终是使用HTTPServer
中的启动服务的方法。而HTTPServer
是Python
标准类库中的接口
def serve_forever(self):
self.shutdown_signal = False
try:
HTTPServer.serve_forever(self)
except KeyboardInterrupt:
pass
finally:
self.server_close()
初步了解了Flask框架所涉及到的术语及基本的网路协议
通过阅读源码一步步的了解了Flask的服务器启动过程