1 WSGI 架构
WSGl(Web Server Gateway Interface)主要规定了服务器端和应用程序间的接口。解决 WSGI Server 与 app 之间的调用接口约定。
浏览器将 HTTP 请求发给 WSGI Server,WSGI Server 将请求解析,将环境信息封装在 environ 中,environ 是个字典,里面是 key, value 对。封装之后将会调用处理请求的函数 App,调用 App 处理请求时会传入两个参数,一个是 environ,另一个是 start_response 函数本身(不是函数调用哦),传入的 environ 参数就会告诉 App 浏览器的请求是什么。App 处理完请求之后返回之前必须要先调用 start_response 函数,这个函数从名字就可以看出在 App 最终返回响应之前调用。start_response 返回的是的头,如 Content-Type,返回的头信息将发送给 WSGI Server,然后 App 将处理的结果返回给 WSGI Server,这部分称为正文,注意这个正文指的不是 HTTP 的正文,只是处理结果的 HTML 文件内容罢了。
WSGI Server 拿到 start_response 返回的头和 App 返回的正文(HTML文档内容)之后封装成 HTTP 报文返回给浏览器。start_response 返回的内容会作为 HTTP 的状态码、报文头信息,App 返回的正文最终被封装成 HTTP 报文的正文。
2 wsgi 架构深入
2.1 最简单的 wsgi 服务–wsgiref
前面讲解了这么多概念,能不能来个直观的例子
#coding=utf-8
# 启动一个 WSGI 服务器
from wsgiref.simple_server import make_server, demo_app
# 一个两参数的函数的函数,小巧完成的 WSGI 的应用程序的实现
ws = make_server("127.0.0.1", 9999, demo_app)
ws.serve_forever()
运行上面的程序启动了一个 wsgi 服务,在浏览器中输入 http://127.0.0.1:9999/ 就可以访问这个服务。
通过这个例子,只需要知道 make_server 起了一个服务,处理请求的函数是 demo_app,通过 url 可以访问该服务,serve_forever 使得该服务一直接受请求。
WSGl 服务器作用
- 监听 HTTP 服务端口(TCPServer,默认端口 80)
- 接收浏览器端的 HTTP 请求并解析封装成 environ 环境数据
- 负责调用应用程序,将 environ 数据和 start_response 方法两个实参传入给 Application
- 将应用程序响应的正文封装成 HTTP 响应报文返回浏览器端
2.2 WSGI APP 应用程序端
上面的 demo_app 就是处理请求的 app。他有如下要求
- 应用程序应该是一个可调用对象,Python 中应该是函数、类、实现了__call__方法的类的实例。一句话,app 必须能调用,函数,类,示例都行。
- 这个可调用对象应该接收两个参数,environ 和 start_response
- 以上的可调用对象实现,都必须返回一个可迭代对象
就以上面的 demo_app 为例,我们来看看他做了什么处理,函数如下:
def demo_app(environ, start_response):
from io import StringIO
stdout = StringIO()
print("Hello world!", file=stdout)
print(file=stdout)
h = sorted(environ.items())
for k, v in h:
print(k, '=', repr(v), file=stdout)
start_response("200 OK", [('Content-Type', 'text/plain; charset=utf-8')])
return [stdout.getvalue().encode("utf-8")]
StringIO 就是在内存中读写 str,print 函数向内存中写数据,getvaue 函数获得写入后的 str。demo_app 的做的事情就是将 “Hello world!” 写入内存,再写入一个空行,再将 environ 中的内容写入内容,最后获得写入后的 str 并返回。
是不是突然觉得很简单,app 这个处理请求的函数也没干啥嘛,改一下这个 app 函数就可以实现自己的 app 函数了
#coding=utf-8
from wsgiref.simple_server import make_server
def app(environ, start_response):
from io import StringIO
stdout = StringIO()
print("Hello world!", file=stdout)
print(file=stdout)
h = sorted(environ.items())
for k, v in h:
if k.startswith("HTTP_"): # 修改1:将HTTP_开头的过滤出来
print(k, '=', repr(v), file=stdout)
start_response("200 OK", [('Content-Type', 'text/plain; charset=utf-8')])
return [stdout.getvalue().encode("utf-8"), b"app~~~~~~~~~~~~~~~~"] # 修改2:自己拼接了一个字符串
ws = make_server("127.0.0.1", 9999, app)
ws.serve_forever()
这样我们就实现了自己的请求处理函数了。
前面说过,app 必须是可调用的,返回的必须是可迭代对象。函数,实现 __call__ 方法的类,可调用的对象都是可调用的,所以 app 的实现有三种方法,下面逐个进行讲解
1)函数实现 app
# coding=utf-8
from wsgiref.simple_server import make_server
def app(environ, start_response):
start_response("200 OK", [('Content-Type', 'text/plain; charset=utf-8')])
return [b'def app~~~~~~~~~']
ws = make_server("127.0.0.1", 9999, app)
ws.serve_forever()
这个例子很简单,不管什么请求,直接返回字符串
2)可调用的类实现 app
# coding=utf-8
from wsgiref.simple_server import make_server
class App:
def __init__(self, environ, start_response):
self.environ = environ
self.start_response = start_response
def __iter__(self):
self.start_response("200 OK", [('Content-Type', 'text/plain; charset=utf-8')])
yield from [b'class App~~~~~~~~~']
ws = make_server("127.0.0.1", 9999, App)
ws.serve_forever()
3)可调用的对象实现 app
# coding=utf-8
from wsgiref.simple_server import make_server
class Application:
def __call__(self, environ, start_response):
start_response("200 OK", [('Content-Type', 'text/plain; charset=utf-8')])
return [b'class Application~~~~~~~~~~']
ws = make_server("127.0.0.1", 9999, Application()) # 这里app传入的是对象
ws.serve_forever()
类实现了 __call__ 方法后对象就可以调用了,所以 app 传入的是一个类的对象。
注意:第 2、第 3 种实现调用时的不同
还可以在 start_response 相应字段中加入自己的字段
# coding=utf-8
from wsgiref.simple_server import make_server
class Application:
def __call__(self, environ, start_response):
start_response("200 OK", [
('Content-Type', 'text/plain; charset=utf-8'),
('X-server', 'Application1')
])
return [b'class Application~~~~~~~~~~']
ws = make_server("127.0.0.1", 9999, Application()) # 这里app传入的是对象
ws.serve_forever()
4)environ 和 start_response
environ 和 start_response 这两个参数名可以是任何合法名,但是一般默认都是这两个名字。
- environ
environ 是包含 Http 请求信息的 dict 字典对象,有如下等字段
名称 | 含义 |
---|---|
REQUEST_METHOD | 请求方法,GET、POST 等 |
PATH_INFO | URL 中的路径部分 |
QUERY_STRING | 查询字符串 |
SERVER_NAME, SERVER_PORT | 服务器名、端口 |
HTTP_HOST | 地址和端口 |
SERVER_PROTOCOL | 协议 |
HTTP_USER_AGENT | UserAgent 信息 |
字段太多,有需要的时候再找
- start_response
它是一个可调用对象,有三个参数,定义如下
start_response(status, response_headers, exc_info=None)
参数名称 | 说明 |
---|---|
status | 状态码和状态描述,例如200 OK |
response-headers | 一个元素为二元组的列表,例如 [('Content-Type', 'text/plain; charset=utf-8')] |
exc_info | 在错误处理的时候使用 |
start response 应该在返回可迭代对象之前调用,因为它返回的是 Response Header。返回的可迭代对象是 Response Body。
2.3 服务器端
服务器程序需要调用符合上述定义的可调用对象 APP,传入 environ、start_response,APP处理后,返回响应头和可迭代对象的正文,由服务器封装返回浏览器端。
小贴士:
访问 url 可以用浏览器,也可以使用 curl 命令
curl -I http://127.0.0.1:9999/
curl -X POST http://127.0.0.1:9999 -d ‘{“x”:2}’
-I 使用 HEAD 方法
-X 指定方法,-d 传入数据
到这里就完成了一个简单的 WEB 程序开发。做个总结:
WSGI WEB 服务器
- 本质上就是一个 TCP 服务器,监听在特定端口上
- 支持 HTTP 协议,能够将 HTTP 请求报文进行解析,能够把响应数据进行 HTTP 协议的报文封装并返回浏览器端。
- 实现了 WSGl 协议,该协议约定了和应用程序之间接口
WSGI APP 应用程序
- 遵从 WSGl 协议·本身是一个可调用对象
- 调用 start_response,返回响应头部
- 返回包含正文的可迭代对象
WSGl 框架库往往可以看做增强的更加复杂的 Application。
问题:启动服务之后可以访问 http://127.0.0.1:9999,那么在浏览器中输入如下url可以得到相应吗?http://127.0.0.1:9999/abc
经尝试,两个 url 返回的结果没有区别,这是为什么呢?
浏览器将请求发送给 WSGI Server,WSGI Server 将请求封装成 environ,和 start_response 一起传给 app 处理。上面的例子,app 不管 url 是什么,返回的都是相同的结果,自然返回的结果也是一样的,如果需要不同的 url 返回不同的结果,需要做 url 和 app 之间的映射。
3 openstack 中的 wsgi(wsgi+webob+routes)
Openstack 中使用了evenlet 库提供的 wsgi 实现,evenlet 是 python 的一个网络编程库(http://eventlet.net/),提供了许多有用的实现,包括协程,wsgi 等,是网络并发编程的利器
3.1 一个简单的 wsgi 服务
如何启动一个 wsgi 的 server,注册 application,并能响应 http 请求?先来看一个很简单的 wsgi 应用:
"""the most simplest server of wsgi """
import webob
import eventlet
from eventlet import wsgi
from webob import Request
def myapp(env, start_response):
status = "200 OK"
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)
return ['Hello, World! Welcome to wsgi\r\n']
wsgi.server(eventlet.listen(('127.0.0.1', 9999)), myapp)
可以看到 wsgi 已经在 9999 端口上建立了,浏览器输入url 或使用 curl 命令就可以响应 “Hello, World! Welcome to wsgi” 了
在以上程序中:方法 def myapp(env, start_response) 就是用户自己的应用,起入参是 wsgi 规定好的,env 为字典,start_response是个回调函数对象。在 application 中调用了这个 start_response(status,response_headers)。
这个简单的 server 就是 wsgi 服务的骨架
3.2 使用协程启动的 wsgi 服务
上文介绍的 wsgi 服务是直接使用 wsgi.server去启动,进一步我们可以利用evenlet 中协程去启动一个 wsgi 服务,在 openstack 的 wsgi.py 的 Server 类中就是使用这种方式。基于以上,我们可以将第一段程序进行改造:
"""useeventlet to start wsgi server"""
import webob
import eventlet
from eventlet import wsgi
from webob import Request
def myapp(env, start_response):
status = "200 OK"
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)
return ['Hello, World!\r\n']
def start():
print("start wsgi server")
wsgi.server(eventlet.listen(('127.0.0.1', 9999)), myapp)
wsgi_server = eventlet.spawn(start)
wsgi_server.wait()
其中 start 函数是要启动的 wsgi 服务,而 evenlet.spawn(start) 正是启动一个协程去调用 start 函数,其返回结果是一个协程对象,这里有个问题需要提一下,如果只执行 wsgi_server =eventlet.spawn(start) 这句其实并没有真正调用 start() 方法,只有最后调用该对象的 wait() 方法后,才能真正执行 start 函数。Openstack 中 server 类中的 wait 方法其实就是调用了协程的 wait 方法。
3.3 将 application 封装为 class 进行调用
为了进一步接近 openstack 中用法,将上文中的 application 函数可以封装为 class 进行调用,代码如下:
"""calla application class"""
import webob
import eventlet
from eventlet import wsgi
from webob import Request
class Application(object):
def __call__(self, env, start_response):
status = "200 OK"
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)
return ['Hello, World!\r\n']
def start():
print("start wsgi server")
app = Application()
wsgi.server(eventlet.listen(('127.0.0.1', 9999)), app)
wsgi_server = eventlet.spawn(start)
wsgi_server.wait()
其中可以看到,wsgi.server 中 app 已经不是方法了,而是类实例,当然这个类要是可调用的,即要实现 __call__ 方法。
3.4 使用 webob 来包装 wsgi 请求和响应
先介绍下webob: WebOb 是一个 Python 库,主要是用在 WSGI 中对请求环境变量 request environment(也就是 WSGI 应用中的参数 environ)进行包装(提供 wrapper),并提供了一个对象来方便的处理返回 response 消息。WebOb 提供的对象映射了大多数的 HTTP 方法,包括头解析,content 协商等。这里的映射,就是说只需要对对象进行操作,就可以完成 HTTP 的方法,从而大大简化开发难度
因此,可以将代码进一步优化:
"""use webob to warpper request"""
import webob
import eventlet
from eventlet import wsgi
from webob import Request
from webob import Response
class Application(object):
def __call__(self, env, start_response):
req = Request(env)
print(req.method)
print(req.body)
print(req.headers)
response = Response(body="hello world!", content_type='text/plain')
return response(env, start_response)
def start():
print("start wsgi server")
app = Application()
wsgi.server(eventlet.listen(('127.0.0.1', 9999)), app)
wsgi_server = eventlet.spawn(start)
wsgi_server.wait()
这里我使用 webob 将 wsgi server 传入的 env 封装为 Webob 中的 Request 对象,并打印了 request 对象中的 method,body,headers 属性。最后用Webob 中的 Response 对象来封装响应。
3.5 使用 Router 建立 app 与 url 映射
Router 是一个 python 库,可以用来实现 url 到 controller 之间的映射,controller 为自定义的处理函数。可以简单理解为建立不同 url 与不同处理函数的映射关系。直接看例子:
#coding=utf-8
import eventlet
from eventlet import wsgi
import routes.middleware
import webob.dec
import webob.exc
from webob import Request
from webob import Response
# 定义两个请求处理类Application1和Application2
class Application1(object):
def __call__(self, env, start_response):
req = Request(env)
response = Response(body="Welcome to wsgi, I'm in Application1.", content_type='text/plain')
return response(env, start_response)
class Application2(object):
def __call__(self, env, start_response):
req = Request(env)
response = Response(body="Welcome to wsgi, I'm in Application2.", content_type='text/plain')
return response(env, start_response)
class Router(object):
def __init__(self):
self._mapper = routes.Mapper() # _mapper是空的
self._mapper.connect('/test1', # 给_mappper插入数据,建立url与controller的映射关系
controller=Application1(),
action='index',
conditions={'method': {'GET'}})
self._mapper.connect('/test2', # 给_mappper插入数据
controller=Application2(),
action='index',
conditions={'method': {'GET'}})
self._router = routes.middleware.RoutesMiddleware(self._dispatch, self._mapper) # 初始化,调用_dispatch方法取controller
@webob.dec.wsgify
def __call__(self, req):
return self._router # 调用
@staticmethod
@webob.dec.wsgify
def _dispatch(req):
match = req.environ['wsgiorg.routing_args'][1]
if not match:
print("match is empty")
return webob.exc.HTTPNotFound()
return match['controller'] # return执行Application
def start():
print("start wsgi server")
myapp = Router()
wsgi.server(eventlet.listen(('127.0.0.1', 9999)), myapp)
wsgi_server = eventlet.spawn(start)
wsgi_server.wait()
我们使用协程调用 start 函数启动了服务,start 中调用 Router 类,该类中实现了 __call__ 方法,于是调用 Router 中的 __call__ 方法,__call__ 方法返回 _router,_router 调用 _dispatch 方法取 controller,到对应的 app 中处理。
使用 curl 请求结果如下:
可以发现只能访问我们定义的 url,其他的资源不存在。
4 总结
了解基本的 wsgi 中的概念,了解协程,webob,router 在 wsgi 中分别是做什么的,一点点进步。