在学习Flask Web开发时遇到了WSGI,那么WSGI是什么呢?WSGI和Flask有什么关系呢?
一、WSGI为什么会出现?
在学习一个东西之前,我们肯定想知道:它为什么会出现?那么,WSGI为什么会出现呢?
我们知道,部署一个web应用,经常需要使用nginx、apache或者IIS等web服务器把web应用跑起来,然后用户在浏览器可以通过URL进行访问。
为了能够让各种web服务器都能支持web应用,所以必须在web应用和web服务器之间有一个统一的规范(协议)。
其实,在PEP 3333中也有提到它的目标:
为了定义了一个“ Web 服务器和 Python Web 应用程序或框架之间”的标准接口,以便提升 Web 应用程序在不同 Web 服务器上的可移植性。
注意:
1、这里的web应用是Python Web应用程序。
2、这里的web服务器是指nginx、apache等。
3、WSGI为什么会出现在WSGI有更加详细的说明。
https://github.com/python/peps/blob/master/pep-0333.txt
二、什么是WSGI
全称Python Web Server Gateway Interface,指定了web服务器和Python web应用或web框架之间的标准接口,以提高web应用在一系列web服务器间的移植性。
具体可查看 官方文档
从上面的介绍看出,WSGI是一种协议或规范:描述web server如何与web application通信的规范,以及webapp怎么处理前端请求。
一个web服务具体流程类:
- 用户操作操作浏览器发送请求;
- 请求转发至对应的web服务器
- web服务器将请求转交给web应用程序,web应用程序处理请求
- web应用将请求结果返回给web服务器,由web服务器返回用户响应结果
- 浏览器收到响应,向用户展示
WSGI的主要作用是在Web服务器(uwsgi)和Web应用程序(application)承担“翻译官”的角色。对于这一角色可以这样理解:
- Web服务器的责任在于监听和接收请求。在处理请求的时候调用WSGI提供的标准化接口,将请求的信息转给WSGI;
- WSGI的责任在于“中转”请求和响应信息,还有将HTTP报文转换为WSGI规定的格式。WSGI接收到Web服务器提供的请求信息后可以做一些处理,之后通过标准化接口调用Web应用,并将请求信息传递给Web应用。同时,WSGI还将会处理Web应用返回的响应信息,并通过服务器返回给客户端;
- Web应用的责任在于接收请求信息,并且生成响应。
根据以上分析,要实现符合WSGI标准的Web服务,服务器和应用程序的设计就要符合WSGI规范。
有了wsgi这份接口规范,在web开发的过程中,能更加自由的选择服务器端和框架;在服务器端和框架的开发过程能够分离开来,不用过多的考虑双方具体的实现,使得服务器端和框架开发者能够专心自己领域的开发工作。
三、WSGI的实现
因为WSGI其实就是一个协议,根据官方的定义,大致内容如下:
● WSGI application能够调用python对象(函数或者一个带有__call__方法的类。__call__方法有2个参数:第一个参数是WSGI的environ,第二个参数是一个start_response函数。
● application必须使用start_response(status,headers),并且返回值是一个可迭的代序列,序列中的每个对象将标准输出。
● WSGI environ和CGI environ一样,都是一些键值对,要么是提供给server,要么提供给middleware。
● 可以将包装后的middleware添加到你的app中。
一起实现一个简单WSGI服务吧
def application(environ, start_response):
status = "200 OK"
response_headers = [('Content-Type', 'text/html')]
start_response(status, response_headers)
path = environ['PATH_INFO'][1:] or 'hello'
return [b'<h1> %s </h1>' % path.encode()]
方法 application由 web服务器调用,参数env,start_response 由 web服务器实现并传入。其中,env是一个字典,包含了类似 HTTP_HOST,HOST_USER_AGENT,SERVER_PROTOCO 等环境变量。start_response则是一个方法,该方法接受两个参数,分别是status,response_headers。application方法的主要作用是,设置 http 响应的状态码和 Content-Type 等头部信息,并返回响应的具体结果。
接下来,我们需要一个服务器启动WSGI服务器用来处理验证,使用Python内置的WSGI服务器模块wsgiref,编写server.py:
# coding:utf-8
"""
desc: WSGI服务器实现
"""
from wsgiref.simple_server import make_server
from learn_wsgi.client import application
def main():
server = make_server('localhost', 8001, hello)
print('Serving HTTP on port 8001...')
server.serve_forever()
if __name__ == '__main__':
main()
执行python server.py,浏览器打开"http://localhost:8001/a",即可验证。
上述代码就是一个完整的 WSGI 应用,当一个支持 WSGI 的 web服务器接收到客户端的请求后,便会调用这个 application 方法。WSGI 层并不需要关心env,start_response 这两个变量是如何实现的,就像在 application 里面所做的,直接使用这两个变量即可。
当然,以上只是一个简单的案例,那么在python的Web框架内部是如何遵循WSGI规范的呢?以Flask举例,而这个叫做application的东西,在Flask框架内部的名字,叫做wsgi_app。
四、Werkzeug
都知道Flask是一个web框架,而且Flask是基于werkzeug开发的,那werkzeug是什么呢?
Werkzeug是一个WSGI工具包,他可以作为一个Web框架的底层库。这里稍微说一下, werkzeug 不是一个web服务器,也不是一个web框架,而是一个工具包,官方的介绍说是一个 WSGI 工具包,它可以作为一个 Web 框架的底层库,因为它封装好了很多 Web 框架的东西,例如 Request,Response 等等。使用它可以减轻web框架开发工作量。werkzeug也实现了WSGI容器的功能。WSGI容器的作用就是根据web服务器传递而来的参数构建一个让WSGI应用成功执行的环境,例如request,而且还得把WSGI应用上处理好的结果返回给web服务器。此外WSGI容器也叫应用服务器。而且利用python/http/server.py库实现了一个简易的http服务器。因此在调试的时候可以直接使用app.run()把服务器给运行起来。
注: 一般应用服务器都集成了web服务器,主要是为了调试方便,出于性能和稳定性考虑,并不能在生产环境中使用。
WSGI简化了编写Web app的复杂度,使程序员不必关注底层的数据传输而专注于Web本身。框架则基于WSGI进一步抽象,用一个函数处理一个URL。而URL与函数的绑定,称为路由(route),而这些就交给Web框架来做了。Python Flask的路由,是由装饰器实现的
五、Flask的WSGI的实现
有了上面的知识,从最简单的这个flask程序来看WSGI的实现。
- 使用app.run()方法来启动flask应用(app.run()代码隐藏着创建一个服务器),app应用本身会作为参数传递到WSGI服务器中。
- 在客户端(这里就是浏览器)输入网址(发送一个请求),服务器使用WSGI 中间件来处理这个请求。
- WSGI 处理请求对应着wsgi_app(self, environ, start_response)方法,self参数对应着app,即flask程序;environ和 start_response由服务器提供。
- wsgi_app()作用就是调用各种请求处理函数来处理请求,然后返回处理结果。即用户输入网址后,看到了网页响应。
from flask import Flask
app = Flask(__name__)
#生成app实例,传递 __name__参数,__name__ 就是当前模块名字。
@app.route("/")
def index():
return "hello world"
if __name__ == '__main__':
app.run(debug=True)
5.1 首先 app.run() 方法开始
看 run() 方法的定义,调用了werkzeug库中的一个 run_simple() 方法,最后启动了 BaseWSGIServer 服务器。 运行 run() 方法是只传递了 debug=True 参数。 看 run() 方法内部:
- 第一个 if 语句设置默认host参数值为 127.0.0.1
- 第二个 if 语句设置默认port参数值为5000
- 第三个 if 语句中传递了debug 参数值为 True
- the options to be forwarded to the underlying Werkzeug server. 这里把debug状态传递到底层的Werkzeug server。即use_reloader=True ; use_debugger=True
- 最后调用werkzeug库中的一个run_simple()方法。同时,传递了刚刚设置的几个参数
def run(self, host=None, port=None, debug=None, **options):
"""Runs the application on a local development server.
...
"""
from werkzeug.serving import run_simple
if host is None:
host = '127.0.0.1'
if port is None:
server_name = self.config['SERVER_NAME']
if server_name and ':' in server_name:
port = int(server_name.rsplit(':', 1)[1])
else:
port = 5000
if debug is not None:
self.debug = bool(debug)
options.setdefault('use_reloader', self.debug)
options.setdefault('use_debugger', self.debug)
try:
run_simple(host, port, self, **options)
finally:
self._got_first_request = False
5.2 run_simple() 方法
-
hostname, port, application 对应着刚才run()方法中传递过来的host, port, self 参数。(这里self 就是Flask实例化了的app)
-
同时run()方法中还传递了user_debugger=True;user_loader=True 。剩余的参数使用初始值。
-
根据上面 user_loader=True,第一个if语句成立,调用了werkzeug.debug模块中的 DebuggedApplication类来对应用程序包装一次。传入了application参数和use_evalex参数,调用run_simple()方法时设置了use_evalex=True。DebuggedApplication类的简单说明:Enables debugging support for a given application
-
第二个if条件语句不成立,不执行之后的代码。
-
定义了log_startup函数和inner()函数,使用的时候再看具体实现了什么。
-
if use_reloader: 成立,会执行之后的代码。最关键的一行代码: run_with_reloader(inner, extra_files, reloader_interval, reloader_type)。调用了run_with_reloader方法,inner作为run_with_reloader方法中的main_func 参数;run_simple()方法设置了extra_files=None ,reloader_interval=1, reloader_type='auto' 同时作为参数传递到run_with_reloader方法中。
-
然后,inner()方法中关键语句:make_server()创建http服务器; server_forever()让服务器不要关闭,一直等待下一个请求。
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.
:param hostname: The host for the . eg: ``'localhost'``
:param port: The port for the server. eg: ``8080``
:param application: the WSGI application to execute
...
(省略了其余的参数介绍。)
"""
if use_debugger:
from werkzeug.debug import DebuggedApplication
application = DebuggedApplication(application, use_evalex)
if static_files:
from werkzeug.wsgi import SharedDataMiddleware
application = SharedDataMiddleware(application, static_files)
def log_startup(sock):
display_hostname = hostname not in ('', '*') and hostname or 'localhost'
if ':' in display_hostname:
display_hostname = '[%s]' % display_hostname
quit_msg = '(Press CTRL+C to quit)'
port = sock.getsockname()[1]
_log('info', ' * Running on %s://%s:%d/ %s',
ssl_context is None and 'http' or '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 os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
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_ip_version(hostname, port)
s = socket.socket(address_family, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((hostname, port))
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()
# Do not use relative imports, otherwise "python -m werkzeug.serving"
# breaks.
from werkzeug._reloader import run_with_reloader
run_with_reloader(inner, extra_files, reloader_interval,
reloader_type)
else:
inner()
5.3 makeserver() 方法
- inner()方法中调用makeserver()方法时传递了所有需要的参数: hostname = 127.0.0.1;port = 5000; app:在这里就是flask 程序;thread-线程。 processes = 1 单进程 …
- 根据判断条件,make_server()方法最后会返回BaseWSGIServer(host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd) 来启动BaseWSGIServer,创造一个单线程,单进程的WSGI 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)
5.4 class BaseWSGIServer(HTTPServer, object)
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_ip_version(host, port)
if fd is not None:
real_sock = socket.fromfd(fd, self.address_family,
socket.SOCK_STREAM)
port = 0
HTTPServer.__init__(self, (host, int(port)), 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 log(self, type, message, *args):
_log(type, message, *args)
def serve_forever(self):
self.shutdown_signal = False
try:
HTTPServer.serve_forever(self)
except KeyboardInterrupt:
pass
finally:
self.server_close()
def handle_error(self, request, client_address):
if self.passthrough_errors:
raise
return HTTPServer.handle_error(self, request, client_address)
def get_request(self):
con, info = self.socket.accept()
return con, info
5.5 HTTPServer类
- HTTPServer类在python安装路径的Lib/http/server.py 模块中。
- HTTPServer类实现了一个server_bind()方法用来绑定服务器地址和端口。
- class HTTPServer(socketserver.TCPServer) HTTPServer类继承了socketserver模块中的TCPServer类。
class HTTPServer(socketserver.TCPServer):
allow_reuse_address = 1 # Seems to make sense in testing environment
def server_bind(self):
"""Override server_bind to store the server name."""
socketserver.TCPServer.server_bind(self)
host, port = self.server_address[:2]
self.server_name = socket.getfqdn(host)
self.server_port = port
5.6 TCPServer
类
- TCPServer在socketserver模块中。首先,socket 介绍: A network socket is an internal endpoint for sending or receiving data at a single node in a computer network. 。
- TCPServer类中的介绍:Base class for various socket-based server classes. Defaults to synchronous IP stream (i.e., TCP).
- TCPServer类继承了BaseServer基类。都是定义了服务器端的基本属性。后面的继承类可以重写这里的一部分属性。
5.7 再回到BaseWSGIServer
类
BaseWSGIServer
类重写了一大堆东西,首先 __init__
初始化类中的变量,供类中其他函数调用。
- __init__ 函数首先定义了
handler = WSGIRequestHandler
来处理请求。 WSGIRequestHandler
简单介绍:A request handler that implements WSGI dispatching.class WSGIRequestHandler(BaseHTTPRequestHandler, object)
WSGIRequestHandler
继承自BaseHTTPRequestHandler
。class BaseHTTPRequestHandler(socketserver.StreamRequestHandler)
BaseHTTPRequestHandler
继承自socketserver
模块中的StreamRequestHandler
class StreamRequestHandler(BaseRequestHandler)
StreamRequestHandler
继承自BaseRequestHandler
,就是用来处理请求的类。- 最初的
BaseRequestHandler
基类定义的属性很少,一层层的重写,到了WSGIRequestHandler
类,越来越完善了。 - 最重要的要知道,这个
WSGIRequestHandler
类是用来处理请求的。
def __init__(self, host, port, app, handler=None,
passthrough_errors=False, ssl_context=None, fd=None):
if handler is None:
handler = WSGIRequestHandler
...
8. WSGIRequestHandler
类中定义了很多方法。因为WSGI 是单线程、单进程的server,来看这个handle_one_request(self)
方法,用来处理一个请求。
def handle_one_request(self):
"""Handle a single HTTP request."""
self.raw_requestline = self.rfile.readline()
if not self.raw_requestline:
self.close_connection = 1
elif self.parse_request():
return self.run_wsgi()
9. 调用了run_wsgi()
方法,run_wsgi()方法方法好长,重点看这句execute(self.server.app)
,在这儿处理请求使用Flask中的__call__ 方法。。app(Flask实例对象)作为参数在make_server()
时已经传递到服务器中了。
def run_wsgi(self):
...
def write(data):
...
def start_response(status, response_headers, exc_info=None):
...
return write
def execute(app):
...
try:
execute(self.server.app)
except ...
...
10. 服务器收到http请求,去调用app的时候,实际上是用了Flask 的 __call__
方法,会调用wsgi_app()
方法。
def __call__(self, environ, start_response):
"""Shortcut for :attr:`wsgi_app`."""
return self.wsgi_app(environ, start_response)
11. 到了wsgi_app()
,这里wsgi_app作为中间件的存在,连接着服务器和应用程序。对服务器来说wsgi_app是应用程序;对应用程序来说,wsgi_app是服务器。
wsgi_app(self, environ, start_response)需要三个参数,self即需要运行的flask 应用程序,在创建服务器时传递到了 WSGI server。environ, start_response由服务器提供,wsgi_app的功能就是根据请求查找各种请求处理函数,然后返回请求处理结果到服务器。
def wsgi_app(self, environ, start_response):
"""The actual WSGI application. This is not implemented in
`__call__` so that middlewares can be applied without losing a
reference to the class. So instead of doing this::
app = MyMiddleware(app)
: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 response
"""
ctx = self.request_context(environ)
ctx.push()
error = None
try:
try:
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)
六、结束语
最后以 Nginx(web server),WSGI,Flask(web app) 之间的对话结束本文。
===========================================================
Nginx:Hey,WSGI,我刚从用户那里收到了一个请求,现在转发给你。
WSGI:好的,Nginx,我会设置好环境变量,然后将这个请求传递给Flask处理。
Flask:Thanks WSGI!给我一些时间,我将会把请求的响应返回给你。
WSGI:All right,那我等你。
Flask:Okay,我完成了,这里是请求的响应结果,请求把结果传递给Nginx。
WSGI:Good job!Nginx,这里是响应结果,已经按照要求给你传递回来了。
Nginx:Cool,我收到了,我把响应结果返回给客户端。大家合作愉快~