WSGI协议
WSGI
WSGI主要规定了服务器端和应用程序间的接口。
WSGI服务器--wsgiref
wsgiref这个一个WSGI参考实现库
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的应用程序的实现
# 返回文本例子
from wsgiref.simple_serverimportmake_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() 执行一次
WSGI 服务器作用
- 监听HTTP服务端口(TCPServer,默认端口80)
- 接收浏览器端的HTTP请求并解析封装成environ环境数据
- 负责调用应用程序,将environ和start_response方法传入
- 将应用程序响应的正文封装成HTTP响应报文返回浏览器端
WSGI APP应用程序端
-
应用程序应该是一个可调用对象
Python中应该是函数、类、实现了__call__方法的类的实例 -
这个可调用对象应该接收两个参数
# 1 函数实现
def application(environ, start_response):
pass
# 2 类实现
class Application:
def __init__(self, environ, start_response):
pass
# 3 类实现
class Application:
def __call__(self, environ, start_response):
pass
3、以上的可调用对象实现,都必须返回一个可迭代对象
res_str = b'gloo.com.cn\n'
# 函数实现
def application(environ, start_response):
return [res_str]
# 类实现
class Application:
def __init__(self, environ, start_response):
pass
def __iter__(self): # 实现此方法,对象即可迭代
yield res_str
# 类实现
class Application:
def __call__(self, environ, start_response):
return [res_str]
environ和start_response这两个参数名可以是任何合法名,但是一般默认都是这2个名字。应用程序端还有些其他的规定,暂不用关心
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信息 |
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。
服务器端
服务器程序需要调用符合上述定义的可调用对象APP,传入environ、start_response,APP处理后,返回响应头和可迭代对象的正文,由服务器封装返回浏览器端。
# 返回网页的例子
from wsgiref.simple_server import make_server
def application(environ, start_response):
status = '200 OK'
headers = [('Content-Type', 'text/html;charset=utf-8')]
start_response(status, headers)
# 返回可迭代对象
html = "<h1>I'm Gloo</h1>".encode("utf-8")
return [html]
ip = '127.0.0.1'
port = 9999
server = make_server(ip, port, application)
server.server_forever() # server.handle_request() 一次
simple_server 只是参考用,不能用于生产。
测试用命令
-I 使用HEAD方法
-X 指定方法,-d传输数据
到这里就完成了一个简单的WEB 程序开发。
WEB服务器
- 本质上就是一个TCP服务器,监听在特定端口上
- 支持HTTP协议,能够将HTTP请求报文进行解析,能够把响应数据进行HTTP协议的报文封装并返回浏览器端。
- 实现了WSGI协议,该协议约定了和应用程序之间接口(参看PEP333,https://www.python.org/dev/peps/pep-0333/)
APP应用程序
- 遵从WSGI协议
- 本身是一个可调用对象
- 调用start_response,返回响应头部
- 返回包含正文的可迭代对象
WSGI请求environ处理
WSGI服务器程序会帮我们处理HTTP请求报文,但是提供的environ还是一个用起来不方便的字典
http://127.0.0.1:9999/python/index.html?id=1234&name=tom
('SERVER_PROTOCOL', 'HTTP/1.1')
('wsgi.url_scheme', 'http')
('HTTP_HOST', '127.0.0.1:9999')
('SERVER_PORT', '9999')
('REMOTE_ADDR', '127.0.0.1')
('REQUEST_METHOD', 'GET')
('CONTENT_TYPE', 'text/plain')
('PATH_INFO', '/python/index.html')
('QUERY_STRING', 'id=1234&name=tom')
('HTTP_USER_AGENT', 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/5.0 Chrome/55.0.2883.75 Safari/537.36')
QUERY_STRING 查询字符串的解析
WSGI服务器程序处理过HTTP报文后,返回一个字典,可以得到查询字符串('QUERY_STRING', 'id=1234&name=tom')。这个键值对用起来不方便。
- 编程序解析
# id=5&name=gloo
qstr = environ.get('QUERY_STRING')
print(qstr)
ifqstr:
for pair in qstr.split('&'):
k, _, v = pair.partition('=')
print("k={}, v={}".format(k,v))
# id=5&name=wayne
querystr = environ.get('QUERY_STRING')
if querystr:
querydict = {k:v for k,_,v in map(lambdaitem: item.partition('='), querystr.split('&'))}
print(querydict)
- 使用cgi模块
# id=5&name=gloo
qstr = environ.get('QUERY_STRING')
print(qstr)
print(parse_qs(qstr))
# {'name': ['gloo'], 'id': ['5']}
可以看到使用这个库,可以解析查询字符串,请注意value是列表,为什么?这是因为同一个key可以有多个值。
cgi模块过期了,建议使用urllib
- 使用urllib库
# http://127.0.0.1:9999/?id=5&name=gloo&age=&comment=1,a,c&age=19&age=20
qstr = environ.get('QUERY_STRING')
print(qstr)
print(parse.parse_qs(qstr)) # 字典
print(parse.parse_qsl(qstr)) # 二元组列表
# 运行结果
id=5&name=gloo&age=&comment=1,a,c&age=19&age=20
{'name': ['wayne'], 'age': ['19', '20'], 'id': ['5'], 'comment': ['1,a,c']}
[('id', '5'), ('name', 'wayne'), ('comment', '1,a,c'), ('age', '19'), ('age', '20')]
可以看到使用这个库,可以解析查询字符串,请注意value是列表,为什么?这是因为同一个key可以有多个值。cgi模块过期了,建议使用urllib3、使用urllib库parse_qs函数,将同一个名称的多值,保存在字典中,使用了列表保存。comment=1,a,c 这不是多值,这是一个值。age 是多值。
envrion的解析--webob库
环境数据有很多,都是存在字典中的,字典的存取方式没有对象的属性访问方便。使用第三方库webob,可以把环境数据的解析、封装成对象。
webob简介
Python下,可以对WSGI请求进行解析,并提供对响应进行高级封装的库。
pip install webob
官网文档docs.webob.org
webob.Request对象
将环境参数解析并封装成request对象
GET方法,发送的数据是URL中Query string,在Request Header中。request.GET就是一个字典MultiDict,里面就封装着查询字符串。
POST方法,"提交"的数据是放在Request Body里面,但是也可以同时使用Query String。request.POST可以获取Request Body中的数据,也是个字典MultiDict。
不关心什么方法提交,只关心数据,可以使用request.params,它里面是所有提交数据的封装。
request = webob.Request(environ)
print(request.headers) # 类字典容器print(request.method)
print(request.path)
print(request.query_string) # 查询字符串
print(request.GET) # GET方法的所有数据
print(request.POST) # POST方法的所有数据
print('params = {}'.format(request.params)) # 所有数据,参数
MultiDict
MultiDict允许一个key存了好几个值。
from webob.multidict import MultiDict
md = MultiDict()
md.add(1, 'magedu')
md.add(1, '.com')
md.add('a', 1)
md.add('a', 2)
md.add('b', '3')
md['b'] = '4'
for pair in md.items():
print(pair)
print(md.getall(1))
#print(md.getone('a')) # 只能有一个值
print(md.get('a')) # 返回一个值
print(md.get('c')) # 不会抛异常KeyError,返回None
webob.Response对象
res = webob.Response()
print(res.status)
print(res.headerlist)
start_response(res.status, res.headerlist)
# 返回可迭代对象
html = '<h1>马哥教育欢迎你</h1>'.encode("utf-8")
return [html]
如果一个Application是一个类的实例,可以实现__call__方法。
我们来看看webob.Response类的源代码
由此可以得到下面代码
def application(environ:dict, start_response):
# 请求处理
request = webob.Request(environ)
print(request.method)
print(request.path)
print(request.query_string)
print(request.GET)
print(request.POST)
print('params = {}'.format(request.params))
# 响应处理
res = webob.Response() # [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '0')]
res.status_code = 200# 默认200print(res.content_type)
html = "<h1>Hello I'm Gloo</h1>".encode("utf-8")
res.body = html
return res(environ, start_response)
webob.dec 装饰器
wsgify装饰器
文档
https://docs.pylonsproject.org/projects/webob/en/stable/api/dec.html
class webob.dec.wsgify(func=None, RequestClass=None, args=(), kwargs=None, middleware_wraps=None)
要求提供类似下面的可调用对象,以函数举例:
from webob.dec import wsgify
@wsgify
def app(request:webob.Request) -> webob.Response:
res = webob.Response("<h1>Hello I'm Gloo</h1>")
return res
wsgify装饰器装饰的函数应该具有一个参数,这个参数是webob.Request类型,是对字典environ的对象化后的实例。
返回值
可以是一个webob.Response类型实例
可以是一个bytes类型实例,它会被封装成webob.Response类型实例的body属性
可以是一个字符串类型实例,它会被转换成bytes类型实例,然后会被封装成webob.Response类型实例的body属性
总之,返回值会被封装成webob.Response类型实例返回
由此修改测试代码,如下
from webob import Response, Request
from webob.dec import wsgify
from wsgiref.simple_server import make_server
classApp:
@wsgify
def __call__(self, request:Request):
return "<h1>Hello I'm Gloo</h1>"
if __name__ == '__main__':
ip = '127.0.0.1'
port = 9999
server = make_server(ip, port, App())
try:
server.serve_forever() # server.handle_request() 一次
except KeyboardInterrupt:
server.shutdown()
server.server_close()