1 WSGI 架构

wsgi 学习笔记--结合openstack中的 wsgi+webob+routes_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。他有如下要求

  1. 应用程序应该是一个可调用对象,Python 中应该是函数、类、实现了__call__方法的类的实例。一句话,app 必须能调用,函数,类,示例都行。
  2. 这个可调用对象应该接收两个参数,environ 和 start_response
  3. 以上的可调用对象实现,都必须返回一个可迭代对象

就以上面的 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()

wsgi 学习笔记--结合openstack中的 wsgi+webob+routes_wsgi _02

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 信息

字段太多,有需要的时候再找
wsgi 学习笔记--结合openstack中的 wsgi+webob+routes_wsgi _03

  • 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 请求结果如下:
wsgi 学习笔记--结合openstack中的 wsgi+webob+routes_wsgi _04
可以发现只能访问我们定义的 url,其他的资源不存在。

4 总结

了解基本的 wsgi 中的概念,了解协程,webob,router 在 wsgi 中分别是做什么的,一点点进步。