「WSGI」WSGI概述

转载请注明出处: https://blog.csdn.net/jinixin/article/details/84677104

WSGI是什么? 为什么涉及部署时就会提到这个概念? WSGI的作用是什么? 没了它, 服务是不是照样没问题? 每当编写Python Web项目的时候, 这些问题就开始困扰我. 经过一番阅读和讨论后, 想在此简单谈谈.

CGI

在谈WSGI前, 我们需要先了解下CGI是什么.

互联网初步开放于上世纪90年代, 一开始主要用于浏览资料之类的, 而对Web服务器的要求, 也就是接收用户请求, 并返回(响应)存储在其上的静态文件. 在此过程中, Web服务器主要负责编解HTTP请求. 但随着互联网深入人们的生活, 出现了表单(form)这种东西, 就开始要求Web服务器能接收用户提交的数据, 在进行一些处理后, 再将生成的内容返给用户, 这就是动态页面技术.

显然进行一些处理的操作并不适合集成在Web服务器里, 试想下A公司需接收购物表单, B公司要接收注册表单, 两者肯定是不同的处理逻辑, 不可能都去修改Web服务器代码, 所以处理的工作就由Web服务器交给扩展程序去做. 因此整个流程就变为, 用户(浏览器)向Web服务器发起请求, 扩展程序接收Web服务器解析好的HTTP请求, 做一些业务处理后, 再将生成内容返给Web服务器, 最后由Web服务器按HTTP协议编码后回传给用户(浏览器), 而这个扩展程序一般被称为应用服务.

Web服务器一般由C编写, 应用服务则由更高级的C++/PHP/Python/GO来编写, Web服务器与应用服务的交互规则就被称为通用网关接口, 即CGI(Common Gateway Interface). 更进一步说, CGI协议描述了Web服务器如何将解析后的请求头传递给应用服务, 以及应用服务如何从标准输入中读取请求内容, 如何将响应内容通过标准输出回传给Web服务器.

CGI的出现使应用服务与Web服务器间的交互成为可能, 但CGI要求对每个Web请求单独创建一个应用服务进程. 因此面对大量请求, 大量进程的创建和消亡会使操作系统性能大大降低. 此外, 由于地址空间无法共享, 这也限制了资源重用, 所以CGI因效率问题已逐渐被淘汰.

虽然CGI已逐渐不被使用, 但他的出现为之后Web服务器与应用服务间的交互奠定了良好基础, 下面即将提到的WSGI就是对CGI的良好继承. 此外, CGI也出现了改良版FastCGI(快速通用网关接口), FastCGI不再为每个请求都创建新的应用服务进程, 而是使用持续的进程来处理一连串请求. 当进来一个请求时, Web服务器会把解析后的请求通过socket(当应用服务位于本地时)或TCP连接(当应用服务位于远端时)传递给应用服务进程.

WSGI的定义

有了上面对CGI的介绍, WSGI就很好理解了. WSGI是类似CGI的概念, 但其只用于Python, 即Python的Web服务器网关接口(Python Web Server Gateway Interface). WSGI作为一种接口协议, 连接Web服务器和Python应用服务这两部分, 使得两者解耦, 互不影响, 也使得Python应用服务可以平滑在不同种类的Web服务器间迁移.

你可能会有疑问: 你说的Web服务器和Python应用服务具体是指什么? 

你可以先认为Web服务器就是能解析或代理HTTP请求的程序, 常见有Nginx, Apache等; 应用服务就是我们用Flask, Tornado编写的Web应用.

WSGI在PEP-0333中被首次提出, 之后在PEP-3333中增加了对Python3的支持.

WSGI是一个同步协议, 其主要规定了两个角色: WSGI服务端(server/gateway), WSGI应用端(application/framework). 而这两部分应该如何实现呢? 下面马上介绍.

WSGI的实现

通过上面我们知道, 支持WSGI协议, 就需要实现WSGI服务端和WSGI应用端两部分.

WSGI应用端需实现一个可调用对象application, 该对象接收两个参数environ和start_response, 并返回一个可迭代对象. 每当WSGI服务端接收到请求后, 就会调用WSGI应用端的application, 并传入environ和start_response, 即调用application(environ, start_response). application由WSGI应用端定义, environ与start_response由WSGI服务端定义.

