【Python】WSGI(Web Server Gateway Interface)、uWSGI Server、uwsgi、WSGI Application 的故事

Posted by 西维蜀黍 on 2019-10-28, Last Modified on 2025-04-23

关于 Web Server(Web 服务器)、Web Application Server(Web 应用服务器)和 CGI(Common Gateway Interface)的区别,可以访问【HTTP】Web Server(Web 服务器)、Web Application Server(Web 应用服务器)和 CGI(Common Gateway Interface)的故事

WSGI(Web Server Gateway Interface)

WSGI 是 Web Server Gateway Interface 的缩写。

它不是服务器、Python 模块、框架、API 或者任何软件,而是一种描述 Web 服务器(如 Nginx)如何与 Web application Server(Web 应用程序,如用 Django、Flask 框架编写的程序)通信的规范。

因此,WSGI 是一种规范,或者说一种接口,或者说一种协议,equivalent 是 Java 世界中的 servlet。

WSGI 是在 PEP 333 提出的,并在 PEP 3333 进行补充(主要是为了支持 Python3.x)。

这个协议旨在解决众多 Python Web 框架和 Web server 软件的兼容问题。有了 WSGI,你再也不用因为使用特定的 Web 应用框架而不得不选择特定的 Web server 软件。

只要遵照了 WSGI 协议的的 Web 应用(Web application,我们称之为 WSGI 应用),都可以在任何遵照了 WSGI 协议的 Web server 软件(我们称之为 WSGI server,比如 uWSGI)上运行。

而对于 WSGI 应用(WSGI Application),我们理论上可以完全自己从头编写一个 WSGI 应用,但是,我们通常会使用 WSGI 应用框架(WSGI Application Framework),或者称为 WSGI MiddleWare,比如 Django,Flask 等,因为框架已经帮我们封装了大量的底层逻辑,而让我们可以更好的专注于业务逻辑上。

为了更好的理解 WSGI 应用(WSGI Application),我们接下来会写一个非常原始的 WSGI 应用。

WSGI Server

WSGI server 就是一个符合 WSGI 规范的 Web server,接收 request 请求,封装一系列环境变量,按照 WSGI 规范调用注册的 WSGI 应用,最后将 response 返回给客户端。

python 自带的 wsgiref 就是按照 WSGI 规范实现的一个简单 WSGI server。它的代码也不复杂。

常用的 WSGI server(WSGI 服务器):wsgiref,uWSGI,Gunicorn 等。

WSGI Application(WSGI 应用)

WSGI Application 就是一个普通的 callable 对象,当有请求到来时,WSGI server 会调用这个 WSGI Application。这个对象接收两个参数,通常为 environ 和 start_response。

  • environ 可以理解为环境变量,跟一次请求相关的所有信息都保存在了这个环境变量中,包括服务器信息,客户端信息,请求信息。
  • start_response 是一个 callback 函数,WSGI application 通过调用 start_response,将 response headers/status 返回给 WSGI server。此外这个 WSGI application 会 return 一个 iterator 对象 ,这个 iterator 就是 response body。

一个 HTTP 请求的过程可以分为两个阶段,第一阶段是从客户端(比如 Postman、浏览器)到 WSGI Server,第二阶段是从 WSGI Server 到 WSGI Application。

WSGI Middleware

有些功能可能介于 WSGI Server 和 WSGI Application 之间,例如,WSGI Server 拿到了客户端请求的 URL, 不同的 URL 需要交由不同的函数处理,这个功能叫做 URL Routing,这个功能就可以放在二者中间实现,这个中间层就是 middleware。

middleware 对 WSGI Server 和 WSGI Application 是透明的,也就是说,WSGI Server 以为它就是应用程序,而 WSGI Application 以为它就是服务器。

手工实现 WSGI Application

为了更好的理解 WSGI 接口,我们接下来会写一个非常原始的 WSGI 应用。

WSGI 接口

