21 Jun 2017
WSGI——Web框架基础
简介
WSGI,全称是Web Server Gateway Interface(Web服务网关接口)。
这是Python中的定义的一个网关协议,规定了Web Server如何跟应用程序进行交互。Web server可以理解为一个Web应用的容器,可以通过Web server来启动应用,进而提供http服务。而应用程序是指我们基于框架所开发的系统。
这个协议最主要的目的就是保证在Python中,所有Web Server程序或者说Gateway程序,能够通过统一的协议跟web框架,或者Web应用进行交互。这对于部署Web程序来说很重要,你可以选择任何一个实现了WSGI协议的Web Server来跑你的程序。
如果没有这个协议,那可能每个程序,每个Web Server都会各自实现各自的接口。
这一节我们来简单了解下WSGI协议是如何运作的,理解这一协议非常重要,因为在Python中大部分的Web框架都实现了此协议,在部署时也使用WSGI容器来进行部署。
简单的Web Server
在看WSGI协议之前,我们先来看一个通过socket编程实现的Web服务的代码。逻辑很简单,就是通过监听本地8080端口,接受客户端发过来的数据,然后返回对应的HTTP的响应。
# coding:utf-8 import socket EOL1 = '\n\n' EOL2 = '\n\r\n' body = '''Hello, world! ‹h1› from the5fire 《Django企业开发实战》‹/h1›''' response_params = [ 'HTTP/1.0 200 OK', 'Date: Sat, 10 jun 2017 01:01:01 GMT', 'Content-Type: text/plain; charset=utf-8', 'Content-Length: {}\r\n'.format(len(body)), body, ] response = b'\r\n'.join(response_params) def handle_connection(conn, addr): request = '' while EOL1 not in request and EOL2 not in request: request += conn.recv(1024) print(request) conn.send(response) conn.close() def main(): # socket.AF_INET 用于服务器与服务器之间的网络通信 # socket.SOCK_STREAM 基于TCP的流式socket通信 serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 设置端口可复用,保证我们每次Ctrl C之后,快速再次重启 serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serversocket.bind(('127.0.0.1', 8080)) # 可参考:https://stackoverflow.com/questions/2444459/python-sock-listen serversocket.listen(1) print('http://127.0.0.1:8080') try: while True: conn, address = serversocket.accept() handle_connection(conn, address) finally: serversocket.close() if __name__ == '__main__': main()
代码的逻辑很简单,但是建议你在自己的电脑上敲一遍,然后Python2运行起来(用Python3的话需要做些调整),通过浏览器访问是否能收到正确响应。并且修改其中代码,观察结果。比如说修改上面Content-Type: text/plain 中的 plain 为 html ,然后Ctrl C结束进程,重新运行,刷新页面,看看结果。
理解这段代码很重要,这是Web服务最基本的模型,通过socket和HTTP协议,提供Web服务。建议你在理解上面的代码之前,不要继续往下学习。
简单的WSGI application
理解了上面的代码之后,我们继续看看WSGI协议,也就是我们一开头介绍的。WSGI协议分为两部分,其中一部分是Web Server或者Gateway,就像上面的代码一样,监听在某个端口上,接受外部的请求。另外一部分是Web Application,Web Server接受到请求之后会通过WSGI协议规定的方式把数据传递给Web Application,我们在Web Application中处理完之后,设置对应的状态和HEADER,之后返回body部分。Web Server拿到返回数据之后,再进行HTTP协议的封装,最终返回完整的HTTP Response数据。
这么说可能比较抽象,我们还是通过代码来演示下这个流程。我们先实现一个简单的application:
# coding:utf-8 def simple_app(environ, start_response): '''Simplest possible application object''' status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return ['Hello world! -by the5fire \n']
这就是一个简单的application,那么我们要怎么运行它呢?我们先按照Python PEP3333文档上的实例代码来运行它。这是一个cgi的脚本。
# coding:utf-8 import os import sys from app import simple_app def run_with_cgi(application): environ = dict(os.environ.items()) environ['wsgi.input'] = sys.stdin 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): 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 sys.stdout.write('Status: %s\r\n' % status) for header in response_headers: sys.stdout.write('%s: %s\r\n' % header) sys.stdout.write('\r\n') sys.stdout.write(data) sys.stdout.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[0], exc_info[1], 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() if __name__ == '__main__': run_with_cgi(simple_app)
我们运行一下这个脚本: python gateway.py,在命令行上能够看到对应的输出:
python gateway.py
Status: 200 OK
Content-type: text/plain
Hello world! -by the5fire
对比下一开始我们通过socket写的server,这个就是一个最基本的HTTP响应了。如果输出给浏览器,浏览器会展示出Hello world! -by the5fire的字样。
我们再通过另外一种方式来运行我们的Application,用到的这个工具就是gunicorn。你可以先通过命令pip install gunicron进行安装。
安装完成之后,进入到app.py脚本的目录。通过命令: gunicorn app:simle_app 来启动程序。这里的gunicron就是一个Web Server。启动之后会看到如下输出:
gunicorn app:simle_app [2017-06-10 22:52:01 +0800] [48563] [INFO] Starting gunicorn 19.4.5 [2017-06-10 22:52:01 +0800] [48563] [INFO] Listening at: http://127.0.0.1:8000 (48563) [2017-06-10 22:52:01 +0800] [48563] [INFO] Using worker: sync [2017-06-10 22:52:01 +0800] [48566] [INFO] Booting worker with pid: 48566
通过浏览器访问:http://127.0.0.1:8000 就能看到对应的页面了。
理解WSGI
通过上面的代码,你应该看到了简单的application中对WSGI协议的实现。你可以在simple_app方法中增加print语句来查看参数分别是什么。
WSGI协议规定,application必须是一个callable对象,这意味这个对象可以是Python中的一个函数,也可以是一个实现了call方法的类的实例。比如这个:
class AppClass(object): status = '200 OK' response_headers = [('Content-type', 'text/plain')] def __call__(self, environ, start_response): print(environ, start_response) start_response(self.status, self.response_headers) return ['Hello AppClass.__call__\n'] application = AppClass()
我们依然可以通过gunicorn这个WSGI Server来启动应用: gunicorn app:aplication,再次访问 http://127.0.0.1:8000 看看是不是输出了同样的内容。
除了这种方式之外,我们可以通过另外一种方式实现WSGI协议,从上面 simple_app 和这里 AppClass.call的返回值来看,WSGI Server中只需要一个可迭代的对象就行,callable也就是返回一个列表。那么我们可以用下面这种方式达到同样的结果:
class AppClassIter(object): status = '200 OK' response_headers = [('Content-type', 'text/plain')] def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): self.start_response(self.status, self.response_headers) yield 'Hello AppClassIter\n'
我们再次使用gunicorn来启动: gunicorn app:AppClassIter,然后打开浏览器访问 http://127.0.0.1:8000,看看结果。
这里的启动命令并不是一个类的实例,而是类本身,为什么呢?通过上面两个代码,我们可以观察到能够被调用的方法会传environ和start_response过来,而现在这个实现,没有可调用的方式,所以就需要在实例化的时候通过参数传递进来,这样在返回body之前,可以先调用start_response方法。
所以我们可以推测出WSGI Server是如何调用WSGI Application的。大概代码如下:
def start_response(status, headers): # 伪代码 set_status(status) for k, v in headers: set_header(k, v) def handle_conn(conn): # 调用我们定义的application(也就是上面的simple_app或者是AppClass的实例或者是AppClassIter本身) app = application(environ, start_response) # 遍历返回的结果,生成response for data in app: response += data conn.sendall(response)
大概如此。
WSGI中间件和Werkzeug(WSGI工具集)
理解了上面的逻辑,我们就可以继续行程了。
除了交互部分的定义,WSGI还定义了中间件部分的逻辑,这个中间件可以理解为Python中的一个装饰器,可以在不改变原方法的同时对方法的输入和输出部分进行处理。
比方说对返回body中的文字部分,把英文转换为中文等之类的操作。或者是一些更为易用的操作,比如对返回内容的封装,上面的例子我们是先调用start_response方法,然后再返回body,我们能不能直接封装一个Response对象呢,直接给对象设置header,而不是这种单独操作的逻辑。比如像这样:
def simple_app(environ, start_response): response = Repsonse('Hello World', start_repsonse=start_response) response.set_header('Content-Type', 'text/plain') return response
这样不是更加自然。
因此就存在了Werkzeug这样的WSGI工具集。让你能够跟WSGI协议更加友好的交互。理论上我们可以直接通过WSGI协议的简单实现,也就是我们上面的代码,写一个Web服务。但是有了Werkzeug之后,我们可以写的更加容易。在很多Web框架中都是通过Werkzeug来处理WSGI协议的内容的。