1. 参数介绍

environ是一个Python字典, 表示解析后的请求头, 其由WSGI服务端定义并传给WSGI应用端的application.

start_response是一个可调用对象, 表示开启响应, 向Web服务器发送响应头. 其由WSGI服务端定义并传给WSGI应用端的application, start_response按序接收三个参数: status(必要参数, 表示响应状态), response_headers(必要参数, 表示响应头), exec_info(可选参数, 表示响应的异常信息), 这些参数均由WSGI应用端传入:

  • status是字符串, 内容为"响应状态码 响应状态信息", 比如"200 OK"或"404 NOT FOUND";
  • response_headers是由(header_key, header_val)组成的列表, WSGI服务端可以修改, 如果响应头里有缺省的key, WSGI服务端会补充;
  • exec_info是sys.exc_info的返回值, 表示WSGI应用端捕获并打算回传给WSGI服务端的错误.

实现application时, 需保证其向WSGI服务端返回第一块响应内容前会调用start_response, 以使得响应头提前于响应内容被发送. 实际上start_response在被调用后, 不会立即向Web服务器发送响应头, 相反其会先将响应头存储起来, 直到application被迭代过一次后, 即准备向用户返回响应内容了, 响应头才会被发出, 此规则唯一例外是响应头中明确指定Content-Length为0. 有必要解释下, 延迟传输响应头是为了到最后一刻都可以用异常输出替换原来的预期输出. 例如, 在缓冲区生成响应正文时发生错误, 则响应状态需从"200 OK"改为"500 Internal Error". 如果响应头还没发出去, 那直接返回500即可, 但如果已经发送了响应头, start_response就必须立即报错, 直接"raise exc_info[1].with_traceback(exc_info[2])".

此外, 如果application返回的可迭代对象有close方法, 则无论请求是否正常完成, WSGI服务端都会调用该迭代对象的close方法, 以帮助WSGI应用端释放资源.

2. 大体实现

下面就是依据WSGI协议对两部分的大体实现:

# WSGI应用端
def application(environ, start_response):
    start_response('200', [('content-type', 'text/html')])  # 开启响应, 向Web服务器返回响应头
    while True:
        yield '...'  # 返回响应内容
# WSGI服务端
def response():
    environ = {}
    def start_response():
        pass  # 开启响应(返回响应头)

    body = ''
    result = application(environ, start_response)  # 每收到一个HTTP请求, 便会调用一次application
    for temp in result:
        body += temp  # 拼接响应内容
    return body  # 返回响应内容

WSGI的应用

Enmmm...WSGI也许的确如你上面所说, 但是该怎么用呢? 他是必须的吗?

1. WSGI该怎么用?

在工作中我们不需要自己编写WSGI, 而是可以依靠现有的第三方库. 比如WSGI应用端有Werkzeug, WSGI服务端有uWSGI, gunicorn等.  WSGI服务端的这些库一般都自带有Web服务器(HTTP服务器), 即Web服务器已经连接WSGI服务端了. 常见的应用框架(Flask, Tornado等)也已经为我们集成了WSGI应用端. 所以我们要做的仅仅是连接WSGI服务端与WSGI应用端, 这个一般不需要编码, 往往通过一条命令即可完成, 具体命令要看所使用的WSGI应用端. 下面让我们看看各个框架是怎样将本身的Web应用变为WSGI应用的.

首先Flask的简单应用:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello Kitty'

if __name__ == '__main__':
    app.run()

其中Flask的定义如下, 当调用"app()"时, 其会被转成一个WSGI应用.

class Flask(_PackageBoundObject):
    ...
    def __call__(self, environ, start_response):
        """The WSGI server calls the Flask application object as the
        WSGI application. This calls :meth:`wsgi_app` which can be
        wrapped to applying middleware."""
        return self.wsgi_app(environ, start_response)

再比如Tornado的简单应用:

# coding=utf-8

import tornado.web
import tornado.wsgi
import tornado.httpserver
import tornado.ioloop


class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello Kitty")