WSGI 接口,其实非常简单,即只要求 WSGI Application 要实现一个函数,且有如下三个要求:

  1. 必须是一个可调用的对象
  2. 接收两个必选参数 environ、start_response。
  3. 返回值必须是可迭代对象,用来表示 http body。

Hello, web

我们来看一个最简单的 Web 版本的 “Hello, web!”:

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [b'<h1>Hello, web!</h1>']

分析

上面的 application() 函数就是符合 WSGI 标准的一个 HTTP 处理函数,它接收两个参数:

  • environ:一个包含所有 HTTP 请求信息的 dict 对象;
  • start_response:一个发送 HTTP 响应的函数。

application() 函数中,调用:

start_response('200 OK', [('Content-Type', 'text/html')])

就发送了 HTTP 响应的 Header,注意 Header 只能发送一次,也就是只能调用一次 start_response() 函数。start_response() 函数接收两个参数,一个是 HTTP 响应码,一个是一组 list 表示的 HTTP Header,每个 Header 用一个包含两个 strtuple 表示。

通常情况下,都应该把 Content-Type 头发送给浏览器。其他很多常用的 HTTP Header 也应该发送。

然后,函数的返回值 b'Hello, web!' 将作为 HTTP 响应的 Body 发送给浏览器。

函数的调用

有了 WSGI,我们关心的就是如何从 environ 这个 dict 对象拿到 HTTP 请求信息,然后构造 HTML,通过 start_response() 发送 Header,最后返回 Body。

整个 application() 函数本身没有涉及到任何解析 HTTP 的部分。而事实上,WSGI Server 会(在调用我们写的 application() 的时候)帮我们准备好这个 environ 对象。

因此,我们只负责在更高层次上考虑如何响应请求就可以了(i.e., 处理业务逻辑)。

函数的回调

而这个 application() 函数会被怎么调用?如果我们自己调用,两个参数 environstart_response 我们没法提供,返回的 bytes 也没法发给浏览器。

所以 application() 函数必须由 WSGI server 来调用。有很多符合 WSGI 规范的服务器,我们可以挑选一个来用。但是现在,我们只想尽快测试一下我们编写的 application() 函数真的可以把 HTML 输出到浏览器,所以,要赶紧找一个最简单的 WSGI server,把我们的 Web 应用程序跑起来。

好消息是 Python 内置了一个 WSGI server,这个模块叫 wsgiref,它是用纯 Python 编写的 WSGI server 的参考实现。所谓 “参考实现” 是指该实现完全符合 WSGI 标准,但是不考虑任何运行效率,仅供开发和测试使用。

运行 WSGI Application

我们先编写 hello.py,实现 Web 应用程序的 WSGI 处理函数:

# hello.py

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [b'<h1>Hello, web!</h1>']

然后,再编写一个 server.py,负责启动 WSGI server,加载 application() 函数:

# server.py
# 从wsgiref模块导入:
from wsgiref.simple_server import make_server
# 导入我们自己编写的application函数:
from hello import application

# 创建一个服务器,IP地址为空,端口是8000,处理函数是application:
httpd = make_server('', 8000, application)
print('Serving HTTP on port 8000...')
# 开始监听HTTP请求:
httpd.serve_forever()

确保以上两个文件在同一个目录下,然后在命令行输入 python server.py 来启动 WSGI server。

打开浏览器,输入 http://localhost:8000/,就可以看到结果了:

在命令行可以看到 wsgiref 打印的 log 信息:

$ python main.py
Serving HTTP on port 8000...
127.0.0.1 - - [28/Oct/2019 20:55:49] "GET / HTTP/1.1" 200 20
127.0.0.1 - - [28/Oct/2019 20:55:49] "GET /favicon.ico HTTP/1.1" 200 20

如果你觉得这个 Web 应用太简单了,可以稍微改造一下,从 environ 里读取 PATH_INFO,这样可以显示更加动态的内容:

