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

Posted by 西维蜀黍 on 2019-10-28, Last Modified on 2021-10-16

关于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