WSGI

image.png

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应用程序端

  1. 应用程序应该是一个可调用对象
    Python中应该是函数、类、实现了__call__方法的类的实例

  2. 这个可调用对象应该接收两个参数

# 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_INFOURL中的路径部分
QUERY_STRING查询字符串
SERVER_NAME, SERVER_PORT服务器名、端口
HTTP_HOST地址和端口
SERVER_PROTOCOL协议
HTTP_USER_AGENTUserAgent信息

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')。这个键值对用起来不方便。

  1. 编程序解析
# 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)
  1. 使用cgi模块
# id=5&name=gloo
qstr = environ.get('QUERY_STRING')
print(qstr)
print(parse_qs(qstr))
# {'name': ['gloo'], 'id': ['5']}

可以看到使用这个库,可以解析查询字符串,请注意value是列表,为什么?这是因为同一个key可以有多个值。

cgi模块过期了,建议使用urllib

  1. 使用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类的源代码
image.png

由此可以得到下面代码

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()