# hello.py

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    body = '<h1>Hello, %s!</h1>' % (environ['PATH_INFO'][1:] or 'web')
    return [body.encode('utf-8')]

你可以在地址栏输入用户名作为 URL 的一部分,将返回 Hello, xxx!

WSGI Middleware

Background

了解了 WSGI 框架,我们发现:其实一个 Web App,就是写一个 WSGI 的处理函数,针对每个 HTTP 请求进行响应。

但是如何处理 HTTP 请求不是问题,问题是如何处理 100 个不同的 URL。

每一个 URL 可以对应 GET 和 POST 请求,当然还有 PUT、DELETE 等请求,但是我们通常只考虑最常见的 GET 和 POST 请求。

一个最简单的想法是从 environ 变量里取出 HTTP 请求的信息,然后逐个判断:

def application(environ, start_response):
    method = environ['REQUEST_METHOD']
    path = environ['PATH_INFO']
    if method=='GET' and path=='/':
        return handle_home(environ, start_response)
    if method=='POST' and path='/signin':
        return handle_signin(environ, start_response)
    ...

只是这么写下去代码是肯定没法维护了。

代码这么写没法维护的原因是因为 WSGI 提供的接口虽然比 HTTP 接口高级了不少,但和 Web App 的处理逻辑比,还是比较低级,我们需要在 WSGI 接口之上能进一步抽象,让我们专注于用一个函数处理一个 URL,至于 URL 到函数的映射,就交给 Web 框架来做。

由于用 Python 开发一个 Web 框架十分容易,所以 Python 有上百个开源的 Web 框架,比如 Flask。

WSGI Middleware

我们可以通过下图来说明 middleware 是如何工作的:

上图中最上面的三个彩色框表示角色,中间的白色框表示操作,操作的发生顺序按照 1 ~ 5 进行了排序,我们直接对着上图来说明 middleware 是如何工作的:

  1. Server 收到客户端的 HTTP 请求后,生成了 environ_s,并且已经定义了 start_response_s
  2. Server 调用 Middleware 的 application 对象,传递的参数是 environ_sstart_response_s
  3. Middleware 会根据 environ 执行业务逻辑,生成 environ_m,并且已经定义了 start_response_m
  4. Middleware 决定调用 Application 的 application 对象,传递参数是 environ_mstart_response_m。Application 的 application 对象处理完成后,会调用 start_response_m 并且返回结果给 Middleware,存放在 result_m 中。
  5. Middleware 处理 result_m,然后生成 result_s,接着调用 start_response_s,并返回结果 result_s 给 Server 端。Server 端获取到 result_s 后就可以发送结果给客户端了。

从上面的流程可以看出 middleware 应用的几个特点:

  1. Server 认为 middleware 是一个 application。
  2. Application 认为 middleware 是一个 server。
  3. Middleware 可以有多层。

因为 Middleware 能过处理所有经过的 request 和 response,所以要做什么都可以,没有限制。比如可以检查 request 是否有非法内容,检查 response 是否有非法内容,为 request 加上特定的 HTTP header 等,这些都是可以的。

WSGI 参数

environ 参数

environ 参数是一个 Python 的字典,里面存放了所有和客户端相关的信息,这样 application 对象就能知道客户端请求的资源是什么,请求中带了什么数据等。environ 字典包含了一些 CGI 规范要求的数据,以及 WSGI 规范新增的数据,还可能包含一些操作系统的环境变量以及 Web 服务器相关的环境变量。我们来看一些 environ 中常用的成员:

