先说下 WSGI 的外表意思,Web Server Gateway Interface 的缩写,即 Web 服务器网关接口。
之前不晓得 WSGI 意思的搭档,看了下面的解释后,我预计也还是不分明,所以上面结合实际场景阐明,先让大家有个大抵的意识。最初咱们再本人实现一个,加深对 WSGI 的了解。
咱们当初应用 Python 编写 Web 利用,能够用比拟风行的 Flask、Django 框架,也能够按本人的想法间接写一个。可选的服务器软件也特地多,比方常见的有 Apache、Nginx、IIS 等,除此外,也有很多小众的软件。然而,当初问题来了,我该怎么部署?在没有 WSGI 标准之前,一个服务器调度 Python 利用是用这种形式,另一款服务器应用的是那种形式,这样的话,编写进去的利用部署时只能抉择局限的某个或某些服务器,达不到通用的成果。
留神:下文中的代码基于 Python 3.6 编写。
如果有这么一个服务器
wsgi/server.py
# coding=utf-8import socketlistener = socket.socket()listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)listener.bind(('0.0.0.0', 8080))listener.listen(1)print('Serving HTTP on 0.0.0.0 port 8080 ...')while True: client_connection, client_address = \ listener.accept() print(f'Server received connection' f' from {client_address}') request = client_connection.recv(1024) print(f'request we received: {request}') response = b"""HTTP/1.1 200 OKHello, World!""" client_connection.sendall(response) client_connection.close()
实现比较简单,就是监听 8080 端口,如果有申请在终端进行打印,并返回 Hello, World! 的响应。
终端中启动服务器
➜ wsgi python server.pyServing HTTP on 0.0.0.0 port 8080 ...
再开一个终端,申请下
➜ ~ curl 127.0.0.1:8080HTTP/1.1 200 OKHello, World!
阐明服务器工作失常。
另外有一个 Web 利用
wsgi/app.py
# coding=utf-8def simple_app(): return b'Hello, World!\r\n'
当初要部署(也就是让这个整体跑起来),简略粗犷的做法就是在服务器外面间接调用 app 中相应的办法。就像这样
wsgi/server2.py
# coding=utf-8import socketlistener = socket.socket()listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)listener.bind(('0.0.0.0', 8080))listener.listen(1)print('Serving HTTP on 0.0.0.0 port 8080 ...')while True: client_connection, client_address = \ listener.accept() print(f'Server received connection' f' from {client_address}') request = client_connection.recv(1024) print(f'request we received: {request}') from app import simple_app response = 'HTTP/1.1 200 OK\r\n\r\n' response = response.encode('utf-8') response += simple_app() client_connection.sendall(response) client_connection.close()
运行脚本
留神:因为应用端口雷同的缘故,请先敞开上次的脚本,而后再执行,不然会因为端口抵触而报错。
➜ wsgi python server2.pyServing HTTP on 0.0.0.0 port 8080 ...
而后申请一下看看成果
➜ ~ curl 127.0.0.1:8080Hello, World!
嗯,能够了。然而,下面的服务器和利用整体是跑起来了,那么我换一个服务器或者利用呢。因为服务器与利用之间怎么交互齐全没有标准,比方服务器应该如何把申请信息传给利用,利用处理完毕后又怎么通知服务器开始返回响应,如果都是各搞各的,服务器须要定制利用,利用也要定制服务器,这要一个利用能跑起来也太麻烦了点吧。
所以,WSGI 的呈现就是为了解决下面的问题,它规定了服务器怎么把申请信息通知给利用,利用怎么把执行状况回传给服务器,这样的话,服务器与利用都按一个规范办事,只有实现了这个规范,服务器与利用随便搭配就能够,灵便度大大提高。
WSGI 标准了些什么,下图能很直观的阐明。
[图片]
首先,利用必须是一个可调用对象,能够是函数,也能够是实现了 __call__()
办法的对象。
每收到一个申请,服务器会通过 application_callable(environ, start_response) 调用利用。
利用在处理完毕筹备返回数据的时候,先调用服务传给它的函数 start_response(status, headers, exec_info),最初再返回可迭代对象作为数据。(不了解可迭代对象的搭档能够看下我之前的一篇文章《搞清楚Python的迭代器、可迭代对象、生成器》)
其中,environ 必须是一个字典,包含了申请的相干信息,比方申请形式、申请门路等等,start_response 是利用处理完毕后,须要调用的函数,用于通知服务设置响应的头部信息或错误处理等等。
status 必须是 999 Message here
这样的字符串,比方 200 OK
、404 Not Found
等,headers 是一个由 (header_name, header_value) 这样的元祖组成的列表,最初一个 exec_info 是可选参数,个别在利用呈现谬误的时候会用到。
晓得了 WSGI 的大抵概念,上面咱们来实现一个。
首先是利用
wsgi/wsgi_app.py
# coding=utf-8def simple_app(environ, start_response): status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [f'Request {environ["REQUEST_METHOD"]}' f' {environ["PATH_INFO"]} has been' f' processed\r\n'.encode('utf-8')]
这里定义了一个函数(可调用对象),它能够应用服务器传给它的申请相干的内容 environ,这里应用了 REQUEST_METHOD 和 PATH_INFO 信息。在返回之前调用了 start_response,不便服务器设置一些头部信息。
而后是服务器
wsgi/wsgi_server.py
# coding=utf-8import socketlistener = socket.socket()listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)listener.bind(('0.0.0.0', 8080))listener.listen(1)print('Serving HTTP on 0.0.0.0 port 8080 ...')while True: client_connection, client_address = \ listener.accept() print(f'Server received connection' f' from {client_address}') request = client_connection.recv(1024) print(f'request we received: {request}') headers_set = None def start_response(status, headers): global headers_set headers_set = [status, headers] method, path, _ = request.split(b' ', 2) environ = {'REQUEST_METHOD': method.decode('utf-8'), 'PATH_INFO': path.decode('utf-8')} from wsgi_app import simple_app app_result = simple_app(environ, start_response) response_status, response_headers = headers_set response = f'HTTP/1.1 {response_status}\r\n' for header in response_headers: response += f'{header[0]}: {header[1]}\r\n' response += '\r\n' response = response.encode('utf-8') for data in app_result: response += data client_connection.sendall(response) client_connection.close()
服务器监听相干代码没怎么变动,次要是解决申请的时候有些不同。
首先定义了 start_response(status, headers) 函数,本身并不会调用。
而后调用利用,将以后的申请信息 environ 和下面的 start_response 函数传给它,让其本人决定应用什么申请信息以及在解决实现筹备返回数据之前调用 start_response 设置头部信息。
好了,启动服务器后(即执行服务器代码,和之前的相似,这里不赘述),而后申请看看后果
➜ ~ curl 127.0.0.1:8080/user/1Request GET /user/1 has been processed
嗯,程序是失常的。
下面为了阐明,代码耦合性较大,如果服务器须要更换利用的话,还得批改服务器代码,这显然是有问题的。当初原理差不多说分明了,咱们把代码优化下
wsgi/wsgi_server_oop.py
# coding=utf-8import socketimport sysclass WSGIServer: def __init__(self): self.listener = socket.socket() self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.listener.bind(('0.0.0.0', 8080)) self.listener.listen(1) print('Serving HTTP on 0.0.0.0' ' port 8080 ...') self.app = None self.headers_set = None def set_app(self, application): self.app = application def start_response(self, status, headers): self.headers_set = [status, headers] def serve_forever(self): while True: listener = self.listener client_connection, client_address = \ listener.accept() print(f'Server received connection' f' from {client_address}') request = client_connection.recv(1024) print(f'request we received: {request}') method, path, _ = request.split(b' ', 2) # 为简洁的阐明问题,这里填充的内容有些随便 # 如果有须要,能够自行欠缺 environ = { 'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': request, 'wsgi.errors': sys.stderr, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'REQUEST_METHOD': method.decode('utf-8'), 'PATH_INFO': path.decode('utf-8'), 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8080', } app_result = self.app(environ, self.start_response) response_status, response_headers = self.headers_set response = f'HTTP/1.1 {response_status}\r\n' for header in response_headers: response += f'{header[0]}: {header[1]}\r\n' response += '\r\n' response = response.encode('utf-8') for data in app_result: response += data client_connection.sendall(response) client_connection.close()if __name__ == '__main__': if len(sys.argv) < 2: sys.exit('Argv Error') app_path = sys.argv[1] module, app = app_path.split(':') module = __import__(module) app = getattr(module, app) server = WSGIServer() server.set_app(app) server.serve_forever()
基本原理没变,只是应用了面向对象的形式批改了下原来的代码,同时 environ 增加了一些必要的环境信息。
能够应用以前的利用
➜ wsgi python wsgi_server_oop.py wsgi_app:simple_appServing HTTP on 0.0.0.0 port 8080 ...
申请
➜ ~ curl 127.0.0.1:8080/user/1Request GET /user/1 has been processed
失去和之前雷同的后果。
Flask 利用能行吗?来试一试,先新建一个
wsgi/flask_app.py
# coding=utf-8from flask import Flaskfrom flask import Responseflask_app = Flask(__name__)@flask_app.route('/user/<int:user_id>', methods=['GET'])def hello_world(user_id): return Response( f'Get /user/{user_id} has been' f' processed in flask app\r\n', mimetype='text/plain' )
重新启动服务器
➜ wsgi python wsgi_server_oop.py flask_app:flask_appServing HTTP on 0.0.0.0 port 8080 ...
申请
➜ ~ curl 127.0.0.1:8080/user/1Get /user/1 has been processed in flask app
因为 Flask 也是恪守 WSGI 标准的,所以执行也没有问题。
至此,一个粗略的 WSGI 标准就实现了,虽说代码不优雅,一些外围的货色还是体现进去了。不过毕竟疏忽了很多货色,比方错误处理等,要在生产环境中应用的话还远远不够,想晓得得更全面的搭档能够去看看 PEP 3333。
目前风行的 Web 利用框架比方 Django、Bottle 等,服务器 Apahce、Nginx、Gunicorn 等也都反对这个标准。因而,框架和利用随便搭配根本没什么问题。
参考
- https://www.python.org/dev/pe...
- http://ruslanspivak.com/lsbaw...
- https://www.toptal.com/python...
- https://gist.github.com/thoma...
原文链接:https://www.kevinbai.com/arti...
关注「小小后端」公众号获取最新文章推送!