详解Python web框架到底是怎么来的?
前言
咱都知道软件开发的架构有两种,分别是C/S架构与B/S架构,本质上都是借助socket实现网络通信,因此Django作为一个web框架本质上也是一个socket服务端,浏览器则是客户端,我们可以自己实现简易的web框架来更好的理解Django。
Web框架推导
在介绍socket时,我们可以自定义一个socket服务端,这个socket服务端是最最原始的web框架,web服务的本质是基于下述代码扩展出来的。
import socket
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn, addr = server.accept()
data = conn.recv(1024)
print(data)
conn.send(b'web frame')
conn.close()
但是使用上述代码开启服务端,在浏览器上访问127.0.0.1:80
返回的结果是发送的相应无效,原因就是上述socket服务端向客户端发送的数据格式不符合HTTP协议的要求,如果想让客户端浏览器收到服务端发送的数据,就要遵循HTTP协议,因此服务端在向客户端发送数据的时候必须按照HTTP协议的规则加上相应状态行。
# 符合http协议的socket服务端
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
conn, addr = server.accept()
data = conn.recv(1024)
print(data)
# 响应状态行
conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
conn.send(b'web frame')
conn.close()
上述代码就是web框架的本质,虽然简陋但是可以对上述代码进行装饰。在使用浏览器上网的时候,访问不同的网址就会返回不同的页面或者是内容,根据已有的代码就可以通过if判断
实现客户端访问不同的url返回不同的数据或者页面。因此如何获取用户访问的url是什么就是需要解决的问题了,可以将浏览器向服务端发送请求时的数据打印出来进行分析,比如浏览器请求的url如下127.0.0.1:8080/index
得到的data
数据如下所示,发现第一行开始就可以看到/index
,因此可以使用代码获取客户端访问的url:
'''
b'GET /index HTTP/1.1\r\n # \r\n表示换行
Host: 127.0.0.1:8080\r\n
Connection: keep-alive\r\n
Cache-Control: max-age=0\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\n
Sec-Fetch-Site: cross-site\r\n
Sec-Fetch-Mode: navigate\r\n
Sec-Fetch-User: ?1\r\n
Sec-Fetch-Dest: document\r\n
Accept-Encoding: gzip, deflate, br\r\n
Accept-Language: zh-CN,zh;q=0.9\r\n\r\n'
'''
# if判断实现根据不同的url返回不同的数据或页面
import socket
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn,addr = server.accept()
data = conn.recv(1024)
print(data)
data_list = data.decode('utf-8').split(' ') # 将数据以空格切分
print(data_list)
current_path = data_list[1] # 列表中的第二个元素就是路由
conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
if current_path == '/index':
conn.send(b'index')
elif current_path == '/login':
conn.send(b'login')
else:
conn.send(b'web frame')
conn.close()
使用if判断
虽然解决了访问不同url返回不同数据的需求,但是如果有非常多的路径要怎么办嘞?难道一个一个的挨个判断吗?显然不现实,所以可以采用函数与列表结合实现更简洁的实现不同url返回不同数据的需求。
import socket
def index(url):
s = 'this is {}'.format(url)
return bytes(s, encoding='utf-8')
def login(url):
s = 'this is {}'.format(url)
return bytes(s, encoding='utf-8')
# 定义一个url与函数功能的对应关系的列表
url_list = [
('/index', index),
('/login', login)
]
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
conn, addr = server.accept()
data = conn.recv(1024)
print(data)
data_list = data.decode('utf-8').split(' ')
print(data_list)
current_path = data_list[1]
conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
# 定义一个变量名用以保存函数的内存地址
func = None
for i in url_list:
if i[0] == current_path:
func = i[1]
break
if func:
res = func(current_path)
else:
res = b'404 not found'
conn.send(res)
conn.close()
访问不同的url时,服务端不仅可以返回不同的数据也可以返回HTML页面:
import socket
import datetime
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
def index(url):
current_time = datetime.datetime.now().strftime('%Y-%m-%d %X')
with open('time.time.html','r',encoding='utf-8') as f:
data = f.read()
# 在网页上定义好特殊符号,用字符串方法替换
s = data.replace('sfsd',current_time)
return bytes(s, encoding='utf-8')
def login(url):
with open('myhtml.html','r',encoding='utf-8') as f:
res = f.read()
return bytes(res,encoding='utf-8')
# 定义一个url与函数功能的对应关系的列表
url_list = [
# (路由,视图函数)
('/index',index),
('/login',login)
]
while True:
conn,addr = server.accept()
data = conn.recv(1024)
print(data)
data_list = data.decode('utf-8').split(' ')
print(data_list)
current_path = data_list[1]
conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
# 定义一个变量名用以保存函数的内存地址
func = None
for i in url_list:
if i[0] == current_path:
func = i[1]
break
if func:
res = func(current_path)
else:
res = b'404 not found'
conn.send(res)
conn.close()
通过上述的web框架推导过程已经可以实现客户端浏览器访问不同的url服务端能够返回不同的数据或者页面了,但是,重点来了,但是代码重复,每个开发web服务端的人都需要重新洗这段代码,并且是手动处理的http格式的数据,并且还只能拿到url后缀,还有就是并发的问题,说这么多并不是说上述代码推导过程没用,上述代码推导只是让小伙伴们更加理解web框架的本质,而提到的问题可以借助其他模块解决即wsgiref模块
。
wsgiref模块
借助wsgiref模块对http格式的数据进行处理时会方便很多,首先看一下该模块的基本用法:
from wsgiref.simple_server import make_server
def run(env,response):
'''
:param env: 请求相关的所有数据
:param response: 响应相关的所有数据
:return: 返回给浏览器的数据,return [b'']
'''
# 响应首行 响应头
response('200 OK', [])
# env就是字典格式的HTTP数据
print(env)
current_path = env.get('PATH_INFO') # 获取客户端浏览器请求的路由
# wsgiref模块帮你处理号HTTP格式数据,封装成成字典
if current_path == '/index':
return [b'index']
elif current_path == '/login':
return [b'welcome']
else:
return [b'404 error']
if __name__ == '__main__':
server = make_server('127.0.0.1',8080,run)
# 实时监听127.0.0.1:8080地址,只要有客户端来了都会交给run函数处理,即触发run函数的运行
server.serve_forever() # 启动服务端
借助wsgiref模块对http数据的处理,可以将web服务端的代码进行修改:
from wsgiref.simple_server import make_server
import datetime
# 将env将参数传给视图函数后,视图函数就可以根据浏览器发送给服务端的数据完成各种事件
def index(env):
with open(r'template/myhtml.html','r',encoding='utf-8') as f:
res = f.read()
return res
def longin(env):
current_time = datetime.datetime.now().strftime('%Y-%m-%d %X')
with open(r'template/time.time.html','r',encoding='utf-8') as f:
res = f.read()
res = res.replace('sfsd',current_time)
return res
urls = [
('/index',index),
('/loging',longin)
]
def run(env,response):
'''
:param env: 请求相关的所有数据
:param response: 响应相关的所有数据
:return: 返回给浏览器的数据,return [b'']
'''
# 响应首行 响应头
response('200 OK', [])
# env就是字典格式的HTTP数据
print(env)
current_path = env.get('PATH_INFO')
# wsgiref模块帮你处理号HTTP格式数据,封装成成字典
func = None
for i in url_list:
if current_path == i[0]:
func = i[1]
break
if func:
res = func(env)
else:
res = '404 not found'
return [res.encode()]
if __name__ == '__main__':
server = make_server('127.0.0.1',8080,run)
# 实时监听127.0.0.1:8080地址,只要有客户端来了都会交给run函数处理,即触发run函数的运行
server.serve_forever() # 启动服务端
看来看去上述代码还可以再改改,代码全部放在一个py文件中是不合理的,后期功能如果增多的话代码的管理或者功能的扩展都会变得困难,因此需要分文件,可以按照下述方式进行分文件,不同的文件中放不同的代码:
urls.py 路由与视图函数对应关系
views.py 视图函数的逻辑(后端业务逻辑)
templates文件夹 专门存储html文件
sever.py 服务端文件
具体代码如下:
# sever.py
from wsgiref.simple_server import make_server
from urls import urls
from views import *
def run(env,response):
'''
:param env: 请求相关的所有数据
:param response: 响应相关的所有数据
:return: 返回给浏览器的数据,return [b'']
'''
# 响应首行 响应头
response('200 OK', [])
# env就是字典格式的HTTP数据
print(env)
current_path = env.get('PATH_INFO')
# wsgiref模块帮你处理号HTTP格式数据,封装成成字典
func = None
for i in urls:
if current_path == i[0]:
func = i[1]
break
if func:
res = func(env)
else:
res = '404 not found'
return [res.encode()]
if __name__ == '__main__':
server = make_server('127.0.0.1',8080,run)
# 实时监听127.0.0.1:8080地址,只要有客户端来了都会交给run函数处理,即触发run函数的运行
server.serve_forever() # 启动服务端
# urls.py
import views
urls = [
('/index',views.index),
('/loging',views.longin)
]
# views.py
import datetime
def index(env):
with open(r'template/myhtml.html','r',encoding='utf-8') as f:
res = f.read()
return res
def longin(env):
current_time = datetime.datetime.now().strftime('%Y-%m-%d %X')
with open(r'template/time.time.html','r',encoding='utf-8') as f:
res = f.read()
res = res.replace('sfsd',current_time)
return res
总结
上文对web服务端是如何出现的进行了简单的推导,在实际开发中无需使用上述代码进行开发,使用现有的web框架比如django或者flask就可以完成,这两种框架都是基于socket服务端并且符合http协议的web框架,介绍上述推导过程希望可以帮助自己对web请求的理解。