《Django企业开发实战》试读:2.2 WSGI——Web 框架基础

2.2.1 简介 WSGI,全称是Web Server Gateway Interface(Web 服务器网关接口)。这是Python 中定义的一个网关协议,规定了Web Server 如何跟应用程序交互。Web Server 可以理解为一个Web 应用的容器,通过它可以启动应用,进而提供HTTP 服务。而应用程序是指我们基于框架所开发的系统。 这个协议最主要的目的就是保证在Python 中所有Web Server 程序或者说Gateway 程序,能够通过统一的协议跟Web 框架或者说Web 应用进行交互。这对于部署Web 程序来说很重要,你可以选择任何一个实现了WSGI 协议的Web Server 来跑你的程序。 如果没有这个协议,那么每个程序、每个Web Server 可能都会实现各自的接口,实现各自的“轮子”,最终的结果会是一团乱。 使用统一协议的另外一个好处就是,Web 应用框架只需要实现WSGI,就可以跟外部请求进行交互了,不用去针对某个Web Server 来独立开发交互逻辑,开发者可以把精力放在框架本身。这一节中,我们简单了解WSGI 协议是如何运作的。理解这一协议非常重要,因为在Python中大部分的Web 框架都实现了此协议,也使用WSGI 容器来进行部署。 2.2.2 简单的Web Server 在了解WSGI 协议之前,我们先来看一个通过socket 编程实现的Web 服务的代码。其逻辑很简单,就是通过监听本地8000 端口,接收客户端发过来的数据,然后返回对应的HTTP 响应: # 文件位置:/code/chapter2/section2/socket_server.py # coding:utf-8 import socket EOL1 = b'\n\n' EOL2 = b'\n\r\n' body = '''Hello, world! <h1> from the5fire 《Django 企业开发实战》</h1>''' response_params = [ 'HTTP/1.0 200 OK', 'Date: Sun, 27 may 2018 01:01:01 GMT', 'Content-Type: text/plain; charset=utf-8', 'Content-Length: {}\r\n'.format(len(body.encode())), body, ] response = '\r\n'.join(response_params) def handle_connection(conn, addr): request = b"" while EOL1 not in request and EOL2 not in request: request += conn.recv(1024) print(request) conn.send(response.encode()) # response 转为bytes 后传输 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', 8000)) serversocket.listen(5) # 设置backlog——socket 连接最大排队数量 print('http://127.0.0.1:8000') try: while True: conn, address = serversocket.accept() handle_connection(conn, address) finally: serversocket.close() if __name__ == '__main__': main() 这段代码的逻辑很简单,建议你在自己的电脑上敲一遍,然后用Python 3 运行(用Python 2的话,需要做些调整),通过浏览器访问看看能否展示页面。这个页面展示稍微有点问题,需要你修改其中的代码,将上面Content-Type: text/plain 中的plain 修改为 html,然后按Ctrl+C 组合键结束进程,重新运行,刷新页面,看看结果。这里思考一下为什么会不同。你也可以修改其他代码,查看结果有何不同。 理解这段代码很重要!这是Web 服务最基本的模型,通过socket 和HTTP 协议提供Web 服务。建议你在理解上面的代码之前,不要继续往下学习。你需要在脑海中有Web 请求处理的模型,才能更好地理解框架为我们提供了哪些能力。 2.2.3 多线程版的Web Server 上面只是简单的单进程、单线程的版本,这能够让你更好地理解Web 服务。现在我们把逻辑变得复杂一些,调整上面的代码,在handle_connection 方法的第一行之前增加如下代码: def handle_connection(conn, addr): print('oh, new conn', conn, addr) import time time.sleep(100) …… 重新运行,然后访问页面,同时再打开一个(或者更多)新的浏览器窗口(注意不是新的标签页),访问页面,同时观察Console 上的输出。看看有什么问题。 思考一下。 有什么发现吗? 好了,问题是这样:当我们处于单进程、单线程模型时,程序接受一个请求,然后需要花100 s 处理,此时新的请求是进不来的,因为只有一个处理程序。类比到生活中就是,你去ATM上取钱,只有一台机器,前面如果有人需要花费100 s 才能完成操作,那么你就要等100 s。 到这里,你应该能明白最简单的模型只是用来理解原理的,实用性并不强。如果我用上面的代码做了个网站,每次只能有一个用户访问,那恐怕就没人来了。 所以,我们需要了解其他的Web 服务模型——多线程模式。 理论是这样的,每来一个新请求,我们就创建一个线程来处理它,这样的话,这个请求处理的耗时不会影响下一个请求。就好像是,只要有人来取钱,就会自动出现一个ATM 一样。 我们来看代码,需要提醒的是,你理解了上面的代码之后,需要再回忆Python 多线程部分的知识点。我知道,对你来说不是难事。来吧,看代码: # 文件位置:/code/chapter2/section2/thread_socketserver.py # coding:utf-8 import errno import socket import threading import time EOL1 = b'\n\n' EOL2 = b'\n\r\n' body = '''Hello, world! <h1> from the5fire 《Django 企业开发实战》</h1> - from {thread_name}''' response_params = [ 'HTTP/1.0 200 OK', 'Date: Sun, 27 may 2018 01:01:01 GMT', 'Content-Type: text/plain; charset=utf-8', 'Content-Length: {length}\r\n', body, ] response = '\r\n'.join(response_params) def handle_connection(conn, addr): # print(conn, addr) # time.sleep(60) # 可以自行尝试打开注释,设置睡眠时间 request = b"" while EOL1 not in request and EOL2 not in request: request += conn.recv(1024) # 注意设置为非阻塞模式时这里会报错, 建议自己搜索一下问题来源 print(request) current_thread = threading.currentThread() content_length = len(body.format(thread_name=current_thread.name).encode()) print(current_thread.name) conn.send(response.format(thread_name=current_thread.name, length=content_length).encode()) 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', 8000)) # 可参考:https://stackoverflow.com/questions/2444459/python-sock-listen serversocket.listen(10) print('http://127.0.0.1:8000') serversocket.setblocking(0) # 设置socket 为非阻塞模式 try: i = 0 while True: try: conn, address = serversocket.accept() except socket.error as e: if e.args[0] != errno.EAGAIN: raise continue i += 1 print(i) t = threading.Thread(target=handle_connection, args=(conn, address), name='thread-%s' % i) t.start() finally: serversocket.close() if __name__ == '__main__': main() 运行代码python thread_socketserver.py,在浏览器中访问http://127.0.0.1:8000,多开几个浏览器窗口,看看有什么不同。再仔细阅读代码,看看是如何处理的。 接着,还需要改一下代码。在handle_connection 下面有两行注释,需要打开,然后重启进程。再次访问页面,同时打开多个浏览器窗口(不是标签页),看看命令行上的输出。即便有一个任务耗时60 s,也不影响下一个请求进来。看到了吧。 可以思考一下具体实现。这里主要涉及的知识点除了一开始用到过的socket 处理HTTP 协议外,还有下面这些。 □ serversocket.setblocking(0):目的是设置为非阻塞模式。所谓非阻塞,就是当前socket不会在accept 或者recv 时处于阻塞状态(必须等待有连接或者数据过来才执行下一步)。 □ serversocket.accept 外的try...catch:在非阻塞模式下,当没有连接可以被接受时,就会抛出EAGAIN 错误。你可以简单理解为此时没有资源(连接)可以使用,所以会抛出错误来,而这个错误是合理的。 □ 多线程的使用:当我们通过accept 接受连接之后,就开启一个新的线程来处理这个连接,而主程序可以继续在while True 循环中处理其他连接。 当然,这里的threading 也可以使用Python 的multiprocessing 模块来替换,从而使用多进程的方式处理请求。另外,这里的非阻塞其实也不是必需的。你可以通过serversocket.setblocking(1)设置为阻塞模式,然后执行后看看结果是否一致。这里使用非阻塞的例子是希望你对这种模式有一个初步理解,因为在Web 框架中,异步非阻塞是一种很常见的模式。 另外,还有一种Web 模型,每次接受新请求时,就会产生一个子进程来处理,它跟多线程编程的模式类似。具体代码可以参考本书在GitHub 上的代码目录,位置为code/chapter2/section2/fork_socketserver.py。 到此为止,你应该能理解每天访问的网站大概是怎么处理你的访问的了(上面的示例代码只是基本原理)。接下来,我们来看看Python 中所有框架在处理HTTP 请求时需要用到的东西。 2.2.4 简单的WSGI Application 理解了上面的代码之后,我们继续看看WSGI 协议。该协议分为两部分:其中一部分是WebServer 或者Gateway,就像上面的代码一样,它监听在某个端口上接受外部的请求;另一部分是Web Application。Web Server 接受请求之后,会通过WSGI 协议规定的方式把数据传递给WebApplication,在Web Application 中处理完之后,设置对应的状态和header,之后返回body 部分。Web Server 拿到返回数据之后,再进行HTTP 协议的封装,最终返回完整的HTTP Response 数据。 这么说可能比较抽象,下面还是通过代码来演示这个流程。我们先实现一个简单的应用: # 文件位置:/code/chapter2/section2/wsgi_example/app.py # 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 [b'Hello world! -by the5fire \n'] 我们要怎么运行这个应用呢?参照Python PEP 3333 文档上的代码,编写能够运行上面应用程序的CGI 脚本: # 文件位置:/code/chapter2/section2/wsgi_example/gateway.py # coding:utf-8 import os import sys from app import simple_app def wsgi_to_bytes(s): return s.encode() def run_with_cgi(application): environ = dict(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: # 在输出第一行数据之前,先发送响应头 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: # 如果已经发送了header,则重新抛出原始异常信息 raise (exc_info[0], exc_info[1], exc_info[2]) finally: exc_info = None # 避免循环引用 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: # 如果没有body 数据,则不发送header write(data) if not headers_sent: write('') # 如果body 为空,就发送数据header finally: if hasattr(result, 'close'): result.close() if __name__ == '__main__': run_with_cgi(simple_app) 运行脚本python gateway.py,在命令行上能够看到对应的输出: Status: 200 OK Content-type: text/plain Hello world! -by the5fire 对比一开始通过socket 写的Web Server,这就是一个最基本的HTTP 响应了。只是现在是直接通过gateway.py 脚本来调用的。如果输出给浏览器,浏览器会展示出Hello world! -bythe5fire 字样。我们再通过另外一种方式来运行应用,此时用到的工具就是Gunicorn。你可以先通过命令pip install gunicorn 进行安装。 安装完成之后,进入app.py 脚本的目录。通过命令gunicorn app:simle_app 来启动程序。这里的Gunicorn 就是一个Web Server。启动之后,会看到如下输出: [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,就能看到对应的页面了。 2.2.5 理解WSGI 通过上面的代码,你应该看到简单的Application 中对WSGI 协议的实现。你可以在simple_app 方法中增加print 语句来查看参数分别是什么。虽然gateway.py 的代码看起来有点麻烦,但是你只需要关注一点,那就是result = application(environ, start_response)这行代码。我们要实现的Application,只需要能够接收一个环境变量以及一个回调函数即可。当我们处理完请求之后,通过回调函数(start_response)来设置response 的状态和header,最后返回最终结果,也就是body。 WSGI 协议规定,application 必须是一个可调用对象,这意味这个对象既可以是Python中的一个函数,也可以是一个实现了 __call__ 方法的类的实例。比如这个: # 文件位置:/code/chapter2/section2/wsgi_example/app.py 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 [b'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 只需要返回一个可迭代的对象就行,那么我们可以用下面这种方式达到同样的效果: 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 b'Hello AppClassIter\n' 这里我们再次使用Gunicorn 来启动:gunicorn app:AppClassIter。然后,在浏览器中访问http://127.0.0.1:8000,看看结果。 这里的启动命令并不是一个类的实例,而是类本身,为什么呢?通过上面两个代码,我们可以观察到能够被调用的方法会传environ 和start_response 过来,而现在这个实现没有可调用的方式,所以就需要在实例化的时候通过参数传递进来,这样在返回body 之前,可以先调用start_response 方法。 因此,可以推测出WSGI Server 是如何调用WSGI Application 的。结合前面练习过的socket编程,大概代码如下: 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) 大概如此。其实上面的gateway.py 做的也是差不多的逻辑。 2.2.6 WSGI 中间件和Werkzeug 除了交互部分的定义,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') # 这个函数里面调用start_response return response 这样看起来就更加自然一些了。 因此,就存在Werkzeug 这样的WSGI 工具集,让你能够跟WSGI 协议更加友好地交互。从理论上看,我们可以直接通过WSGI 协议的简单实现(也就是上面的代码)写一个Web 服务。但是有了Werkzeug 之后,我们可以写得更加容易。在很多Web 框架中,都是通过Werkzeug 来处理WSGI 协议的。 2.2.7 参考资料 □ Python CGI:https://www.the5fire.com/python-project6-cgi.html。 □ gunicorn-sync 源码:https://github.com/benoitc/gunicorn/blob/master/gunicorn/workers/sync.py#L176。 □ gunicorn-wsgi 部分代码:https://github.com/benoitc/gunicorn/blob/master/gunicorn/http/wsgi.py#L241。 □ PEP 3333 中文:http://pep-3333-wsgi.readthedocs.io/en/latest/。 □ PEP 3333 英文:https://www.python.org/dev/peps/pep-3333/。 □ Werkzeug 官网:http://werkzeug.pocoo.org/。 □ Werkzeug 中文文档:http://werkzeug-docs-cn.readthedocs.io/zh_CN/latest/。 2.2.8 扩展阅读 □ ASGI 英文文档:https://channels.readthedocs.io/en/latest/asgi.html。 □ ASGI 中文翻译:https://blog.ernest.me/post/asgi-draft-spec-zh。 □ Django SSE:https://www.the5fire.com/message-push-by-server-sent-event.html

>Django企业开发实战

Django企业开发实战
作者: 胡阳
副标题: 高效Python Web框架指南
isbn: 7115506892
书名: Django企业开发实战
页数: 392
定价: 99.00元
出版社: 人民邮电出版社
出版年: 2019-2
装帧: 平装