一、web应用框架简介及手撸web框架
软件开发架构详细:网络编程之网络架构及其趋势 - Xiao0101 - 博客园 (cnblogs.com)
软件开发架构分为两种:
- c/s架构:客户端软件(client)—服务端软件(server)
- b/s架构 :浏览器(Browser)------服务端软件(server)
总结:BS本质上也是CS架构
1、web应用程序是什么?
WEB应用程序一般是B/S模式。Web应用程序首先是“应用程序”,和用标准的程序语言,如C、C++等编写出来的程序没有什么本质上的不同。然而Web应用程序又有自己独特的地方,就是它是基于Web的,而不是采用传统方法运行的。换句话说,它是典型的浏览器/服务器架构的产物。
本质上:浏览器是一个socket客户端,服务器是一个socket服务端
2、web框架
(1)知识回顾
HTTP协议详细:前端基础之HTTP协议介绍 - Xiao0101 - 博客园 (cnblogs.com)
(2)web框架介绍
Web框架(Web framework)是一种开发框架,用来支持动态网站、网络应用和网络服务的开发。这大多数的web框架提供了一套开发和部署网站的方式,也为web行为提供了一套通用的方法。web框架已经实现了很多功能,开发人员使用框架提供的方法并且完成自己的业务逻辑,就能快速开发web应用了。浏览器和服务器的是基于HTTP协议进行通信的。也可以说web框架就是在以上十几行代码基础张扩展出来的,有很多简单方便使用的方法,大大提高了开发的效率。
(3)web框架的本质
所有的Web应用其实就是一个socket服务端, 而用户使用的浏览器就是一个socket客户端程序, 明白了Web框架的本质, 我们就可以实现自己的Web框架了。
(4)根据之前学的知识形成的自定义web框架
import socket
server = socket.socket() # 默认就是基于网络的TCP协议
server.bind(("127.0.0.1",8080))
server.listen(5)
while 1:
conn,addr = server.accept()
data = conn.recv(1024)
print(data) # 将请求打印出来
conn.send(b"HTTP?1.1 200 OK\r\n\nOur destiny is our own!")
conn.close()
将服务端运行,客户端访问服务端,运行结果如下:
HTTP协议规定了让大家发送消息、接收消息的时候有个格式依据以后浏览器发送请求信息也好,服务器回复响应信息也罢,都要按照这个规则来。
pycharm输出结果如下:
# 请求首行
b'GET / HTTP/1.1\r\n
# 请求头(都是一大堆的K:V键值对)
Host: 127.0.0.1:8080\r\n
Connection: keep-alive\r\n
Cache-Control: max-age=0\r\n
sec-ch-ua: "Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"\r\n
sec-ch-ua-mobile: ?0\r\n
sec-ch-ua-platform: "Windows"\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/122.0.0.0 Safari/537.36\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\n
Sec-Fetch-Site: none\r\n
Sec-Fetch-Mode: navigate\r\nSec-Fetch-User: ?1\r\n
Sec-Fetch-Dest: document\r\nAccept-Encoding: gzip, deflate, br, zstd\r\n
Accept-Language: zh-CN,zh;q=0.9\r\n
Cookie: csrftoken=9qUCCe8NhifmyhgtznpwF9dtBc3Qi3mreGFBPup3hNpwtSNDaGdzpvQACQxUX8Je; sessionid=3o16gvni7tw8kvq01ydx0pwaunvt8r6m\r\n\
# 换行
r\n
'
# 请求体
b'GET /favicon.ico HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
Connection: keep-alive\r\n
sec-ch-ua: "Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"\r\n
sec-ch-ua-mobile: ?0\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\r\n
sec-ch-ua-platform: "Windows"\r\n
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\n
Sec-Fetch-Site: same-origin\r\n
Sec-Fetch-Mode: no-cors\r\n
Sec-Fetch-Dest: image\r\nReferer: http://127.0.0.1:8080/\r\n
Accept-Encoding: gzip, deflate, br, zstd\r\n
Accept-Language: zh-CN,zh;q=0.9\r\n
Cookie: csrftoken=9qUCCe8NhifmyhgtznpwF9dtBc3Qi3mreGFBPup3hNpwtSNDaGdzpvQACQxUX8Je; sessionid=3o16gvni7tw8kvq01ydx0pwaunvt8r6m\r\n\r\n'
可以说web服务的本质都是基于这简单的套接字程序扩展出来的。
(5)根据不同的路径返回不同的内容
通过以上,我们是实现了一个简易版的web框架
但是存在以下问题:
在用户访问不同网页时候,如何让我们的Web服务根据用户请求的URL不同而返回不同的内容呢?
解决方案:
-
HTTP请求数据
-
/favicon.ico
直接忽略 不影响判断 -
利用字符串切割和索引取值获取相应数据
其实很简单,我们可以从请求相关数据里面拿到请求URL的路径,然后拿路径做一个判断…
'''
根据不同的URL返回不同的内容
'''
import socket
server = socket.socket() # 默认就是TCP协议
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn, addr = server.accept() # 三次四次挥手
data = conn.recv(1024)
res = data.decode('utf8')
conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 请求首行,请求头,空行
path = res.split(' ')[1] # 字符串切割获取地址
if path == '/index': # 判断地址
# conn.send(b'index') # 1.如果判断成功则发送请求体
with open(r'1.html','rb') as f: # 2.或者打开文件一内容作为请求体发送
data = f.read()
conn.send(data)
elif path == '/login': # 1.如果判断为login
conn.send(b'login') # 2.就发送b'login'的请求体
else:
conn.send(b'404 error') # 没匹配到则返回404
conn.close()
1.html
内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<h1 style="color: #31b0d5">嗨,朋友!你来了,你好啊!</h1>
</body>
</html>
存在的问题
- 如果网址路径很多,服务端代码重复(因为if…else…会变得非常多。)
- 手动处理http数据格式过于繁琐,并且只能拿到url后缀,其他数据获取繁琐
- 并发的问题
服务器和应用程序
对于真实开发中的python web程序来说,一般会分为两部分:服务器程序和应用程序。
服务器程序负责对socket服务器进行封装,并在请求到来时,对请求的各种数据进行整理。
应用程序则负责具体的逻辑处理。为了方便应用程序的开发,就出现了众多的Web框架,例如:Django、Flask、web.py 等。不同的框架有不同的开发方式,但是无论如何,开发出的应用程序都要和服务器程序配合,才能为用户提供服务。
这样,服务器程序就需要为不同的框架提供不同的支持。这样混乱的局面无论对于服务器还是框架,都是不好的。对服务器来说,需要支持各种不同框架,对框架来说,只有支持它的服务器才能被开发出的应用使用。
这时候,标准化就变得尤为重要。我们可以设立一个标准,只要服务器程序支持这个标准,框架也支持这个标准,那么他们就可以配合使用。一旦标准确定,双方各自实现。这样,服务器可以支持更多支持标准的框架,框架也可以使用更多支持标准的服务器。
WSGI是Python Web应用程序和Web服务器之间的一种标准接口,它定义了一个简单而通用的接口,使得不同的Web应用程序框架(如Flask、Django等)可以与不同的Web服务器(如Apache、Nginx等)进行交互。
常用的WSGI服务器有uwsgi、Gunicorn。而Python标准库提供的独立WSGI服务器叫wsgiref,Django开发环境用的就是这个模块来做服务器。
二、基于 wsgiref 模块搭建web框架
1、wsgiref 模块介绍
wsgiref
模块是Python标准库中的一个模块,用于实现WSGI(Web Server Gateway Interface)规范。
wsgiref
模块提供了一些工具和类,帮助开发者快速构建符合WSGI规范的应用程序和服务器。主要功能包括:
- WSGI应用程序开发:
wsgiref
模块提供了一些类和函数,帮助开发者编写符合WSGI规范的应用程序。开发者可以定义一个WSGI应用程序,接收HTTP请求,并返回HTTP响应。 - WSGI服务器实现:
wsgiref
模块还提供了一个简单的WSGI服务器实现,可以用于在开发和测试阶段运行WSGI应用程序。这样开发者可以在本地快速搭建一个简单的Web服务器来运行和测试他们的应用程序。 - 辅助功能: 除了WSGI应用程序和服务器之外,
wsgiref
模块还提供了一些辅助函数和类,用于处理HTTP请求、构建HTTP响应等操作。
2、使用wsgiref 模块的好处
-
wsgiref模块帮助我们封装了socket 代码
-
帮我们处理 http 格式的数据
-
请求来的时候帮助你自动拆分http格式数据并封装成非常方便处理的数据格式(类似于字典)
-
响应走的时候帮你将数据再打包成符合http格式的数据
3、实现代码
from wsgiref.simple_server import make_server
# 以函数形式定义功能,扩展方便
def index_func(request):
return 'index'
def login_func(request):
return 'login'
def error(request):
return '404 Not found'
# 地址与功能的对应关系
urls = [
('/index', index_func),
('/login', login_func)
]
def run_server(request, response):
"""
函数名定义什么都无所谓,这里我们使用run_server
:param request:请求相关的所有数据,一个类似字典的形式,"PATH_INFO"正好就是我们要找的地址
wsgiref模块帮我们处理好HTTP格式的数据,封装成了字典让你更加方便的操作
:param response:响应相关的所有数据
:return:返回给浏览器的数据,返回格式必须是'return [二进制格式的数据]' 这种样式
"""
response('200 OK', []) # 响应首行, 响应头
current_path = request.get("PATH_INFO") # 找到路径
func = None # 定义一个变量, 存储匹配到的函数名
for url in urls:
if current_path == url[0]:
func = url[1] # 如果匹配到了则将函数名赋值给func
break # 匹配之后立刻结束循环
if func: # 然后判断一下func是否被赋值了(也就是是否匹配到了)
data = func(request) # 执行函数拿到结果,request可有可无,但放进去以后好扩展
else:
data = error(request)
return [data.encode('utf-8')]
if __name__ == '__main__':
server = make_server('127.0.0.1', 8080, run_server) # 一旦被访问将会交给run_server处理
'''
会实时监听127.0.0.1:8080地址,只要客户端来了
都会交给run函数处理(加括号触发run函数的运行)
flask启动源码
make_server('127.0.0.1',8080,obj)
__call__
'''
server.serve_forever() # 启动服务端并一直运行
产生的问题
- 网址很多的情况下如何匹配
- 网址多匹配如何解决
- 功能复杂代码块如何解决
看起来上面的代码还是要挨个写if判断,怎么办?
三、封装处理优化
1、根据功能划分模块
- 根据功能的不同拆分成不同的py文件
(1)views.py
- 存储路由与函数对应关系
# 功能函数
def register(request):
return 'register'
def login(request):
return 'login'
def index(request):
return 'index'
def error(request):
with open(r'templates/error.html', 'r', encoding='utf8') as f:
return f.read()
(2)urls.py
- 存放路径与功能的对应关系
from views import *
# 后缀匹配
urls = (
('/register', register),
('/login', login),
('/index', index),
)
(3)server.py
- 存储启动及分配代码
from wsgiref.simple_server import make_server
from urls import urls
from views import *
def run_server(request,response):
"""
:param request:请求相关的所有数据,一个类似字典的形式,"PATH_INFO"正好就是我们要找的地址
:param response:响应相关的所有数据
:return:
"""
response('200 OK',[]) # 响应首行, 响应头
current_path = request.get("PATH_INFO") # 找到路径
func = None # 定义一个变量, 存储匹配到的函数名
for url in urls:
if current_path == url[0]:
func = url[1] # 如果匹配到了则将函数名赋值给func
break # 匹配之后立刻结束循环
if func: # 然后判断一下func是否被赋值了(也就是是否匹配到了)
data = func(request) # 执行函数拿到结果,request可有可无,但放进去以后好扩展
else:
data = error(request)
return [data.encode('utf-8')]
if __name__ == '__main__':
server = make_server('127.0.0.1', 8080, run_server) # 一旦被访问将会交给run_server处理
server.serve_forever() # 启动服务端并一直运行
总结:
拆分后好处在于要想新增一个功能,只需要在views.py中编写函数,urls.py添加对应关系即可
2、模板文件与静态文件
(1)templates文件夹
- 存储html文件
(2)static文件夹
- 存储html页面所需静态资源
四、返回动静态页面
1、静态网页
- 页面上的数据是直接写死的,万年不变
- 如果不想仅仅返回几个字符串, 而是想给浏览器返回完整的HTML内容, 对此我们只需要通过 open 打开 HTML文件将内容读出来再发送给浏览器就行了
# 静态网页制作
def login_func(request):
with open(r"./login.html", "r", encoding="utf-8")as f:
res = f.read() # 打开文件读出内容,再返回文件内容
return res
2、动态网页
- 数据是实时获取的
- 后端获取当前时间展示到html页面上
- 数据是从数据库中获取的展示到html页面上
# 动态网页制作
def get_time(env):
current_time = datetime.datetime.now().strftime('%Y-%m-%d %X')
# 如何将后端获取到数据"传递"给html文件?
with open(r'templates/03 mytime.html', 'r', encoding='utf8') as f:
data = f.read()
# 得到的data就是一堆字符串
data = data.replace('fwefwef', current_time) # 在后端将html页面处理好之后再返回给前端
return data
3、练习
将一个字典传递给html文件,并且可以在文件上方便快捷的操作字典数据
def get_dict(env):
user_dic = {'username': 'xiao', 'age': 18}
with open(r'templates/04 get_dict.html','r',encoding='utf8') as f:
data = f.read()
tmp = Template(data)
res = tmp.render(user=user_dic)
# 给get_dict.html传递了一个值,页面上通过变量名user就能够拿到user_dict
return res
五、模版语法之Jinja2
举例演示模版语法与之前的后端到前端传输局方式的不同
1、原始方式
- 页面展示当前时间
(1)后端
def get_time(request):
# 1.获取当前时间
import time
c_time = time.strftime('%Y-%m-%d %X')
# 2.读取html文件
with open(r'templates/get_time.html','r',encoding='utf8') as f:
data = f.read()
# 3.思考:如何给字符串添加一些额外的字符串数据>>>:字符串替换
new_data = data.replace('random_str',c_time)
return new_data
(2)前端
<h1>展示后端获取的时间数据</h1>
<span>random_str</span>
2、jinja2模板语法
(1)下载安装
- 第三方模块需要先下载后使用
pip3 install jinja2
(2)功能和语法
① 功能
支持将数据传递到html页面并提供近似于后端的处理方式简单快捷的操作数据
② 语法
变量插值: 使用双大括号{{ }}
来插入变量值到模板中。
Hello, {{ name }}!
表达式: 可以在{{ }}
中使用表达式。
{{ 2 + 2 }}
**过滤器:**使用管道符|
应用过滤器对变量进行处理。
{{ name|capitalize }}
控制结构: 使用{% %}
来表示控制结构,如条件语句和循环。
{% if condition %}
Content to show if condition is true.
{% endif %}
{% for item in items %}
{{ item }}
{% endfor %}
模板继承: 使用{% extends %}
和{% block %}
来实现模板继承和重写。
{% extends "base.html" %}
{% block content %}
Content specific to this template.
{% endblock %}
宏(Macro): 定义可重用的代码块。
{% macro input(name, value='') %}
<input type="text" name="{{ name }}" value="{{ value }}">
{% endmacro %}
(3)views.py
from jinja2 import Template
def get_dict(request):
user_dict = {'name': 'xiao', 'pwd': 123, 'hobby': 'read'}
new_list = [11, 22, 33, 44, 55, 66]
with open(r'templates/get_dict.html', 'r', encoding='utf8') as f:
data = f.read()
temp_obj = Template(data)
res = temp_obj.render({'user':user_dict,'new_list':new_list})
return res
(4)templates
- –get_dict.html
<h1>字典数据展示</h1>
<p>{{ user }}</p>
<p>{{ user.name }}</p>
<p>{{ user['pwd'] }}</p>
<p>{{ user.get('hobby') }}</p>
<h1>列表数据展示</h1>
<p>
{% for i in new_list%}
<span>元素:{{ i }}</span>
{% endfor %}
</p>
pip install jinja2
"""模版语法是在前端起作用的,后端不支持"""
# 模版语法
{{ user }}
{{ user.get('username') }}
{{ user.age }}
{{ user['hobby'] }}
{% for user_dict in user_list %}
<tr>
<td>{{ user_dict.id}}</td>
<td>{{ user_dict.username}}</td>
<td>{{ user_dict.password}}</td>
<td>{{ user_dict.hobby}}</td>
</tr>
{% endfor %}
六、自定义简易版本web框架请求流程图
流程图流程
浏览器客户端
wsgiref模块
请求来:处理浏览器请求,解析浏览器HTTP格式的数据,封装成大字典(PATH_INFO中存放的用户访问资源的路径)
响应去:将数据打包成符合HTTP格式,在返回给浏览器
后端:
urls.py:找出用户输入的路径有么有与视图层的对应关系,如果有则取到views.py找对应的视图函数。
view.py:
功能1(静态):视图函数找templates中的html文件,返回给wsgiref做HTTP格式的封装处理,再返回给浏览器.
功能2(动态):视图函数通过pymysql链接数据库, 通过jinja2模板语法将数据库中取出的数据在tmpelates文件夹下的html文件做一个数据的动态渲染, 最后返回给wsgiref做HTTP格式的封包处理, 再返回给浏览器.
功能3(动态):也可以通过jinja2模板语法对tmpelates文件夹下的html文件进行数据的动态渲染, 渲染完毕, 再经过wsgiref做HTTP格式的封包处理, 再返回给浏览器.
templates:html文件
数据库
七、总结
1、urls.py
- 后缀与函数名对应关系
- (‘/index’,register)
- 后缀专业名词称之为’路由’
- 函数名专业名词称之为’视图函数’
- urls.py专业名词称之为’路由层’
2、views.py
- 专门编写业务逻辑代码
- 可以是函数 也可以是类
- 函数专业名词称之为’视图函数’
- 类专业名词称之为’视图类’
- views.py专业名词称之为’视图层’
3、templates文件夹
- 专门存储html文件
- html文件专业名词称之为’模板文件’
- templates文件夹专业名词称之为’模板层’
4、static文件夹
- 专门存储静态文件资源
- 页面所需css文件、js文件、图片文件、第三方文件可统称为’静态资源’