开发者学堂课程【Python Web 开发基础:WSGI 概述和 APP 端开发】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/554/detail/7637
WSGI概述和APP端开发
内容简介:
一. WSGI 概述
二. WSGI 服务——wsgiref
三. WSGI 服务器作用
四. WSGI APP 应用程序端
五. environ
六. start_response
一. WSGI 概述
1、上图是 Python 的一种协议
即客户端的请求发送过来之后,Python 如何按照约定处理请求,处理之后把请求转换成数据,这些数据如何发送回浏览器端的流程图。
这张图可以分为两部分,第一部分是请求,一部分是 APP 。
这个协议要求两次返回。
两次返回的步骤如下:
首先,浏览器发起一个请求通过 Server,Server 本身是 TCP 的,在端口号等着,然后把这些请求做解析报文,解析完之后把数据封装成 environ,只解决 HTTP 协议,数据由 APP 处理。
处理好之后状态码和报文头由它自己去发送。实际上在这个协议中,在 Server 调用 APP 写的程序时,有两个参数进来,一个是 environ 解析的变量,另一个是 environ 发状态码和报文头的方法,即函数名。
函数是在 APP 处调用的,但是这个函数是 Server 提供的。返回的正文要被 Server 进行 HTTP 的封装,然后再返回给浏览器。
一个请求过来之后被解析,解析之后传入给 APP,其中传入两个参数,一个是 environ,另一个是写 HTTP 状态码、报文头给出的方法。一旦调用函数,报文头和状态码就返回去了,HTTP 正文会处理剩下的内容。这是协议本身的要求。
遵从这个协议建立的 Server 叫做 WSGI Server,遵从这个协议构建的 App 叫做 WSGI App。
总体来说,所有外部框架都是这样做的,浏览器发请求给 Server,Server再返回来,不管后面多少层,也不管后面调用多少次,给一个请求就有一个响应。 Server 主要就是 HTTP 解析和 HTTP 的报文封装。
APP 协议,最终就是函数套函数,或者调用类的方法,调用之后函数依次返回,最后正文封装返回过去。
2、WSGI 主要规定了服务器端和应用程序间的接口。
实际上 WSGI 解决了 Server 和 APP 之间的协议问题。Server 做与服务相关的事情,即客户端请求来了之后,建立一个连接,把数据传送过去。因为数据如何处理不是 Server 能决定的,应该由编程者来决定数据如何处理,数据处理之后应该得到什么样的结果返回回去。不能把这部分包含到 Server 里面,应该独立出去,所以 WSGI 解决的是 Server 与 APP 之间的接口问题。
二. WSGI 服务——wsgiref
1、wsgiref 这是一个 WSGl 参考实现库
wsgiref.simple_server 模块实现一个简单的 WSGI HTTP 服务器。
wsgiref.simple_server.make_server(host,port,app,server_class=WSGIServer,handler_class=WSGIRequestHandler) 启动一个 WSGI 服务器
wsgiref.simple_server .demo_app(environ,start_response) 一个函数,小巧完整的 WSGI 的应用程序的实现
2、返回文本例子
from wsgiref.simple_server import make_server,demo_app
ip = '127.0.0.1'
port = 9999
server = make_server(ip,port,demo_app) # demo_app 应用程序,可调用
server.serve_forever() # server.handle_request() 执行一次
3、范例:
From wsgiref.util import setup_testing_defaults
from wsgiref.simple_server import make_server
# A relatively simple WSGI application. It's going to print out the environment dictionary after being updated by setup_testing_defaults
def simple_app (environ, start_response):
setup_testing_defaults (environ)
status = '200 OK'
headers = [ ( 'Content-type', 'text/plain; charset=utf-8')]
start_response (status, headers)
ret = [ ( "%s : %s\n" % (key, value)) .encode ( "utf-8")
for key, value in environ.items()]
return ret # 返回要求可迭代对象,正文就是这个列表的元素,可以是一个元素——字符串
httpd = make_server ( '0.0.0.0',8000, simple_app) #创建 server
print ( "Serving on port 8000...")
httpd.serve_forever()
1)点开 make_server 可以看到以下内容:
def make_server (host, port, app, server_class=WSGIServer,handler_class=WSGIRequestHandler) :
" ""Create a new WSGI server listening on 'host' and 'port’for 'app ' """
server = server_class ((host, port), handler_class)
server.set_app (app)
return server
2)这里是最简单的 APP,里面有两个参数。
Request Headers
GET /img/ bd_logo1.png HTTP/1.1
Host: www . baidu.com
Connection: keep-alive
Pragma: no-cache
把以上这些东西全部都封装到字典里面,然后就可以调用了。还要传一个参数: start_response,它是由 make_server (能够支持 HTTP 协议)。如果有请求来了,就调用 simple_app。
WSGIServer 是从 HTTPServer 来的,而 HTTPServer 是从 socket TCPServer 来的,但是要复杂一点,因为它里面要处理很多的数据,要进行 HTTP 的解析和封装。
from wsgiref.simple_server import make_server,demo_app
server = make_server ( ‘0.0.0.0', 9000,demo_app)
server.serve_forever ()
APP 也是有要求的,APP 必须能接受两个参数,APP 本身是一个可调用对象, APP 只能是函数类型的。
不管对服务器发起什么样的 HTTP 请求,都会用同一个函数去处理。
3)demo_app 里的内容如下:
def demo_app(environ, start_response) :
from io import stringIo
stdout = StringIo()
print ( "Hello world! ", file=stdout)
print (file=stdout)
h = sorted (environ. items () )
for k, v in h:
print (k,'=' , repr(v), file=stdout)
start_response ("200 oK",[( ' Content-Type ' , ' text/plain; charset=utf-8' )])
return [stdout.getvalue ().encode ( "utf-8"")]
demo_app 返回的内容放在一个内存中,关键把环境变量全部排序打印了。以上就是一个套路,我们要写的东西和它一样。
WSGI Server 内部是一个 TCP 的,但是它能够认得 HTTP 协议,可以做 HTTP 的解析和封装。它还能够把请求来到之后,调用写好的 APP。
Demo 为什么这样做,因为现在也不知道本地有什么文件,就用内存中的一个文件对象准备数据,准备好数据以后把这个字符串发出去即可,然后封装,通过 APP 返回浏览器端就可以了。
我们在那个基础上做,实际上不太关心最后怎么去读取环境变量,真正关心的是 HTTP 请求过来的东西被解析到字典,想看一下这个字典,代码如下:
def simple_app (environ, start_response):
print (environ)
status = '200 OK'
headers =[( 'Content-type', 'text/plain; charset=utf-8')]
start_response (status, headers)
ret = [( "%s: %s\n" %( key, value)) .encode ( "utf-8")
for key, value in environ.items () ]
return ret # 返回要求可迭代对象,正文就是这个列表的元素,可以是一个元素——字符串
httpd = make_server ('0.0.0.0',9000, simple_app) #创建 server
try:
httpd.serve_forever ()
except Exception as e:
print (e)
except KeyboardInterrupt:
print (' stop ' )
httpd.server_close()
4)注意:APP 这块要想办法要把里面的异常尽量捕获掉,而不是把异常抛给 WSGI Server,因为 WSGI Server 处理不了就会直接崩掉。如果能用 TCP server,那也是影响一个线程,关键在于怎么处理,如果一个请求扔给一个线程,那么最多那个请求所在线程崩掉。对于自己来写的 APP,异常处理是需要的,但是如果遇到一个框架,那个框架是在外面,包裹着把异常处理掉了。
捕获到网络只剩一个,因为只发起了一次 HTTP 请求,把环境数据准备好,然后都打印出来,之后把这些数据是又作为响应的报文传回来了。
5)Request Headers
GET / HTTP/1.1
Host: 127.8.e.1:9000
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
X-DevTools-Emulate-Network-Conditions-client-Id: 9f2371c6-1a96-42c4-a081-a90705ded03f
User-Agent : Mozilla/5.0 (Windows NT 6.1; WOW64) ApplewebKit/537.36 (KHTML,like Gecko) Maxthon/5.1.0.4.0000 Chrome/55.0.2883.75 Safari/537.36
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,* /*; q=0.8
请求依然是 get,对根发起了访问,然后允许长段时间连接,尽量保持连接。都是浏览器发的,浏览器在跟 server 之间做协商。不管怎么样,HTTP 协议都是支持文本的,所以没返回 html,返回的是纯文本。
响应了一些内容:
HTTP/1.1 200 OK
Date: Mon,25 Jun 2018 06:40:22 GMT
Server: WSGIServer/0.2 CPython/ 3.5.3
Content-type: text/plain; charset=utf-8
Transfer-Encoding: chunked
即发回来纯文本,不是 html,是 utf-8 编码的。
6)environ
environ 是包含 Http 请求信息的 dict 对象
名称及含义如下:
REQUEST_METHOD :请求方法,GET、POST 等
PATH_INFO :URL 中的路径部分
QUERY_STRING :查询字符串
SERVER_NAME, SERVER_PORT :服务器名、端口
HTTP_HOST :地址和端口
SERVER_PROTOCOL :协议
HTTP_USER_AGENT :UserAgent 信息
请求头的字典在客户端打印出来,其中 REMOTE ADDR127.0.0.1 远程地址还是要关心的,即浏览器是用什么 IP 地址来访问的。主要关心 environment 里面的比如 pass,method 这些信息。
三. WSGI 服务器作用
1)监听 HTTP 服务端口 (TCPServer,默认端口80 )
2)接收浏览器端的 HTTP 请求并解析封装成 environ 环境数据
3)负责调用应用程序,将 environ 和 start_response 方法传入
4)将应用程序响应的正文封装成 HTTP 响应报文返回浏览器端
四. WSGI APP 应用程序端
1、应用程序应该是一个可调用对象
Python 中应该是函数、类、实现了 _call_ 方法的类的实例
class A:
def _init_(self, name, age):
pass
def _call_(self, environ, start_response) :
pass
class B:
def _init_(self, environ, start_response):
for k, v in environ.items ():
print (k, v)
print ('-‘ *30)
status ='200 OK‘
headers = [ ( 'Content-type', 'text/plain; charset=utf-8')]
start_response (status, headers)
ret =[("%s : %s\n" %(key, value) ) .encode ( "utf-8")
for key, value in environ.items()]
self.ret = ret
return ret # 返回要求可迭代对象,正文就是这个列表的元素,可以是一个元素——字符串
def _iter_(self):
yield from self.ret
2、这个可调用对象应该接收两个参数
#1函数实现
def application(environ, start_response):
pass
#2类实现
class Application:
def _init_(self, environ, start_response):
pass
#3类实现
class Application:
def _cal1_(self, environ, start_response):
pass
3、以上的可调用对象实现,都必须返回一个可迭代对象
res_str = b 'magedu.com\n '
#函数实现
def application(environ, start_response):
return [res_str]
#类实现
class Application:
def _init_(self, environ, start_response):
pass
def _iter_(se1f): # 实现此方法,对象即可迭代公
yield res_str
#类实现
class Application:
def _call_(self, environ, start_response):
return [res_str]
environ和start_response这两个参数名可以是任何合法名,但是一般默认都是这2个名字。应用程序端还有些其他的规定,暂不用关心。
for x in B (environ, start response):
for x in simple_app(environ, start response) :
xyz(x)
对比这两行,初始化函数传参进去之后,返回的对象要可迭代,返回不能直接 return。
五. environ
environ 是包含 Http 请求信息的 dict 对象
名称及含义如下:
REQUEST_METHOD :请求方法,GET、POST 等
PATH_INFO :URL 中的路径部分
QUERY_STRING :查询字符串
SERVER_NAME, SERVER_PORT :服务器名、端口
HTTP_HOST :地址和端口
SERVER_PROTOCOL :协议
HTTP_USER_AGENT :UserAgent 信息
其中比较重要的是 PATH_INFO,QUERY_STRING 和HTTP_USER_AGENT。
UserAgent 一般是用来做分析的,其实请求到底是 A 浏览器还是 B 浏览器,一般情况下不太关心,但是有的时候要关心。因为用户如果是 IE 的,实际上支持的 html 和 CSS 的版本是不一样的。如果返回的东西是统一的回去的话,最后会发现可能在某一个浏览器里面错位了,整个 html 就变形了。所以用户体验会非常糟糕,就会选择关闭。
所以 UserAgent 最早是解决,浏览器端告诉是什么软件,用户必须按照浏览器支持的版本,至于到底兼容不兼容正在使用浏览器,那是你的问题。但是现在在客户端也是用各种框架,其实用框架来解决不同浏览器之间的兼容性问题,所以现在可以写个统一的页面。但是如果特别在意用户这边的显示的话,实际上还是要让你去看一下用户的 UserAgent 究竟是什么,然后给他配相应的 html 回去,不然不支持。
六. start_response
它是一个可调用对象。有3个参数,定义如下∶
start_response(status, response_headers, exc_info=None) status 是状态码,例如200 OK
response_headers 是一个元素为二元组的列表,例如 [('Content-Type', 'text/plain; charset=utf-8')]
exc_info 在错误处理的时候使用
start_response 应该在返回可迭代对象之前调用,因为它返回的是 Response Header。返回的可迭代对象是 Response Body。