由于需要在公司内部分享wsgi
,看了 pep3333
文章以及pycon中关于 wsgi
的一些视频,对 wsgi
有了那么一点的了解,赶紧趁现在还有感觉的时候记录下来
假设一下,当我们没有 **Flask、Django **这些web框架使用,我们如何自己手动写一个简单服务。理一下思路 ,我们需要的步骤差不多如下:
代码如下:
import socket
def parse_http(http_text):
print(http_text)
request, *headers, _, body = http_text.split(‘\r\n‘)
method, path, protocol = request.split(‘ ‘)
headers = {}
for line in headers:
key, value = line.split(‘:‘, maxsplit=1)
headers[key] = value
return method, path, protocol, headers, body
def process_http_response(response):
list_ = (
‘HTTP/1.1 200 OK\r\n‘,
‘Content-Length: {}\r\n‘.format(len(response)),
‘Content-Type: text/plain\r\n‘,
‘\r\n‘,
‘{}\r\n‘.format(response),
‘\r\n‘,
)
return ‘‘.join(list_)
def view(http_request):
method, path, *_ = http_request
response = ‘%s %s‘ % (method, path)
return response
if __name__ == ‘__main__‘:
host = ‘127.0.0.1‘
port = 5000
with socket.socket() as s:
s.bind((host, port))
s.listen(1)
while True:
conn, addr = s.accept()
with conn:
http_text = conn.recv(2048).decode(‘utf-8‘) # 1. 通过socket获取http请求
http_request = parse_http(http_text) # 2. 解析http请求,获取请求方法、请问内容等
response = view(http_request) # 3. 调用具体的业务逻辑代码,返回结果
http_response = process_http_response(response) # 4. 拼装http响应
conn.sendall(http_response.encode(‘utf-8‘)) # 5. 发送http响应
运行下代码,通过 curl 打一下请求试一试
> curl 127.0.0.1:5000/hello
GET /hello
一切ok。
突然我们的同事小沈发现认为我们写的这个web服务器性能很棒,于是决定将它写的 Flask 程序接入我们的web服务器,结果发现我们的程序根本无法支持 Flask 程序 。但是小沈说为什么 Flask 可以被其他的python web服务器(gunicorn、uwsig)启动,为什么我们的不行
去谷歌找到了 pep333 文档,发现了 gunicron 和 Flask 等python web程序之间的py交易。决定加入他们。
WSGI 是 web服务器 以及 web框架之间规定的一个接口规定,他规定了两者之间如何进行数据传输。WSGI 的web应用/框架,不再需要局限于特定的一个web服务器,只要是遵守了WSGI协议的都可以。例如 Flask 程序可以被 gunicorn、uwsgi 甚至是 django内置的wsgi服务器启动。
在改造 web服务器 之前,我们首先认识一下wsgi application
来看一个最简单的 wsgi application
def application(environ, start_response):
""" 一个简单 wsgi application
:param environ: dict字典。包含 http信息(请求头、url、方法...)以及 wsgi规定的一些参数(wsgi.input...)
:param start_response: callable对象。用于发送 响应状态和响应头。接收 响应码、响应头、异常信息 三个参数
:return: iterable对象。bytes类型的可迭代对应,返回具体的印象内容
"""
status = ‘200 OK‘ # string类型。状态码和状态码信息用空格隔开
response_headers = [(‘Content-type‘, ‘text/plain‘)] # list类型。里面的元素都是(‘响应头的名称‘, ‘响应头的值‘)格式的元组
start_response(status, response_headers)
yield b‘hello‘
用 flask自带的wsgi 服务器启动我们的应用后,用 curl 请求下
from werkzeug.serving import run_simple
if __name__ == ‘__main__‘:
run_simple(‘127.0.0.1‘, 7000, application)
# 请求接收
> curl 127.0.0.1:7000
hello
知道了 wsgi application 是什么样子,就可以改造我们的web服务器了
server需要做以下的改变以适配 wsgi。
首先改造 parse_http
方法,我们不再返回一个list,而是一个dict字典。
def parse_http(http_text):
request, *headers, _, body = http_text.split(‘\r\n‘)
method, path, protocol = request.split(‘ ‘)
environ = {
‘REQUEST_METHOD‘: request,
‘PATH_INFO‘: path,
‘SERVER_PROTOCOL‘: protocol,
}
for line in headers:
key, value = line.split(‘:‘, maxsplit=1)
environ[‘HTTP_{}‘.format(key.upper())] = value
return environ
environ 的key必须符合 wsgi规定,不能够直接乱造。由于这里的只是demo,所以很多必须的key值都没有放,如果需要知道详细信息,可以自己去阅读 pep333。
接下来,我们需要提供一个 start_response 方法,用于发送 响应码和响应头。
headers_set = []
headers_sent = []
http_response = ""
def write(data):
global headers_sent, headers_set, http_response
if not headers_set:
raise AssertionError("write() before start_response()")
elif not headers_sent:
status, response_headers = headers_sent[:] = headers_set
http_response += ‘Status:%s\r\n‘ % status
for header in response_headers:
http_response += ‘%s: %s\r\n‘ % header
http_response += ‘\r\n‘
http_response += data.decode(‘utf-8‘)
def start_response(status, response_headers, exc_info=None):
headers_set[:] = [status, response_headers]
return write
这里用 headers_set 接收 响应头和响应码,是因为如果直接写入到 http_response 或者直接通过socket 返回到客户端。不利于其他中间件对 响应码和响应头 进行修改。
最后,用for循环解析application返回的iterable对象,并返回最终的结果
if __name__ == ‘__main__‘:
host = ‘127.0.0.1‘
port = 5001
with socket.socket() as s:
s.bind((host, port))
s.listen(1)
while True:
conn, addr = s.accept()
with conn:
http_text = conn.recv(2048).decode(‘utf-8‘) # 1. 通过socket获取http请求
environ = parse_http(http_text)
response = application(environ, start_response)
try:
for data in response:
if data:
write(data)
if not headers_sent:
write(‘\r\n‘)
conn.sendall(http_response.encode(‘utf-8‘))
# 清空 响应数据
http_response = ""
headers_sent = []
finally:
if hasattr(response, ‘close‘):
response.close()
application 就是我们写的wsgi application,当然你也可以替换成 flask app或者其他的wsgi application.
当我们的 应用需要支持 URL路由、CROS跨域、接口监控 等功能的时候,我们可以使用中间件。中间件很特殊,它即是application也是server,对于 application来说它是server,对于 server来说他是application。所以他既要满足 application,也要满足server的要求。
接下来我们实现一个支持 URL路由 的中间件。
class UrlRoutingMiddleware:
apis = dict()
def route(self, uri_path: str):
def decorator_route(func):
self.apis[uri_path] = func
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator_route
@staticmethod
def _execute_exc(environ, start_response):
path = environ[‘PATH_INFO‘]
start_response(‘200 OK‘, [(‘Content-type‘, ‘text/plain‘)])
result = (‘%s not found‘ % path).encode(‘utf-8‘)
yield result
def __call__(self, environ, start_response):
path = environ[‘PATH_INFO‘]
api = self.apis.get(path, self._execute_exc)
return api(environ, start_response)
mw = UrlRoutingMiddleware()
注意??:UrlRoutingMiddleware 并不是我们的中间件,他的实例 mw 才是我们需要的中间件。
我们可以用 mw.route
在 apis
添加的 url 和 wsgi application 的映射关系。输入不同的 url 时,可以调用不同的wsgi application;如果在 apis 没有找到对应 url,则最终调用 _execute_exec 这个方法(他同时也是一个 wsgi application)。
@mw.route(‘/a1‘)
def application1(environ, start_response):
pass
@mw.route(‘/a2‘)
def application1(environ, start_response):
pass
在server端,我们传入 mw 替代我们的application
wsgi 是同步的,并且他只能处理类似于 http 这样一输入一输出 的网络协议。所以 python3 的推出,协程被越来越多的使用,以及 websocket 等长连接的兴起,wsgi 显然已经更不上时代的发展。
asgi 是django团队一个新的python web服务的规范,它具有以下的特点:
目前,新出的框架以及服务基本都实现了 asgi,例如 fastapi、sanic 等 新的
原文:https://www.cnblogs.com/Baskershu/p/13905522.html