python后端开发必会WSGI

WSGI是一个约定,一份协议,并不是什么高深的技术,而且它的约定十分简单,使得服务器程序和应用程序可以非常轻便的结合在一起,任何服务器,任何web应用程序,只要他们遵守了这份约定,就可以一起在服务器上部署,提供服务

背景

python后端开发,服务端程序分为两部分,一个是服务器程序,一个是应用程序,我们用flask,django,tornado框架写出来的都是应用程序,这些应用程序必须通过服务器部署才能被访问使用。

这里说的服务器程序,可不是你理解的nginx,tomcat,他们是无法直接部署我们写的python web 应用程序的。

服务器负责收集用户的请求,初步处理后传递给我们的应用程序,应用程序负责具体的业务逻辑处理,为了开发的便利性,人们把常用的功能封装在一起,这就形成了各种web 框架,例如flask, django。

还有那么一批人,出于对技术的热爱,实现了不同的服务器,比如tornado(既是服务器也是web框架),wsgiref.simple_server(官方实现),gevent中的pywsgi.WSGIServer,gunicorn中的Application等等,他们的底层实现各不相同,为了追求性能,有的服务器用epoll网络模型,有的用协程,还有的用多进程,总之百花齐放。

那么多的web框架,那么多的服务器,他们怎么配合工作呢?这就是WSGI的由来。

WSGI是什么

The Web Server Gateway Interface, 即 WSGI,WSGI是一个协议,是一份约定,它规定服务器和应用程序之间如何传递数据,各自应该实现什么样的接口,以便彼此间配合工作。

WSGI对应用程序约定

WSGI的约定不能太复杂了,因为这是服务器和应用程序要共同遵守的约定,弄的太复杂了,两边的人开发起来都困难,所以,对于应用程序段,它只规定了3个简单的要求

  1. 应用程序是一个可调用对象(callable)
  2. 可调用对象接受两个参数,分别是environ和start_response
  3. 可调用对象需要返回一个可迭代对象

下面,对这3点逐一进行解释

应用程序是一个可调用对象

函数,类,实现了__call__方法的类的对象,都是可调用对象,俗称可callable对象。

def test():
    pass


class Test:
    pass


print(callable(test), callable(Test))

print 输出两个True,所谓可调用对象,就是对象后面可以加小括号的对象,这是最简单粗暴的理解了。使用flask框架时,我们通常这样写

app = Flask(__name__)

app就是一个可调用对象,Flask类实现了__call__方法,当我们执行下面的代码

app.run(debug=True)

其实就是在执行Flask类里的__call__方法

可调用对象接受两个参数,分别是environ和start_response

之所以强调,应用程序是一个可调用对象,是因为服务器在部署我们写的web程序时,本质上就是在调用这个对象,因此它必须是可调用的,只有可调用,服务器才能把environ和start_response传给我们的应用程序,那么这两个参数都用什么用呢?

environ

environ可不简单,它是一个字典,包含了一次请求的几乎所有信息,下面是一个内容示例

