官网
wsgi offical website: https://www.python.org/dev/peps/pep-0333/
了解了HTTP协议和HTML文档,我们其实就明白了一个Web应用的本质就是:
- 浏览器发送一个HTTP请求;
- 服务器收到请求,生成一个HTML文档;
- 服务器把HTML文档作为HTTP响应的Body发送给浏览器;
- 浏览器收到HTTP响应,从HTTP Body取出HTML文档并显示。
所以,最简单的Web应用就是先把HTML用文件保存好,用一个现成的HTTP服务器软件,接收用户请求,从文件中读取HTML,返回。Apache、Nginx、Lighttpd等这些常见的静态服务器就是干这件事情的。
如果要动态生成HTML,就需要把上述步骤自己来实现。不过,接受HTTP请求、解析HTTP请求、发送HTTP响应都是苦力活,如果我们自己来写这些底层代码,还没开始写动态HTML呢,就得花个把月去读HTTP规范。
正确的做法是底层代码由专门的服务器软件实现,我们用Python专注于生成HTML文档。因为我们不希望接触到TCP连接、HTTP原始请求和响应格式,所以,需要一个统一的接口,让我们专心用Python编写Web业务。
这个接口就是WSGI:Web Server Gateway Interface。
WSGI接口定义非常简单,它只要求Web开发者实现一个函数,就可以响应HTTP请求。我们来看一个最简单的Web版本的“Hello, web!”:
def application(environ, start_response):
start_response(‘200 OK’, [(‘Content-Type’, ‘text/html’)])
return [b’
Hello, web!
‘]上面的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用一个包含两个str的tuple表示。
通常情况下,都应该把Content-Type头发送给浏览器。其他很多常用的HTTP Header也应该发送。
然后,函数的返回值b’
Hello, web!
‘将作为HTTP响应的Body发送给浏览器。有了WSGI,我们关心的就是如何从environ这个dict对象拿到HTTP请求信息,然后构造HTML,通过start_response()发送Header,最后返回Body。
整个application()函数本身没有涉及到任何解析HTTP的部分,也就是说,底层代码不需要我们自己编写,我们只负责在更高层次上考虑如何响应请求就可以了。
不过,等等,这个application()函数怎么调用?如果我们自己调用,两个参数environ和start_response我们没法提供,返回的bytes也没法发给浏览器。
所以application()函数必须由WSGI服务器来调用。有很多符合WSGI规范的服务器,我们可以挑选一个来用。但是现在,我们只想尽快测试一下我们编写的application()函数真的可以把HTML输出到浏览器,所以,要赶紧找一个最简单的WSGI服务器,把我们的Web应用程序跑起来。
好消息是Python内置了一个WSGI服务器,这个模块叫wsgiref,它是用纯Python编写的WSGI服务器的参考实现。所谓“参考实现”是指该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用。
运行WSGI服务
我们先编写hello.py,实现Web应用程序的WSGI处理函数:
# hello.py
def application(environ, start_response):
start_response(‘200 OK’, [(‘Content-Type’, ‘text/html’)])
return [b’
Hello, web!
‘]然后,再编写一个server.py,负责启动WSGI服务器,加载application()函数:
# server.py
# 从wsgiref模块导入:
from wsgiref.simpleserver import make_server
# 导入我们自己编写的application函数:_
from hello import application
# 创建一个服务器,IP地址为空,端口是8000,处理函数是application:
httpd = makeserver(‘’, 8000, application)
print(‘Serving HTTP on port 8000…’)
# 开始监听HTTP请求:_
httpd.serve_forever()
确保以上两个文件在同一个目录下,然后在命令行输入python server.py来启动WSGI服务器:
注意:如果8000端口已被其他程序占用,启动将失败,请修改成其他端口。
启动成功后,打开浏览器,输入http://localhost:8000/,就可以看到结果了:
在命令行可以看到wsgiref打印的log信息:
按Ctrl+C终止服务器。
如果你觉得这个Web应用太简单了,可以稍微改造一下,从environ里读取PATHINFO,这样可以显示更加动态的内容:
# hello.py_
def application(environ, start_response):
start_response(‘200 OK’, [(‘Content-Type’, ‘text/html’)])
body = ‘
Hello, %s!
‘ % (environ[‘PATH_INFO’][1:] or ‘web’)return [body.encode(‘utf-8’)]
你可以在地址栏输入用户名作为URL的一部分,将返回Hello, xxx!:
是不是有点Web App的感觉了?
小结
无论多么复杂的Web应用程序,入口都是一个WSGI处理函数。HTTP请求的所有输入信息都可以通过environ获得,HTTP响应的输出都可以通过start_response()加上函数返回值作为Body。
复杂的Web应用程序,光靠一个WSGI函数来处理还是太底层了,我们需要在WSGI之上再抽象出Web框架,进一步简化Web开发。
参考源码
what is wsgi
WSGI: server, middleware, application
作者:不求东西链接:https://www.zhihu.com/question/19998865/answer/74748765来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
WSGI里的组件分为『Server』,『Middleware』和『Application』三种,其中的『Middleware』是『设计模式』里的Decorator(装饰器)。
WSGI规范在PEP-333里讲得很详细:PEP 0333 — Python Web Server Gateway Interface v1.0 ,但我觉得比理解规范更重要的,是理解其设计目的和工作原理。
WSGI规范写得有点绕,之所以绕, 主要原因可能是没有用『类型提示(Type Hints)』,如果用强类型OOP语言的那种『Interface』和『UML』来解释会清晰很多。下面我试试用这种带有『类型提示』的风格简单讲讲WSGI的原理。
首先是『Application』如果把它算作接口的话,它 要能被直接调用(Callable),换句话说,它应该是一个函数或者定义了『call』方法的对象,『call』方法的签名是这样的:
def call(environ: Dict[String, Any],
start_response: (String, List) -> Any)): Iterable[String]
为了便于叙述,这里用的『类型提示』没有严格遵从Python语法。这里大致的意思是 call 方法需要两个参数,『environ』是一个键(Key)为 String 类型,值(Value)为 任意(Any)类型的字典,『startresponse』则是一个函数(实际上为了兼容已有框架,它也可以是一个函数工厂,详见PEP-0333)。 _call 方法返回一个可迭代的对象。
可以这样理解,如果想正确调用『Application』,就必须知道两样东西,『数据』和『上层处理方法』,『数据』是那个environ变量(CGI里就叫这个名字),『上层处理方法』是那个start_response 回调函数,这两个参数,须在Server调用Application的时候,传给Application。或者说Application依赖于『数据』和『上层处理方法』,这两个依赖由Server『注入』给Application(称作『依赖注入 DI』)。
容易困惑的一点是为什么『上层处理方法』要通过参数传给Application,直接在Server里处理好了,把处理好的结果传给Application不是更简单吗?的确这样做似乎也可以,不过把『上层处理方法』作为回调函数传给Application,可以由Application本身控制该方法的调用时机,是更加灵活的一种方案(称作『控制反转 IoC』)。
接下来是『Middleware』为了简化代码,我们先把『Application』单独定义为一个类型:
type Application = (Dict[String, Any], (String, List) -> Any)) ->
Iterable[String]
其实就是前面那个 call 方法的签名,至于Application究竟是什么,叫它『函数』,或者『实现了call方法的 对象』,『实现了init方法的类』都可以,总之都可以通过『Application()』去使用,我们暂且统称为『Callable对象』。
『Middleware』本质上是一个『装饰器 Decorator』,和Application类似它也是一个『Callable对象』,如果它有『call』方法,其签名应该是这样的:
def call(app: Application): transformedApp: Application
这个Middleware返回的类型是一个被『装饰』过的Application(transformedApp变量),这个transformedApp所依赖的『environ』和『start_response』可以被当前Middleware所在上层的Server/Middleware所注入。
再看 这个Middleware的参数,它也是一个Application(app变量),所以Middleware本身的一些附加的逻辑和数据也可以通过app的参数注入到下层的Application里。
Middleware是可以嵌套使用的,比如有『mw1』和『mw2』两个Middleware和『app』一个Application,就可以通过
mw1(mw2(app))
返回一个新的Application,如果mw1和mw2是相互独立的,嵌套顺序理论上也可以互换。
也可以使用Python的『Decorator』语法,直接装饰app:
@mw1
@mw2
def app(environ, start_response):
…
最后是『Server』好像能说的不多,这一层更像是一个『适配器 Adapter』,比如PEP-0333里面给的例子就是一个『CGI』的Adapter。它从 os.environ 或 sys.stdin(CGI规定的输入方式) 中获取Request数据,自定义了start_response和write函数,在其中使用 sys.stdout(CGI规定的输出方式) 进行响应。它把environ和start_response传给Application进行调用,然后遍历Application返回的Iterable,使用write函数把结果写到sys.stdin里。 写进sys.stdin,其实就是和更上层的,也就是Web Server(例如Apache/Nginx)进行通讯,Web Server通过Socket获取到内容以后,就可以最终把Response返回给用户了。
WSGI的作用
很多框架都自带了 WSGI server ,比如 Flask,webpy,Django、CherryPy等等。当然性能都不好,自带的 web server 更多的是测试用途,发布时则使用生产环境的 WSGI server或者是联合 nginx 做 uwsgi 。
WSGI有两方:“服务器”或“网关”一方,以及“应用程序”或“应用框架”一方。服务方调用应用方,提供环境信息,以及一个回调函数(提供给应用程序用来将消息头传递给服务器方),并接收Web内容作为返回值。
所谓的 WSGI中间件同时实现了API的两方,因此可以在WSGI服务和WSGI应用之间起调解作用:从WSGI服务器的角度来说,中间件扮演应用程序,而从应用程序的角度来说,中间件扮演服务器。“中间件”组件可以执行以下功能:
- 重写环境变量后,根据目标URL,将请求消息路由到不同的应用对象。
- 允许在一个进程中同时运行多个应用程序或应用框架。
- 负载均衡和远程处理,通过在网络上转发请求和响应消息。
- 进行内容后处理,例如应用XSLT样式表。
WSGI 的设计确实参考了 Java 的 servlet。http://www.python.org/dev/peps/pep-0333/ 有这么一段话:
By contrast, although Java has just as many web application frameworks available, Java’s “servlet” API makes it possible for applications written with any Java web application framework to run in any web server that supports the servlet API.
WDGI 连接了 服务器OS和Web APP
1 首先理解:用户通过浏览器访问网页,具体经过了哪些环节?
用户通过浏览器访问网页,在应用层就是用户的浏览器和服务器的Web App会话,而建立应用层的会话需要依托TCP/IP协议封装与数据传输,具体步骤有:
(1)用户输入URL
(2)浏览器代为封装成符合http格式的Request请求,包含请求首行、请求头和请求体
(3)Request请求是应用层数据,再由OS完成TCP、IP、MAC层封装,送到网卡处以比特流形式送送
(4)经过网络传输,比特流到达服务器端,被服务器接收。
(5)服务器OS逐一去掉 MAC、IP、TCP层封装,剥出应用层数据,也就是Request请求,并交给应用层的Web应用
(6)Web解析Request请求内容,并生成Respond响应,交给服务器OS
(7)Respond响应也是应用层数据,由服务器OS完成TCP、IP、MAC层封装,送到网卡处以比特流形式送送
(8)经过网络传输,比特流到达服务器端,被用户机器接收。
(9)用户机器OSS逐一去掉 MAC、IP、TCP层封装,剥出应用层数据,也就是Respond响应,并交给应用层的浏览器。
(10)浏览器根据Response响应内容,组织显示给用户看。
2 接着着重理解:在上述流程里,服务器OS和Web APP各自做了什么事情?
【服务器OS】
从比特流拆出Request请求,以及将Response打包成比特流。
【Web APP】
主要在实现http协议,具体有:
1收到 Request;
2解析Request具体请求内容,调用资源生成html,并组织成一个Response;
3发出Response;
3、再理解:服务器OS跟Web APP如何完成数据交换?
注意到服务器OS跟Web APP有数据交换:服务器OS将Request请求传给Web APP,Web APP处理后,将Respond响应传给服务器OS。
那么,服务器OS怎么把Request请求传给Web APP?
答案,通过调用WSGI接口。WSGI接口是抽象的,具体实现一般是调用函数。
如httpd = make_server(‘’, 8001, application)创建一个服务器,这个服务器上的WSGI接口实现,就是调用application函数。在application中实现了Web App的请求处理,服务器调用application函数,同时将自己的成果Request作为参数传递进去,就由Web App接力完成下一步处理。Web App处理后,成果作为application的返回值返回给服务器。
最后回答开头的两个问题:
1、这个接口连接了哪两个模块?
答:服务器OS和Web APP。
2、两个模块各自做了什么工作,通过接口连接后,共同实现了什么功能?
服务器OS完成TCP/IP底层的工作,Web App完成TCP/IP应用层的工作,共同组成Web服务器。