Python WSGI 协议
0.1 WSGI概念
出自python的增强性建议书:PEP-3333,由PEP-333发展而来(为了支持python3)全称Web Server Gateway Interface
在python中有各种web应用框架,不同的应用框架会限制使用他们的web服务器,相比于JAVA,它虽然也有众多的web开发框架,但自从servlet API出现之后,JAVA web框架都可以在支持servlet API的web服务器上运行。WSGI协议也就是充当了srvlet API这样的一个角色,它定义了应用或框架和web服务器之间通信的接口,使得python的web框架可以在任何支持WSGI协议的web服务器上运行。
那到底开发遵循WSGI协议去开发框架、应用、web服务器等带给我们什么好处呢
至少有了一个明确的领域划分,我们不需要在开发一个web应用或框架的同时还要去想着去实现一遍web服务器的功能,专注各自的领域,减少重复造轮子。
在就是移植性强,项目灵活,我们不要再去考虑说只能在项目里使用一种web框架,当我们的web服务器遵守了WSGI协议,应用层的框架选择不在是问题。
0.2 WSGI协议内容
WSGI协议把整个web服务端分为三个部分,Server、Application、Middleware。
0.2.1. Server
Server端每次从http客户端收到一个请求,就调用一次应用对象。需要实现的是一个将请求中包含的参数、请求头、元数据写入到一个字典中,和一个返回数据给客户端的函数,一并传入到Application中。
下面用python实现了一个cgi进程,通过环境变量获取一个请求的参数,并用cgi进程处理请求输出到标准输出。
import os, sys
enc, esc = sys.getfilesystemencoding(), 'surrogateescape'
def unicode_to_wsgi(u):
# Convert an environment variable to a WSGI "bytes-as-unicode" string
return u.encode(enc, esc).decode('iso-8859-1')
def wsgi_to_bytes(s):
return s.encode('iso-8859-1')
def run_with_cgi(application):
environ = {k: unicode_to_wsgi(v) for k,v in os.environ.items()}
environ['wsgi.input'] = sys.stdin.buffer
environ['wsgi.errors'] = sys.stderr
environ['wsgi.version'] = (1, 0)
environ['wsgi.multithread'] = False
environ['wsgi.multiprocess'] = True
environ['wsgi.run_once'] = True
if environ.get('HTTPS', 'off') in ('on', '1'):
environ['wsgi.url_scheme'] = 'https'
else:
environ['wsgi.url_scheme'] = 'http'
headers_set = []
headers_sent = []
def write(data):
out = sys.stdout.buffer
if not headers_set:
raise AssertionError("write() before start_response()")
elif not headers_sent:
# Before the first output, send the stored headers
status, response_headers = headers_sent[:] = headers_set
out.write(wsgi_to_bytes('Status: %s\r\n' % status))
for header in response_headers:
out.write(wsgi_to_bytes('%s: %s\r\n' % header))
out.write(wsgi_to_bytes('\r\n'))
out.write(data)
out.flush()
def start_response(status, response_headers, exc_info=None):
if exc_info:
try:
if headers_sent:
# Re-raise original exception if headers sent
raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None # avoid dangling circular ref
elif headers_set:
raise AssertionError("Headers already set!")
headers_set[:] = [status, response_headers]
return write
result = application(environ, start_response)
try:
for data in result:
if data: # don't send headers until body appears
write(data)
if not headers_sent:
write('') # send headers now if body was empty
finally:
if hasattr(result, 'close'):
result.close()
0.2.2. Application
应用就是一个简单的接受两个参数的可调用对象,可以是函数,方法,类,实现了call的实例,该应用对象必须可以被多次调用,web服务器会重复的调用它。
基本结构:
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return ['Hello World!']
app = application()
先用回调函数start_response返回状态码
和响应头
,最后返回响应正文,响应正文是可迭代对象。
下面是Application两种实现:函数和类
def simple_app(environ, start_response):
"""最简单的应用对象"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return b"Hello world!\n"
class AppClass:
"""
生成一个AppClass的实例对象,那是一个生成器对象,当我们遍历这个对象,就会执行
__iter__方法,来达到重复执行的效果。当然我们想通过执行这个实例对象来执行,可以去 实现__call__方法。
"""
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
def __iter__(self):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield b"Hello world!\n"
0.2.3. Middleware
中间件是是一个可以与两端交互的组件,也可看做是一个Application,它接受一个Application作为参数,并返回一个Application,这正是利用了Application的可嵌套性,用法类似app = mw1(mw2(app))
,常见用法
-
重写environ,然后基于 URL,将请求对象路由给不同的应用对象
-
支持多个应用或者框架顺序地运行于同一个进程中
-
通过转发请求和响应,支持负载均衡和远程处理
-
支持对内容做后处理(postprocessing)
0.3. WSGI协议的一些延伸
0.3.1 关于uWSGI和uwsgi
uWSGI是一个web服务器,它实现了WSGI接口。
而uwsgi是一种二进制协议,用于两个web服务器用来通信,常见是使用nginx和uWSGI一起部署,nginx是uWSGI之间通讯使用uwsgi协议,而nginx就负责把http协议包转换成uwsgi协议包。
当然uWSGI是可以不依赖于nginx的,但客户端到服务端通常用的都是http协议,那么在uWSGI服务器就有两种选择:自己将http协议解析成uwsgi协议,它起了一个http进程接受客户端请求并解析然后用uwsgi协议传递到每个uWSGI服务器的work;另外一种就是整个过程都用http协议流通。
uwsgi --http :9000 --wsgi-file flask_test.py --processes 1 --threads 1 --callable app
中文翻译文档:
0.3.1 什么是envrion
文档https://www.python.org/dev/peps/pep-3333/#environ-variables
{'wsgi.errors': <gunicorn.http.wsgi.WSGIErrorsWrapper object at 0x7f8416243a58>, 'wsgi.version': (1, 0), 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'wsgi.file_wrapper': <class 'gunicorn.http.wsgi.FileWrapper'>, 'SERVER_SOFTWARE': 'gunicorn/19.9.0', 'wsgi.input': <gunicorn.http.body.Body object at 0x7f84162434a8>, 'gunicorn.socket': <socket.socket fd=17, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8001), raddr=('127.0.0.1', 59571)>, 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'dd=1', 'RAW_URI': '/v1/frontend/competition?dd=1', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_AUTHORIZATION': '123', 'HTTP_USER': '123', 'CONTENT_TYPE': 'application/json', 'HTTP_USER_AGENT': 'PostmanRuntime/7.26.1', 'HTTP_ACCEPT': '*/*', 'HTTP_POSTMAN_TOKEN': '992ee3de-4eba-4866-b32a-9705ddb742fd', 'HTTP_HOST': 'localhost:8001', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_CONNECTION': 'keep-alive', 'CONTENT_LENGTH': '572', 'HTTP_COOKIE': 'session=835faf17-25e5-432c-9c2a-fe6dfa4f0d99', 'wsgi.url_scheme': 'http', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '59571', 'SERVER_NAME': '0.0.0.0', 'SERVER_PORT': '8001', 'PATH_INFO': '/v1/frontend/competition', 'SCRIPT_NAME': ''}
1. 介绍
Werkzeug是一个WSGI的工具库,你可以使用他来构建应用或框架。
2. 实现一个简单的web应用
从WSGI协议来看,一个WSGI应用应该是这样的
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return ['Hello World!']
我们可以使用Werkzeug封装好的类Response,来帮我们简化上述代码
from werkzeug.wrappers import Response
def application(environ, start_response):
response = Response('Hello World!', mimetype='text/plain')
return response(environ, start_response)
2.1 开始我们的应用
我们用类的方式来实现一个application,在python的魔法函数__call__
下执行start_response
,这样我们可以用执行对象的方式app()
来处理请求。
class MyApp(object):
def __init__(self):
print("创建app")
def dispatch_request(self, request):
return Response("hello world")
def wsgi_app(self, environ, start_response):
request = Request(environ)
response = self.dispatch_request(request)
return response(environ, start_response)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
函数wsgi_app
是我们整个应用的核心,这样的写法容易让我们去扩展WSGI中间件,里面创建了一个Request
对象,传递给dispatch_request
并返回一个WSGI应用Response
。
现在我们可以构建一个统一的生成函数来生成一个MyApp
对象,方便管理。
def create_app():
app = MyApp()
return app
最后我们通过WerkZeug来实现一个简单的本地web服务器,传入我们写的应用。
if __name__ == '__main__':
from werkzeug.serving import run_simple
app = create_app()
run_simple('127.0.0.1', 5000, app)
2.1.1 flask应用的是怎么启动的
我们看一下我们DSP项目的app_runner
def run():
app = create_app()
# 启动celery worker
if config.RUN_MODEL in ('ALL', 'WORKER'):
worker_thread = Thread(target=run_worker, args=(app,))
worker_thread.daemon = True
worker_thread.start()
# 启动flask项目
if config.RUN_MODEL in ('ALL', 'CONTROLLER'):
try:
app.run('0.0.0.0', 8001, debug=app.config["DEBUG"], threaded=True)
except Exception as e:
LOG.info("Program exit unexpectly because an error {}".format(e))
我们进入到app.run()
发现里面有这么一句
from werkzeug.serving import run_simple try: run_simple(host, port, self, **options) finally: self._got_first_request = False
所以我们知道,我们的app_runner
其实本质上用的还是werkzeug的run_simple
.
2.1.2 那到底这个run_simple
干了些什么
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, ): """Start a WSGI application. Optional features include a reloader, multithreading and fork support. ……
启动一个WSGI的应用,可选特性包括 自动加载、多线程、多进程。这其中我们注意两个参数,threaded
和processes
。在整个run_simple
里核心是这个函数的内置函数inner
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()
这里的fd
其实就是一个文件描述符,让socket可以从文件描述符来创建对象。
进入make_server
我们查看下代码
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
)
整个过程的逻辑很清晰,根据参数threaded
和processes
来判断最后返回的是那个WSGIServer,其实这个函数的注释也说明了实现的功能:创建一个服务支持对请求的多线程处理、或者多进程处理、或者一个请求接一个请求处理的单线程,但是不支持多进程加多线程的方式。
到此我们也知道,Flask原生支持的并发模式,我们可以再往里挖一点:
2.1.3 WSGIServer又是如何工作的
我们找到BaseWSGIServer
,它继承自HTTPServer
class BaseWSGIServer(HTTPServer, object):
"""Simple single-threaded, single-process WSGI server."""
告诉我们它是个单线程的WSGI server,在回顾run_simple下的inner函数最后执行了srv.serve_forever()
看这个名字貌似就是启动了一个永久的服务。
def serve_forever(self):
self.shutdown_signal = False
try:
HTTPServer.serve_forever(self)
except KeyboardInterrupt:
pass
finally:
self.server_close()
你能看到就是扩展了HTTPServer.serve_forever
函数,最后执行server_close
if hasattr(selectors, 'PollSelector'):
_ServerSelector = selectors.PollSelector
else:
_ServerSelector = selectors.SelectSelector
……
def serve_forever(self, poll_interval=0.5):
"""Handle one request at a time until shutdown.
Polls for shutdown every poll_interval seconds. Ignores
self.timeout. If you need to do periodic tasks, do them in
another thread.
"""
self.__is_shut_down.clear()
try:
with _ServerSelector() as selector:
selector.register(self, selectors.EVENT_READ)
while not self.__shutdown_request:
ready = selector.select(poll_interval)
# bpo-35017: shutdown() called during select(), exit immediately.
if self.__shutdown_request:
break
if ready:
self._handle_request_noblock()
self.service_actions()
finally:
self.__shutdown_request = False
self.__is_shut_down.set()
我们去寻找_ServerSelector
函数,最后指向的是标准库selectors.py
,实现IO模型select、poll、epoll,kqueue(大部分unix系统上都存在,包括OS X)的封装。这里用的是 poll
,监控一组文件句柄,返回当有活跃的文件描述符活跃,去执行_handle_request_noblock
def _handle_request_noblock(self): """Handle one request, without blocking. I assume that selector.select() has returned that the socket is readable before this function was called, so there should be no risk of blocking in get_request(). """ try: request, client_address = self.get_request() except OSError: return if self.verify_request(request, client_address): try: self.process_request(request, client_address) except Exception: self.handle_error(request, client_address) self.shutdown_request(request) except: self.shutdown_request(request) raise else: self.shutdown_request(request)
注释和函数名都表明了,只要在selector返回的ready为真情况下,执行过程就不可能被阻塞。
request, client_address = self.get_request()
这个get_request
函数在HTTPServer
的父类TCPServer
里定义了
def get_request(self): """Get the request and client address from the socket. May be overridden. """ return self.socket.accept()
最后走到了socket
的accept
. 返回新的socket(这里是连接套接字, 而前面的socket则是监听套接字),和请求方的地址,在socket编程里,我们执行accept函数会阻塞,一直到客户端有消息过来,但其实这里是根本不会阻塞的,这就是poll
io模型的特点,遍历所有的连接,直到找到一个有新消息的连接就返回真,通知服务端的socket的去accept,这时候是必定能收到值。
再回到上面的_handle_request_noblock
我们看到了一个 process_request
,这个名字也告诉了我们,是请求到来时执行的操作,这个里面其实是在同步的执行指令。
def process_request(self, request, client_address): """Call finish_request. Overridden by ForkingMixIn and ThreadingMixIn. """ self.finish_request(request, client_address) self.shutdown_request(request)
注释也告诉我们这是可以被ForkingMixIn、ThreadingMixIn改写,所以在werkzeug的make_server
函数中另外两个WSGIServer其实就是去继承这两个类来改写这个process_request
来实现多线程或多进程.
在Server_forever
中我们是循环的去调用selector.select(poll_interval)
来获取活跃的socket,然后执行我们的处理函数,在里面或是用os.fork
实现多进程或是thread
实现多线程。
2.2 添加路由
现在我们已经有了一个基本的wsgi应用,接下来我们需要完善一下应用的路由规则。
Werkzeug 提供了一个灵活的集成路由。 你需要创建一个 Map
实例并添加一系列 Rule
对象。每一个Rule
我们可以传入两个参数,一个是url
, 一个是endpoint
我们在MyApp
中维护一个Map
, 并在create_app
函数里注册我们的路由
# 添加路由 app.url_map = Map( [Rule('/', endpoint="new_url")] )
endpoint
在这里其实就是路由对应的函数的名称,接下来我们实现的就是通过endpoint
去指向一个函数
def dispatch_request(self, request): # 根据请求路由找出匹配的endpoint,value是一个字典,代表的是路由的位置参数 adapter = self.url_map.bind_to_environ(request.environ) try: endpoint, values = adapter.match() # 通过 endpoint + _handler 找到对应的函数 return getattr(self, endpoint + '_handler')(request, **values) except HTTPException as e: print(repr(e)) return Response("hello world")
2.2.1 flask是如何添加路由的呢
装饰器@route(“/home”, methods=["GET"])
添加路由,我们可以从源码分析下
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
rule指的就是路由,这个endpoint
其实和上面的是一个意思,主要逻辑都在add_url_rule
def add_url_rule(
self,
rule,
endpoint=None,
view_func=None,
provide_automatic_options=None,
**options,
):
# 没有传入endpoint就默认为view_func的函数名
if endpoint is None:
endpoint = _endpoint_from_view_func(view_func)
options["endpoint"] = endpoint
# 获取当前view_func支持的所有函数
methods = options.pop("methods", None)
methods = {item.upper() for item in methods}
required_methods = set(getattr(view_func, "required_methods", ()))
methods |= required_methods
# 生成werkzeug Rule对象 绑定endpoint和路由地址
rule = self.url_rule_class(rule, methods=methods, **options)
# werkzeug Map对象中添加Rule对象
self.url_map.add(rule)
# 通过endpoint来寻找绑定的视图函数,如果已经绑定了且和当前函数不同就抛错
if view_func is not None:
old_func = self.view_functions.get(endpoint)
if old_func is not None and old_func != view_func:
raise AssertionError(
"View function mapping is overwriting an existing"
f" endpoint function: {endpoint}"
)
# 存取view_func到属性view_functions里
self.view_functions[endpoint] = view_func
整个路由添加的流程是先用路由地址和endpoint
生成Rule
对象绑定到Map
对象--也就是self.url_map
,最后检查endpoint
关联的view_func
是否是被更新了,更新了则抛出异常,否则就添加endpoint
和view_func
的键值对到self.view_functions
。如果是flask的蓝图添加的路由,则endpoint
变为:蓝图名称 + “.” + endpoint
2.2 添加WSGI中间件
从WSGI协议中我们得知可以通过app=mw1(app)
的方式添加中间件
我们新建一个中间件类,作用是记录整个响应过程用时。结构和我们的MyApp
类很像,实现了一个__call__
这也是一个WSGI应用,不同的是我们先保存了一个application,然后再执行中间件是实例时去处理保存的该application
class MyMiddleWare(object):
"""
wsgi中间件
"""
def __init__(self, application):
self.application = application
print("创建middleware")
def __call__(self, environ, start_response):
b = time.time()
result = self.application(environ, start_response)
duration = (time.time() - b)/1000
print("duration: %f" % duration)
return result
现在我们在create_app
中添加该中间件, 我们可以直接改写app的wsgi_app
函数为中间件的对象(实现了__call__
使它可以像函数样调用)
app.wsgi_app = MyMiddleWare(app.wsgi_app)
2.2.1 Flask中如何添加中间件呢
在dsp项目的app_runner
文件中
def create_app(): flask_app = Flask('csp-controller') with flask_app.app_context(): i18n(flask_app) create_db(flask_app) configure_models() configure_blueprints(flask_app) init_monitor(flask_app) setup_default_data() add_app_hook(flask_app) return flask_app def run_worker(app=None): from app.scheduling.celery_app import make_celery if not app: app = create_app() celery_app = make_celery(app)
我们能看到在初始化了flask_app
这个wsgi应用后,传入到下面的几个
2.3 最终代码
from werkzeug.wrappers import Response
class MyMiddleWare(object):
"""
wsgi中间件
"""
def __init__(self, application):
self.application = application
print("创建middleware")
def __call__(self, environ, start_response):
b = time.time()
result = self.application(environ, start_response)
duration = (time.time() - b)/1000
print("duration: %f" % duration)
return result
class MyApp(object):
def __init__(self):
self.url_map = None
print("创建app")
def url_adapter(self):
pass
# handler方法需要返回Response对象(werkzeug封装的实现wsgi application)
def new_url_handler(self, request):
return Response('{"code": 0}', status=404)
def dispatch_request(self, request):
adapter = self.url_map.bind_to_environ(request.environ)
try:
# 根据请求路由找出匹配的endpoint,value是一个字典,代表的是路由的位置参数
endpoint, values = adapter.match()
# 通过 endpoint + _handler 找到对应的函数
return getattr(self, endpoint + '_handler')(request, **values)
except HTTPException as e:
print(repr(e))
return Response("hello world")
def wsgi_app(self, environ, start_response):
request = Request(environ)
response = self.dispatch_request(request)
return response(environ, start_response)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def create_app():
app = MyApp()
# 加入中间件
app.wsgi_app = MyMiddleWare(app.wsgi_app)
# 添加路由,endpoint指向的是一个函数,通过路由地址绑定到该endpoint上
app.url_map = Map(
[Rule('/', endpoint="new_url")]
)
return app
if __name__ == '__main__':
app = create_app()
run_simple('127.0.0.1', 5000, app)
3. 源码解析
2.1 开始
2.2 werkzeug reloader 机制
2.2.1 介绍
我们在本地开发flask应用时,常常会用到debug模式,类似这样app.run(debug=True)
他为我们调试代码带来了很多方便,其中就包括代码修改后本地服务的自动reload。但这个功能并不是flask提供的,而是werkzeug。整个过程大概的流程是这样的
当我们在主进程中使用debug模式启动flask,这时不在是简单的通过run_simple()
中的inner()
函数启动server,而是我们的werkzeug主进程派生出一个子进程,这个子进程负责运行我们的flask应用,还有启用reloader`来监控代码文件的变化,一旦代码发生改变,退出子进程,而我们的父进程获知到子进程退出,则重新的去创建子进程,如此循环达到一次服务的reload。
2.3.2 源码分析
在上面提到的werkzeug的run_simple()
有这么一段,是用来启动werkzeug的reloader
if use_reloader:
# 判断当前进程是否是reloder生成的子进程
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."
)
# 先创建socket,监听服务端口,如果当前操作系统允许根据文件操作符打开socket,就把fd
# 保存到环境变量,否则先关闭连接
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)
s.set_inheritable(True)
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)
from ._reloader import run_with_reloader
run_with_reloader(inner, extra_files, reloader_interval, reloader_type)
将inner()
函数传入到run_with_reloader()
reloader = reloader_loops[reloader_type](extra_files, interval) signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)) try: if os.environ.get("WERKZEUG_RUN_MAIN") == "true": ensure_echo_on() t = threading.Thread(target=main_func, args=()) t.setDaemon(True) t.start() reloader.run() else: sys.exit(reloader.restart_with_reloader()) except KeyboardInterrupt: pass
这一段代码的是根据环境变量WERKZEUG_RUN_MAIN
来判断当前进程是不是子进程,如果是子进程,那么环境变量的值就为true
,后面我们会看到在主进程去生成进程的时候会设置这个变量。如果是子进程,其实就是开了一个线程用于跑我们的inner()
,然后在子进程的主线程去启动我们在上方选择的reloader
。
werkzeug
有两种reloader
,StatReloaderLoop
和WatchdogReloaderLoop
,默认是StatReloaderLoop
通过遍历整个项目下的文件mtime和上次reload的存的进行比较。而WatchdogReloaderLoop
则是借助第三方包
watchdog
实现。
现在我们还是在主进程,且环境变量并没有设置,进入restart_with_reloader()
def restart_with_reloader(self):
"""Spawn a new Python interpreter with the same arguments as the
current one, but running the reloader thread.
"""
while 1:
_log("info", f" * Restarting with {self.name}")
args = _get_args_for_reloading()
new_environ = os.environ.copy()
new_environ["WERKZEUG_RUN_MAIN"] = "true"
exit_code = subprocess.call(args, env=new_environ, close_fds=False)
if exit_code != 3:
return exit_code
到此就是整个reloader
的实现的核心部分,
-
_get_args_for_reloading()
这个是获取的是运行当前进程的执行参数,也就是我们启动flask的完整命令["python", "xxx.py"]
类似这样。 -
new_environ = os.environ.copy()
是复制当前的环境变量,并添加环境变量WERKZEUG_RUN_MAIN
。 -
标准库的
subprocess.call()
这是阻塞式的系统命令执行方式,并指定运行环境变量,需要注意的是,这个subprocess.call()
此时是阻塞的,且如果当前主进程异常退出,是会为我们kill掉生成的子进程。 -
exit_code != 3:
这是父进程用来判断子进程是否需要重启的重要判断,因为如果是文件改动,子进程的reloader将会调用sys.exit(3)
退出子进程。
其实到此我们也清楚了整个werkzeug reloader的具体实现方式:父进程轮询创建子进程的步骤,并监控子进程的退出码来判断是否要退出轮询,而子进程就是父进程的所有运行环境的copy,只是通过环境变量WERKZEUG_RUN_MAIN
来控制代码要走的逻辑分支。