def main():
    application = tornado.web.Application([
        (r"/", IndexHandler),
    ])

    wsgi_app = tornado.wsgi.WSGIAdapter(application)  # 将Tornado的Web应用转为WSGI应用端
    container = tornado.wsgi.WSGIContainer(wsgi_app)  # WSGI服务端

    http_server = tornado.httpserver.HTTPServer(container)  # 定义HTTP服务器
    http_server.listen(8888)
    tornado.ioloop.IOLoop.current().start()


if __name__ == "__main__":
    main()

其中Tornado的WSGIAdapter定义如下, 该类会把一个Tornado的Web应用转变为WSGI应用. 但副作用也很明显, Tornado本身是支持异步请求的, 而WSGI是一个同步协议, 所以经过WSGIAdapter转换后, 整个应用将不再支持异步.

class WSGIAdapter(object):
    def __init__(self, application):
        if isinstance(application, WSGIApplication):
            self.application = lambda request: web.Application.__call__(application, request)
        else:
            self.application = application

    def __call__(self, environ, start_response):
        pass

下面是Tornado中WSGIContainer定义, 可以看出该类的__call__方法会主动调用WSGI应用端的application对象, 并会传入environ和start_response, 很明显该类实现了WSGI服务端. 事实上WSGIContainer可以接收任何Python框架(Tornado, Flask, Django等)所实现的WSGI应用端, 并使得这些应用可以跑在Tornado的HTTP服务器与IO循环上.

class WSGIContainer(object):
    def __init__(self, wsgi_application):
        self.wsgi_application = wsgi_application

    def __call__(self, request):
        data = {}
        response = []

        def start_response(status, response_headers, exc_info=None):  # 定义了start_response
            data["status"] = status
            data["headers"] = response_headers
            return response.append

        app_response = self.wsgi_application(WSGIContainer.environ(request), start_response)  # 调用WSGI应用端的application, 并传入environ与start_response

其实Tornado的简单应用还有另种写法:

# coding=utf-8

import tornado.ioloop
import tornado.web


class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.write('Hello Kitty')


def main():
    app = tornado.web.Application([  # Tornado的Web应用
        (r'/', IndexHandler),
    ])
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()


if __name__ == '__main__':
    main()

所以我们可以看出Tornado是一个比较有意思的框架, 一般框架也就实现个WSGI应用端, 但Tornado另外还实现了可用于生产的HTTP服务器和WSGI服务端. 因此目前由Tornado开发的应用服务, 不仅可以像其他Python Web应用那样通过"Web服务器 --> WSGI实现 --> Web应用"的模式做到线上部署, 还可通过自身"自带的HTTP服务器 --> Tornado应用"模式实现一步到位

但好景不长啊, WSGIAdapter将在Tornado 6.0中被移除, 那之后Tornado应用将如何变为WSGI应用呢, 我们拭目以待, 或者Tornado根本就不关心.

2. WSGI是必须的吗?

结论是只要Web应用涉及线上服务, 即会和用户打交道, WSGI基本上就是必须的(排除Tornado哈). 因为没有WSGI, 常见的应用框架(Django, Flask)是没有办法将HTTP请求解析成Python的request对象的.

你也许会质疑, 我在调试这些框架编写的应用时, 直接就可以运行了, 我也没装Web服务器和WSGI服务端呀?

这主要是因为各大框架为了方便调试, 都内置了测试服务器, 但如果需要用于线上环境, 还是需要正式服务器的支持. 当然如果这些应用仅仅用于离线服务, 那么内置的测试服务器大抵也是可以的.

整个流程

1. 用户(浏览器)发起请求, Web服务器接收HTTP请求, 并将解析后的请求转发给WSGI服务端.

2. WSGI服务端收到请求后立即调用WSGI应用端的application对象, 并将生成的environ与事先定义好的start_response传给该对象.

3. application对象会解析environ, 根据其中的URI调用应用服务中对应的处理方法. 处理完成后, application对象会先调用start_response开启响应, 再将应用服务方法的响应内容迭代返回给WSGI服务端.

4. 最后, WSGI服务端将响应发送给Web服务器, 再由Web服务器返回给用户(浏览器).

此外, 目前比较流行在Web服务器前再加一个Web服务器(比如Nginx)用以代理用户的HTTP请求, 当然其并不会去解析请求. 具体作用嘛, 可能是负载均衡, 控制访问峰值等.

文中如有不当之处, 还望包容和指出, 感谢.

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值