WSGI毕竟是Python社区官方认可的规范,可是这种规范在多线程多进程模式下实现很简单,不太适合单线程异步这种情况。鉴于此Tornado并没有按照这个框架去实现一个WSGI的web框架,可是它却提供了基本的兼容,1.允许Tornado的application对象转变成WSGI application。2. 允许WSGI application在tornado ioloop中执行,只是这一切都不是完美的,它仅仅提供了基本的兼容,效率和可用性得不到保证。因此个人还是感觉很鸡肋的

回顾WSGI

wsgi由PEP333开始,在PEP3333得到增强。分为两部分,一部分为容器,另一部分为应用。应用的最简形式如下
传入environ基础环境字典(必须包含一些值)和回调函数,最终返回一个可迭代对线

1
2
3
4
5
def simple_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return ['Hello world!\n']

对于容器。则是每接收到一个http请求则调用一次应用。这样的好处是容器和应用可以完全隔离,应用可以使用任意容器来执行

Tornado application转变成WSGI application

实现效果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import tornado.web
import tornado.wsgi
import wsgiref.simple_server


class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")


if __name__ == "__main__":
application = tornado.wsgi.WSGIApplication([
(r"/", MainHandler),
])
server = wsgiref.simple_server.make_server('', 8888, application)
server.serve_forever()

在Tornado里面application接受一个参数调用,传入Requests对象。调用的结果是最终生成header以及handler._write_buffer对象(会触发iostream的写事件,最终由IOLoop触发socket写操作)。因此虽然它执行了RequestsHandler._execute操作,实际并没有发生socket发送操作,我们可以从handler对象取得完整的HTTP回复报文。然后调用start_response和返回一个可迭代对象
可以看到重点就是从WSGI server传递的environ字典里面重组出一个Request对象(模拟到和httpserver模块中的HTTPRequest对象一样)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class WSGIApplication(web.Application):
def __init__(self, handlers=None, default_host="", **settings):
web.Application.__init__(self, handlers, default_host, transforms=[],
wsgi=True, **settings)

def __call__(self, environ, start_response):
handler = web.Application.__call__(self, HTTPRequest(environ))
assert handler._finished
status = str(handler._status_code) + " " + \
httplib.responses[handler._status_code]
headers = handler._headers.items()
for cookie_dict in getattr(handler, "_new_cookies", []):
for cookie in cookie_dict.values():
headers.append(("Set-Cookie", cookie.OutputString(None)))
start_response(status, headers)
return handler._write_buffer

class HTTPRequest(object):
pass

可以看到重点就是从environ里面构建出一个兼容httpserver的HTTPRequest对象。另外由于IOLoop在这种情况下不允许使用,所以依赖IOLoop的异步特性也就不存在了。而且连同多线程的特性也不在存在。所以只是一个玩具罢了吧

在Tornado中运行WSGI application

上面的是将tornado的应用转变成WSGI兼容,然后让WSGI的容器去运行。这一个是反过来,让tornado作为容器去运行WSGI应用。实现这样的效果

1
2
3
4
5
6
7
8
9
10
def simple_app(environ, start_response):
status = "200 OK"
response_headers = [("Content-type", "text/plain")]
start_response(status, response_headers)
return ["Hello world!\n"]

container = tornado.wsgi.WSGIContainer(simple_app)
http_server = tornado.httpserver.HTTPServer(container)
http_server.listen(8888)
tornado.ioloop.IOLoop.instance().start()

tornado.httpserver.HTTPServer会调用函数并传入一个Request对象。刚好和上面的过程相反。将Request对象分割为environstart_response。直接贴1.0.0的原始代码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class WSGIContainer(object):
def __init__(self, wsgi_application):
self.wsgi_application = wsgi_application

def __call__(self, request):
data = {}
response = []
# 将status和headers放入字典中,后续调用则写入body
def start_response(status, response_headers, exc_info=None):
data["status"] = status
data["headers"] = response_headers
return response.append
# wsgi app最终返回可迭代对象作为body
app_response = self.wsgi_application(
WSGIContainer.environ(request), start_response)
response.extend(app_response)
body = "".join(response)
if hasattr(app_response, "close"):
app_response.close()
if not data: raise Exception("WSGI app did not call start_response")
# 组合成整个http报文
status_code = int(data["status"].split()[0])
headers = data["headers"]
header_set = set(k.lower() for (k,v) in headers)
body = escape.utf8(body)
if "content-length" not in header_set:
headers.append(("Content-Length", str(len(body))))
if "content-type" not in header_set:
headers.append(("Content-Type", "text/html; charset=UTF-8"))
if "server" not in header_set:
headers.append(("Server", "TornadoServer/0.1"))

parts = ["HTTP/1.1 " + data["status"] + "\r\n"]
for key, value in headers:
parts.append(escape.utf8(key) + ": " + escape.utf8(value) + "\r\n")
parts.append("\r\n")
parts.append(body)
# 发送完成
request.write("".join(parts))
request.finish()
self._log(status_code, request)

@staticmethod
def environ(request):
hostport = request.host.split(":")
if len(hostport) == 2:
host = hostport[0]
port = int(hostport[1])
else:
host = request.host
port = 443 if request.protocol == "https" else 80
environ = {
"REQUEST_METHOD": request.method,
"SCRIPT_NAME": "",
"PATH_INFO": request.path,
"QUERY_STRING": request.query,
"REMOTE_ADDR": request.remote_ip,
"SERVER_NAME": host,
"SERVER_PORT": port,
"SERVER_PROTOCOL": request.version,
"wsgi.version": (1, 0),
"wsgi.url_scheme": request.protocol,
"wsgi.input": cStringIO.StringIO(escape.utf8(request.body)),
"wsgi.errors": sys.stderr,
"wsgi.multithread": False,
"wsgi.multiprocess": True,
"wsgi.run_once": False,
}
if "Content-Type" in request.headers:
environ["CONTENT_TYPE"] = request.headers["Content-Type"]
if "Content-Length" in request.headers:
environ["CONTENT_LENGTH"] = request.headers["Content-Length"]
for key, value in request.headers.iteritems():
environ["HTTP_" + key.replace("-", "_").upper()] = value
return environ

def _log(self, status_code, request):
if status_code < 400:
log_method = logging.info
elif status_code < 500:
log_method = logging.warning
else:
log_method = logging.error
request_time = 1000.0 * request.request_time()
summary = request.method + " " + request.uri + " (" + \
request.remote_ip + ")"
log_method("%d %s %.2fms", status_code, summary, request_time)

主要是将Requests转变成environ,传入给WSGI应用。最终根据应用的返回重组成一个HTTP回复报文字符串调用发送逻辑,流程完成。