David Wheeler有一句名言:“计算机科学中的任何问题,都可以通过加上另一层间接的中间层解决。”
为了提高Python网络服务的可移植性,Python社区在PEP 333中提出了Web服务器网关接口(WSGI,Web Server Gateway Interface)。
WSGL标准就是添加了一层中间层。通过这一个中间层,用Python编写的HTTP服务就能够与任何Web服务器进行交互了。现在,WSGI已经成为了使用Python进行HTTP操作的标准方法。
按照标准的定义,WSGI应用程序是可以被调用的,并且有两个输入参数。
1、WSGI
下面是第一段代码,第一个参数是environ,用于接收一个字典,字典中提供的键值对是旧式的CGI环境集合的拓展。第二个参数本身也是可以被调用的,习惯上会将其命名为start_response(),WSGI应用程序通过这个参数来声明响应头信息。
- # 用WSGI应用形式编写的简单HTTP服务。
- #!/usr/bin/env python3
- # A simple HTTP service built directly against the low-level WSGI spec.
- from pprint import pformat
- from wsgiref.simple_server import make_server
- def app(environ, start_response):
- headers = {'Content-Type': 'text/plain; charset=utf-8'}
- start_response('200 OK', list(headers.items()))
- yield 'Here is the WSGI environment:
- '.encode('utf-8')
- yield pformat(environ).encode('utf-8')
- if __name__ == '__main__':
- httpd = make_server('', 8000, app)
- host, port = httpd.socket.getsockname()
- print('Serving on', host, 'port', port)
- httpd.serve_forever()
上述只是一个简单的情况。但是在编写服务器程序时,复杂度就大大提升了。这是因为要完全考虑标准中的描述的许多注意点和边界情况。
2、前向代理与反向代理
无论前向代理还是反向代理,HTTP代理其实就是一个HTTP服务器,用于接收请求,然后对接收到的请求(至少是部分请求)进行转发。转发请求时代理会扮演客户端的角色,将转发的HTTP请求发送至真正的服务器,最后将从服务器接受到的响应发挥扮演客户端的角色,将转发的请求发送至真正的服务器,最后将从服务器接受到的响应发回给最初的客户端。
下面是前向代理和反向代理的简图。
反向代理已经广泛应用于大型的HTTP服务当中。反向代理是Web服务的一部分,对于HTTP客户端并不可见。
3、四种架构
架构师一般都使用很多种复杂的机制来将多个子模块组合建成一个HTTP服务。现在在Python社区中,已经形成了4种基本的模式。如果已经编写了用于生成动态内容的Python代码,并且已经选择了某个支持WSGI的API或框架,应该如何将HTTP服务部署到线上呢?
- 运行一个使用Python编写的服务器,服务器的代码中可以直接调用WSGI接口。现在流行的是Green Unicorn(Gunicorn)服务器,不过也有其他已经可以用于生产环境的纯Python服务器。
- 配置mod_wsgi并运行Apache,在一个独立的WSFIDaemonProcess中运行Python代码,由mod_wsgi启动守护进程。
- 在后端运行一个类似于Gunicorn的Python HTTP服务器(或者支持所选异步框架的任何服务器),然后在前端运行一个既能返回静态文件,又能对Python编写的动态资源服务进行反向代理的Web服务器。
- 在最前端运行一个纯粹的反向代理(如Varnish),在该反向代理后端运行Apache或者nginx,在后端运行Python编写的HTTP服务器。这是一个三层的架构。这些反向代理可以分布在不同的地理位置,这样子就能够将离客户端最近的反向代理上的缓存资源返回给发送请求的客户端。
长期以来,对这4个架构的选择主要基于CPython的3个运行时的特性,即解释器占用内存大、解释器运行慢、全局解释器(GIL,Global Interpreter Lock)禁止多个线程同时运行Python字节码。但同时带来了内存中只能载入一定数量的Python实例。
4、平台即服务
这个概念的出现是因为现在的自动化部署、持续集成以及高性能大规模服务的相关技术的出现和处理有一些繁杂。所以有一些提供商提出了PaaS(Platform as a Service),现在只需关心应该如何打包自己的应用程序,以便将自己的应用部署到这些服务之上。
PaaS提供商会解决构建和运行HTTP服务中的出现的各种烦心事。不需要再关心服务器,或者是提供IP地址之类的事情。
PaaS会根据客户规模提供负载均衡器。只需要给PaaS提供商提供配置文件即可完成各种复杂的步骤。
现阶段比较常用的有Heroku和Docker。
大多数PaaS提供商不支持静态内容,除非我们在Python应用程序中实现了对静态内容的更多支持或者向容器中加入了Apache或ngnix。尽管我们可以将静态资源和动态页面的路径放在两个完全不同的URL空间内,但是许多架构师还是倾向于将两者放在同一个名字空间内。
5、不使用Web框架编写WSGI可调用对象
下面第一段代码是用于返回当前时间的原始WSGI可调用对象。
- #!/usr/bin/env python3
- # A simple HTTP service built directly against the low-level WSGI spec.
- import time
- def app(environ, start_response):
- host = environ.get('HTTP_HOST', '127.0.0.1')
- path = environ.get('PATH_INFO', '/')
- if ':' in host:
- host, port = host.split(':', 1)
- if '?' in path:
- path, query = path.split('?', 1)
- headers = [('Content-Type', 'text/plain; charset=utf-8')]
- if environ['REQUEST_METHOD'] != 'GET':
- start_response('501 Not Implemented', headers)
- yield b'501 Not Implemented'
- elif host != '127.0.0.1' or path != '/':
- start_response('404 Not Found', headers)
- yield b'404 Not Found'
- else:
- start_response('200 OK', headers)
- yield time.ctime().encode('ascii')
第一段比较冗长。下面使用第三方库简化原始WGSI的模式方法。
第一个示例是使用WebOb编写的可调用对象返回当前时间。
- #!/usr/bin/env python3
- # A WSGI callable built using webob.
- import time, webob
- def app(environ, start_response):
- request = webob.Request(environ)
- if environ['REQUEST_METHOD'] != 'GET':
- response = webob.Response('501 Not Implemented', status=501)
- elif request.domain != '127.0.0.1' or request.path != '/':
- response = webob.Response('404 Not Found', status=404)
- else:
- response = webob.Response(time.ctime())
- return response(environ, start_response)
- 第二个是使用Werkzeug编写的WSGI可调用对象返回当前时间。
第二个是使用Werkzeug编写的WSGI可调用对象返回当前时间。
- #!/usr/bin/env python3
- # A WSGI callable built using Werkzeug.
- import time
- from werkzeug.wrappers import Request, Response
- @Request.application
- def app(request):
- host = request.host
- if ':' in host:
- host, port = host.split(':', 1)
- if request.method != 'GET':
- return Response('501 Not Implemented', status=501)
- elif host != '127.0.0.1' or request.path != '/':
- return Response('404 Not Found', status=404)
- else:
- return Response(time.ctime())
大家可以对比这两个库在简化操作时的不同之处,Werkzeug是Flask框架的基础。