{
    'SERVER_PROTOCOL': 'HTTP/1.1',
    'SERVER_SOFTWARE': 'WSGIServer/0.2',
    'REQUEST_METHOD': 'GET',
    'PATH_INFO': '/book/python',
    'QUERY_STRING': 'name=flask',
    'REMOTE_ADDR': '127.0.0.1',
    'CONTENT_TYPE': 'text/plain',
    'HTTP_HOST': 'localhost: 8800',
    'HTTP_CONNECTION': 'keep-alive',
    'HTTP_CACHE_CONTROL': 'max-age=0',
    'HTTP_UPGRADE_INSECURE_REQUESTS': '1',
    'HTTP_USER_AGENT': 'Mozilla/5.0(Macintosh;IntelMacOSX10_14_2)AppleWebKit/537.36(KHTML,
    likeGecko)Chrome/75.0.3770.142Safari/537.36',
    'HTTP_ACCEPT': 'text/html,
    application/xhtml+xml,
    application/xml;q=0.9,
    image/webp,
    image/apng,
    */*;q=0.8,
    application/signed-exchange;v=b3',
    'HTTP_ACCEPT_ENCODING': 'gzip,
    deflate,
    br',
    'HTTP_ACCEPT_LANGUAGE': 'zh-CN,
    zh;q=0.9',
    'HTTP_COOKIE': '_ga=GA1.1.1246835462.1564651565;token=uQITFBiKxFOfLWDHZxHQbdIDJVJLGRdR;email=admin;session_uuid=2c563845-4f93-4832-9e3d-4cd14c179fd2;wordpress_test_cookie=WP+Cookie+check;wordpress_logged_in_70490311fe7c84acda8886406a6d884b=kwsy%7C1568971347%7C8F3nK8fRBa6x2ePaFjvSYrmB3QMGm3I32Gyh3KGJp2t%7C322f03c8e5e825a25db050d60ae1e6430ee5417b95530fb3a10afdf658cbcd46;wp-settings-1=mfold%3Do;wp-settings-time-1=1567761754'

实际的内容比这要多出很多,为了突出重点,减少文章篇幅,我删除了一部分,剩下的这些,如果你对http协议有所了解的,很容易就能看出environ的作用。为了得到这份environ,我写了一个简单的web 程序

from wsgiref.simple_server import make_server

def handle_request(env, start_response):
    print(env)
    start_response("200 OK",[("Content-Type","text/html")])
    body = "<h1>Hello World!</h1>"
    return [body.encode("utf-8")]


if __name__ == "__main__":
    httpd = make_server("",8800,handle_request)
    print("Serving http on port 8800")
    httpd.serve_forever()

启动程序后,在浏览器里输入http://localhost:8800/book/python?name=flask, 即可得到上述内容。现在,我们的应用程序已经过了本次请求的全部信息,包括本次请求的资源地址(PATH_INFO),请求的方法(REQUEST_METHOD),请求的参数(QUERY_STRING),客户端的IP地址(REMOTE_ADDR),请求的cookie(HTTP_COOKIE)。

start_response

start_response是一个方法,以上面的代码为例,其方法定义和实现为

    def start_response(self, status, headers,exc_info=None):
        """'start_response()' callable as specified by PEP 3333"""

        if exc_info:
            try:
                if self.headers_sent:
                    # Re-raise original exception if headers sent
                    raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
            finally:
                exc_info = None        # avoid dangling circular ref
        elif self.headers is not None:
            raise AssertionError("Headers already set!")

        self.status = status
        self.headers = self.headers_class(headers)

它最重要的功能是设置response的http code和headers。熟悉http协议的人都知道,服务器处理完请求后,要将数据发送给客户端,这个就教做response,根据http协议,返回的response要标识http状态码,还要设置headers,你接触过的任何一个框架都会这样做处理的,只是这些框架封装的很好,不需要你自己去做处理而已。

如何接收environ和start_response

要明确一点,environ和start_response是服务器传给我们的,前者是一个字典,存储了请求的信息,后者是一个方法,用于我们在应用程序里设置响应头,那么如何接收这两个参数呢,前面已经说过,应用程序是一个可调用对象,它可以是函数,也可以是类,也可以是一个类的对象,那么方法就有三种

先说第一种,我们的应用程序提供的可调用对象是一个函数,定义如下

def application(environ, start_response):
    pass

application(environ, start_response)  # 调用

第二种,应用程序提供的可调用对象是一个类,定义如下

class Application:
    def __init__(self, environ, start_response):
        pass

Application(environ, start_response) # 调用

第三种,应用程序提供的可调用对象是类的对象,该类实现了__call__方法

class Application:
    def __call__(self, environ, start_response):
        pass
        
app = Application()
app(environ, start_response)  # 调用

可调用对象需要返回一个可迭代对象

为啥非得返回一个可迭代对象呢?在上面的例子中,我返回的是一个列表,列表是可迭代对象,为啥不可以直接返回一个字符串呢?

这样做,是为了更好的提供web服务,比如服务端要返回给客户端一个2个G的文件数据,如果应用程序是一次性加载到内存中进行返回,那么就会占用非常多的内存,要知道,这可是web服务啊,吃不消的。如果可调用对象返回的是可迭代对象,情形就不一样了,同样是返回这个文件,我们可以使用生成器,一次只生成一小块固定大小的文件数据,这样占用内存就很小,我们可能会写一个生成器,类似下面的代码

def send_file(file_path, BLOCK_SIZE=1000):
    with open(file_path) as f:
        block = f.read(BLOCK_SIZE)
        while block:
            yield block
            block = f.read(BLOCK_SIZE)

我们的可调用对象,则可以这样来处理

def application(environ, start_response):
    size = os.path.getsize('big_file')
    headers = [
        ("Content-length", str(size)),
    ]
    start_response("200 OK", headers)

    return send_file('big_file')

在服务器端,则是这样来处理我们返回的可迭代数据的

    def finish_response(self):
        try:
            if not self.result_is_file() or not self.sendfile():
                for data in self.result:
                    self.write(data)
                self.finish_content()
        finally:
            self.close()

遍历self.result,这就是我们返回的结果。

WSGI对服务器约定

对服务器的约定,就更为简单了,它只要求服务器提供一个start_response方法,当请求到来时,将environ整理好一并传给应用程序,还是以官方实现的wsgiref.simple_server为例,其调用应用程序的过程如下,代码有删减

def run(self, application):
    self.setup_environ()
    self.result = application(self.environ, self.start_response)
    self.finish_response()

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案