Python WSGI
一、什么是WSGI
WSGI: Web Server Gateway Interface,即Web服务网关接口。WSGI不是Python的包、模块、类等,它是 Web application/Web Framework 和 Web Server 之间进行通信的接口规范, 如Web框架Django 和 UWSGI 之间的关系。WSGI 的存在是为了让Web Server 和 application或者Web 框架更加通用,而不是 为了使用一个特殊的Web框架还需要使用一个特殊的Web Server。
二、 WSGI 规范概述
WSGI 接口 分为两个部分: server/gateway和application/framework。 server 端调用由 application端提供的可调用对象。 如何提供对象的具体细节由server或gateway决定。 某些server或gateway要求应用程序的部署者编写一个短脚本来创建服务器或网关的实例,并向其提供应用程序对象。 另一写server和gateway可以使用配置文件或其他机制来指定应从哪个应用程序对象导入或以其他方式获取的位置。
2.1 Application/Framework 端实现
WSGI规定,WSGI 应用程序接口是一个可调用对象:一个函数、一个方法、一个类或者是带有object.__call__()方法的实例。Application 对象必须能够多次调用,因为几乎所有的服务器/网关(除了CGI)都会发出这样的重复请求。调用应用程序接口必须满足两个条件:
-
1. 接收两个位置参数
- 包含像变量的CGI的字典(即环境变量,这个环境变量是根据request 信息生成的)
- 一个回调函数: start_response。应用程序将HTTP 响应的状态码和headers返回给 server端
- 2. application 调用对象需要将 响应body返回到服务器。返回值是一个可迭代的字符串。
注意:尽管我们将其称为“应用程序”对象,但这并不意味着应用程序开发人员将使用WSGI作为web编程API!假定应用程序开发人员将继续使用现有的高级框架服务来开发他们的应用程序。WSGI是框架和服务器开发人员的工具,并不是直接支持应用程序开发人员的工具。) 简单来说,如果只是一个使用框架和web 服务的人,是不会使用到WSGI的;WSGI 是开发Web 框架 和 Web server的人遵循的标准。
根据WSGI规范,我们的 application/framework骨架如下(命名为app.py):
def application(environ, start_response):
response_body = 'Request method: %s' % environ['REQUEST_METHOD']
status = '200 OK'
response_headers = [
('Content-Type', 'text/plain'),
('Content-Length', str(len(response_body)))
]
start_response(status, response_headers)
return [response_body]
2.2 Server/Gateway 端实现
现在看server端的实现。Application端的可调用对象接收的环境参数和回调函数start_response都是在server端实现的。因此我们需要我们:
- 定义一个start_response(self, status, response_headers, exc_info=None)函数。
- 定义一个函数来生成env这个环境变量字典。
那么我们在server端 调用应用程序的方法如下:
body = application(env, start_response)
Apllication 一端会返回一个列表并赋值给body变量。
下面是实现了一个简单的遵循 WSGI 协议的Web Server。
- server.py
#!/usr/bin/env python
# coding: utf-8
import socket
import StringIO
import sys
class WSGIServer(object):
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
request_queue_size = 1
def __init__(self, server_address):
# 创建socket
self.listen_socket = listen_socket = socket.socket(
self.address_family,
self.socket_type
)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(server_address)
listen_socket.listen(self.request_queue_size)
# 获取服务器主机名和端口
host, port = self.listen_socket.getsockname()[:2]
self.server_name = socket.getfqdn(host)
self.server_port = port
self.headers_set = []
def set_app(self, application):
'''设置应用程序的可调用对象
'''
self.application = application
def server_forever(self):
'''接收客户端请求
'''
listen_socket = self.listen_socket
while True:
self.client_connection, client_address = listen_socket.accept()
self.handle_one_request()
def handle_one_request(self):
'''处理请求
'''
# "GET /hello HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: 118.89.28.185:8888\r\nAccept: */*\r\n\r\n"
self.request_data = request_data = self.client_connection.recv(1024)
print 'Reuest:'
print(''.join(
'< {line}\n'.format(line=line)
for line in request_data.splitlines()
))
self.parse_request(request_data)
env = self.get_environ()
# 调用 应用程序的可调用对象
body = self.application(env, self.start_response)
self.finish_response(body)
def parse_request(self, text):
'''解析HTTP 请求,获取头部信息
'''
# 获取HTTP头部: 'GET /hello HTTP/1.1'
request_line = text.splitlines()[0]
request_line = request_line.rstrip('\r\n')
(self.request_method,
self.path,
self.request_version
) = request_line.split()
def get_environ(self):
'''使用HTTP Request 构造 env字典
字典内容遵循 WSGI规范(PEP333)
'''
env = {}
env['wsgi.version'] = (1, 0)
env['wsgi.url_scheme'] = 'http'
env['wsgi.input'] = StringIO.StringIO(self.request_data)
env['wsgi.errors'] = sys.stderr
env['wsgi.multithread'] = False
env['wsgi.multiprocess'] = False
env['wsgi.run_code'] = False
env['REQUEST_METHOD'] = self.request_method
env['PATH_INFO'] = self.path
env['SERVER_NAME'] = self.server_name
env['SERVER_PORT'] = str(self.server_port)
return env
def start_response(self, status, response_headers, exc_info=None):
'''服务端的回调函数,用于获取应用程序响应的状态码和响应头
'''
server_headers = [
('DATE', 'Tue, 31 Mar 2015 12:54:48 GMT'),
('Server', 'WSGIServer 0.2'),
]
self.headers_set = [status, response_headers + server_headers]
def finish_response(self, body):
'''响应客户端。这里已经和WSGI协议无关了。
'''
try:
status, response_headers = self.headers_set
response = 'HTTP/1.1 {status}\r\n'.format(status=status)
for header in response_headers:
response += '{0}: {1}\r\n'.format(*header)
response += '\r\nCotent: {body}\r\n'.format(body=body[0])
print 'Response:'
print(''.join(
'> {line}\n'.format(line=line)
for line in response.splitlines()
))
self.client_connection.sendall(response)
finally:
self.client_connection.close()
SERVER_ADDRESS = (HOST, PORT) = '', 8888
def make_server(server_address, application):
server = WSGIServer(server_address)
server.set_app(application)
return server
if __name__ == '__main__':
if len(sys.argv) < 2:
sys.exit('Provide a WSGI application object as module:callable')
# 通过命令行参数获取 应用程序的可调用对象
app_path = sys.argv[1]
module, application = app_path.split(':')
module = __import__(module)
application = getattr(module, application)
httpd = make_server(SERVER_ADDRESS, application)
print('WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT))
httpd.server_forever()
2.3 测试
上述的server 端代码 和 应用程序代码现在是可以工作的,测试如下(注意,server运行后面的结果,是使用curl测试后才出来的,并不是一开始就产生了的。):
[root@devops2 webserver]# python server.py app:application
WSGIServer: Serving HTTP on port 8888 ...
Reuest:
< GET / HTTP/1.1
< User-Agent: curl/7.29.0
< Host: 118.89.28.185:8888
< Accept: */*
<
Response:
> HTTP/1.1 200 OK
> Content-Type: text/plain
> Content-Length: 19
> DATE: Tue, 31 Mar 2015 12:54:48 GMT
> Server: WSGIServer 0.2
>
> Cotent: Request method: GET
[root@devops2 ~]# curl -v http://118.89.28.185:8888
* About to connect() to 118.89.28.185 port 8888 (#0)
* Trying 118.89.28.185...
* Connected to 118.89.28.185 (118.89.28.185) port 8888 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 118.89.28.185:8888
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 19
< DATE: Tue, 31 Mar 2015 12:54:48 GMT
< Server: WSGIServer 0.2
<
* Excess found in a non pipelined read: excess = 10, size = 19, maxdownload = 19, bytecount = 0
* Connection #0 to host 118.89.28.185 left intact
2.4 工作原理
上述代码的工作原理如下:
- app.py 程序(也可以使用一个Web矿建,如Flask) 提供一个可调用对象 application
- server 从客户端接收请求后,解析HTTP请求,创建一个名为env的字典
- 调用 application。它会想可调用对象传递一个名为 env的字典作为参数,其包含了 WSGI/CGI的诸多变量; 以及一个名为start_response的可调用对象。
- app.py 中的application可调用对象生成HTTP 状态码和HTTP Header,然后调用 start_response将其传给server端,等待服务器保存。然后,application可调用对象还会return HTTP 响应正文。
- server 将状态码、头部、body返回给客户端。
.....待续