首先是 CGI 规范中要求的变量:

  • REQUEST_METHOD: 请求方法,是个字符串,‘GET’, ‘POST’等
  • SCRIPT_NAME: HTTP 请求的 path 中的用于查找到 application 对象的部分,比如 Web 服务器可以根据 path 的一部分来决定请求由哪个 virtual host 处理
  • PATH_INFO: HTTP 请求的 path 中剩余的部分,也就是 application 要处理的部分
  • QUERY_STRING: HTTP 请求中的查询字符串,URL 中 **?** 后面的内容
  • CONTENT_TYPE: HTTP headers 中的 content-type 内容
  • CONTENT_LENGTH: HTTP headers 中的 content-length 内容
  • SERVER_NAMESERVER_PORT: 服务器名和端口,这两个值和前面的 SCRIPT_NAME, PATH_INFO 拼起来可以得到完整的 URL 路径
  • SERVER_PROTOCOL: HTTP 协议版本,HTTP/1.0 或者 HTTP/1.1
  • HTTP_: 和 HTTP 请求中的 headers 对应。

WSGI 规范中还要求 environ 包含下列成员:

  • wsgi.version:表示 WSGI 版本,一个元组 (1, 0),表示版本 1.0
  • wsgi.url_scheme:http 或者 https
  • wsgi.input:一个类文件的输入流,application 可以通过这个获取 HTTP request body
  • wsgi.errors:一个输出流,当应用程序出错时,可以将错误信息写入这里
  • wsgi.multithread:当 application 对象可能被多个线程同时调用时,这个值需要为 True
  • wsgi.multiprocess:当 application 对象可能被多个进程同时调用时,这个值需要为 True
  • wsgi.run_once:当 server 期望 application 对象在进程的生命周期内只被调用一次时,该值为 True

上面列出的这些内容已经包括了客户端请求的所有数据,足够 application 对象处理客户端请求了。

start_resposne 参数

start_response 是一个可调用对象,接收两个必选参数和一个可选参数:

  • status: 一个字符串,表示 HTTP 响应状态字符串
  • response_headers: 一个列表,包含有如下形式的元组:(header_name, header_value),用来表示 HTTP 响应的 headers
  • exc_info(可选): 用于出错时,server 需要返回给浏览器的信息

当 application 对象根据 environ 参数的内容执行完业务逻辑后,就需要返回结果给 server 端。我们知道 HTTP 的响应需要包含 status,headers 和 body,所以在 application 对象将 body 作为返回值 return 之前,需要先调用 start_response(),将 status 和 headers 的内容返回给 server,这同时也是告诉 server,application 对象要开始返回 body 了。

application 对象的返回值

application 对象的返回值用于为 HTTP 响应提供 body,如果没有 body,那么可以返回 None。如果有 body 的化,那么需要返回一个可迭代的对象。server 端通过遍历这个可迭代对象可以获得 body 的全部内容。

uWSGI 服务器

uWSGI 是一个全功能的 HTTP 服务器,实现了 WSGI 协议、uwsgi 协议、HTTP 协议等。它要做的就是把 HTTP 协议转化成语言支持的网络协议。比如把 HTTP 协议转化成 WSGI 协议,让 Python 可以直接使用。

在 Java 的世界中,servlet 容器会基于 servlet 接口向其内部运行的 servlet application 转发 HTTP 请求,当 servlet application 处理完成后,servlet 容器将 HTTP Reponse 返回给 Client。

其实,uWSGI 服务器对应于实现了 servlet 接口的 Tomcat 或者 Jetty。

uwsgi 协议

与 WSGI 一样,uwsgi 是一个 uWSGI 服务器自有通信协议,用于定义传输信息的类型 (type of information)。每一个 uwsgi packet 前 4byte 为传输信息类型的描述,与 WSGI 协议是两种东西。

WSGI 的支持

一般常用的 Web 服务器,如 Apache 和 nginx,都不会内置 WSGI 的支持,而是通过扩展来完成。比如 Apache 服务器,会通过扩展模块 mod_wsgi 来支持 WSGI。Apache 和 mod_wsgi 之间通过程序内部接口传递信息,mod_wsgi 会实现 WSGI 的 server 端、进程管理以及对 application 的调用。Nginx 上一般是用 proxy 的方式,用 nginx 的协议将请求封装好,发送给应用服务器,比如 uWSGI,应用服务器会实现 WSGI 的服务端、进程管理以及对 application 的调用。

Reference