基于 Python 的 Web 框架
HTTP 基础
Http 协议是一个给予请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式
1. HTTP 请求
http请求头由三部分组成:请求方法,消息报头,请求正文
请求方法的作用是定义客户端(浏览器)获取资源的方式,服务器收到不同请求会进行不同处理,返回不同结果给客户端。最常见的请求方法:
GET 请求: 请求获取request-URL所标识的资源
POST 请求:在request-URL所标识的资源后附加新的数据
POST 请求 相对 GET 请求附加的参数隐藏在消息报头中, GET 请求 则是附着在 URL 链接中一起发到服务器
2. HTTP 响应
响应消息由三个部分组成:状态码、消息报头、响应正文
常见状态码类别:
1xx:提示消息,表示请求接收,等待处理
2xx:连接成功,表示请求已被成功接收、理解、接手
3xx:重定向,表示要完成请求必须进一步的操作
4xx:客户端错误
5xx:服务端错误
常见状态码
200: 客户端请求成功
400: 客户端请求有语法错误,不能被服务器接收
401: 非法请求,无法授权
403: 服务器收到请求,但是拒绝提供服务
404: 请求资源不存在
500: 服务器发生错误
503: 服务器当前不能处理客户端请求
3. 消息报头
HTTP 消息 由 客户端到服务器 的请求和 服务器到客户端 的响应的组成部分
请求报头:客户端向服务端传递请求的附加信息以及客户端自身的信息
Accept:用于指定客户端接受哪些类型的信息
Accept-Charset:用于指定客户端接受的字符集
Accept-Encoding:用于指定可以接受的内容编码
Accept-Language:用于指定自然语言
Authorization:用于证明客户端有权查看某个资源
Host:用于指定被请求资源的主机和端口号
User-Agent:告诉服务端发出请求的客户端的操作系统,浏览器和其他属性
Cookie: 记录会话信息,通常用于标识身份
响应报头
Location:用于指定重定向的位置
Server:服务器的软件信息
Set-Cookie:通知浏览器设置 Cookie
Content-Disposition:控制浏览的资源如何展示,比如可以指定为下载,并且可以指定下载的文件名
WSGI(Web Server Gate Interface)
WSGI 是 Web 服务器 与框架交互的桥梁接口,负责在它们之间实现 HTTP 请求 体 和 HTTP 响应 体的传输。
Web 服务器 无法直接解释 Python 脚本, WSGI 负责把请求封装成 Python 可解释的数据结构,调度 Python 应用 进行处理,再让 Python 应用 根据一定的格式规则封装成一个响应体, WSGI 模块解释过后再把它返回给服务器,最后服务器再返回给客户端。
Werkzeug 模块可用来实现 Web框架入口 ,其内部的 run_simple 最后是用了 socket 实现了一个 TCP 隧道,数据交互遵守 HTTP 协议,并对客户端发过来的数据解析后封装为 Request,然后就到达我们的 WSGI 入口传给框架了,Response 则是 Request 的逆向思路实现。
实现 URL 路由追踪
1. 设计流程
从请求中判断客户端想获取的URL资源,然后再到对应的函数中获取处理结果。需要先建立映射关系,实际开发中不仅有路由,视图,还存在静态资源(图片,CSS、JS 文件)和未知类型的扩展,所以需要再把处理函数封装为一个有类型标识的数据结构。
2. 定义处理逻辑ExecFunc数据结构
需要三个成员:执行函数func、附带参数**options、结构类型func_type
3. 建立映射关系
需要三个映射关系
绑定 URL 与处理函数的 endpoint: url_map
绑定 endpoint 与处理函数结构体即 ExecFunc 的实例: function_map
绑定静态资源文件内容与 静态资源 URL: static_map
当请求传来一个url的时候,会先通过url_rule找到endpoint(url_map),然后再根据endpoint再找到对应的view_func(view_functions)。
为了低耦合度和模块化,我们给处理函数结构命名。开发中就可以先写好模块,需要时直接替换与 URL 绑定的endpoint 中的模块来达到更改处理逻辑,提高模块的复用性,endpoint名通常都和视图函数名一样。
4. 实现规则绑定函数add_url_rule
当 URL 和静态类型以外的 节点 名都没有被绑定时,先绑定 URL 与 节点,再绑定 节点 与 处理函数对象
5. 实现静态资源规则绑定函数与静态资源路由
dispatch_static选招匹配的URL并返回对应类型和文件内容封装成的响应体,找不到返回404。然后在启动函数 run 中把静态资源相关的节点函数命名为 static, 且绑定处理方法为 dispatch_static
6. 实现 URL 路由追踪函数dispatch_request
路由的本质就是要找到 URL 对应的处理函数,负责分析请求的 URL,找到对应节点名,再找到对应的 处理函数对象
7. 路由装饰器
实现 MVC 设计模式的支持
MVC 设计模式
Model-View-Controller(模型-视图-控制器)模式,用于应用程序的分层开发
Model:模型代表一个存取数据的对象,可以带有逻辑,在数据变化时更新控制器
View:视图代表模型包含的数据的可视化
Controller:控制器作用于模型和视图之上,它控制数据流向模型对象,在数据变化时更新视图,使视图与模型分离开
通常的开发流程,首先由产品设计用户交互功能,定稿最初版本需求,在最初版本完成的期间内,通常是不会允许直接对需求进行更改替换而是在下一个版本在迭代更新,这么做是为了防止一个项目产品因为需求一直变更,比如追加和删减,导致开发者开发周期变相被迫延长出现没有产出的假象。但是现实终究是残酷的,除非在非常正规的企业中,否则需求变更是一个无时不刻都在发生的事情,而作为一个开发者,我们为了应对这种情况,就需要在开发之初先设计好项目整体架构,尽可能的降低每个功能模块之间的联系甚至做到完全独立开来,以此应对频繁变化的需求。
1. 视图--逻辑处理
视图通过实现 add_url_rule 接口把规则绑定到框架,所以视图也需要发送参数: url, func 和 func_type 到 add_url_rule 方法中来添加视图规则。 add_view 方法的 endpoint 参数即是视图对象中处理函数的名字,又要作为规则绑定的节点名
2. 控制器—规则绑定
Controller(name,url_map)两个参数,节点名和自己的 url_map ,用户直接在控制器 url_map 内更新 url 数据,然后通过 load_Controller 遍历将其绑定到框架上的url_map,实现控制器对模型的数据更新
MVC 范式开发实战
在 main.py 同级目录下创建 core文件夹,生成base_view.py
定义class BaseView(VIew) 继承 View 基类并实现 dispatch_request 类,内部对 Get与 Post 两种请求方法做好路由,作为应用开发时的底层库,通过 BaseView,之后开发只要继承它,并实现对应的 get 或者 post 方法就行。
定义 syl_url_map 做好 URL 与 视图 的映射并把它初始化在控制器中,最后用框架的 load_controller 加载以下控制器
一个找了很久的Bug
MVC写好了之后,运行测试,/test 页面下显示错误,无法获得请求,终端显示如下
dispatch_request() missing 1 required positional argument: 'request'
最后发现
obj=func.view_class() # 实例化 obj 时少写了(),导致参数无法传入
return obj.dispoatch_request()
实现置换型模版引擎
置换型模版引擎就是对模版文件内容中特殊标记的地方,用对应的内容数据进行替换。
1. 定义模版标记:pattern = r'{{(.*?)}}'
2. 用正则匹配出所有模版标记的解析函数parse_args
3. 置换函数replace_template
逻辑是先读取模版文件内容,再找出所有标记进行内容替换。函数内部的 path 由应用本身的 template_folder 与指定路径结合而成
实现 HTTP 会话维持
如何对无状态的HTTP协议实现用户身份识别,做到从无状态到有状态实现交互会话维持
Cookie
客户端在每一次发起请求时,把自己的身份信息存放在 Cookie 中并附加在请求报头里一起发送,服务端收到请求之后再从 Cookie 中把这些信息读出来以此判断具体是哪一个用户,实现通信双方的交互。
Cookie的弊端:用户信息保存在客户端,Cookie本身的存储量限制在4KB左右,太大太复杂的信息无法发送,且每一次都需要附带在请求发起,过大的请求报文变相的增加了流量。Cookie中存放的数据是一个键值对,假设有一个服务端的,假设有一个服务端的判断用户身份是通过 Cookie 中的 User 这个键的值来识别用户是谁,这个值的设置通常情况下是在登录验证通过之后,返回给客户端让客户端保存,之后发起请求附带进去就行,但是这里面其实会有一些安全隐患存在的,比如有恶意用户通过修改这个 User键的值,让服务端误以为是另一个用户,返回了这个用户的信息造成泄漏,所以后来又出现了一种把信息保存在服务端的技术,也就是 Session。
Session
与 Cookie 的本质区别就是信息保存放由客户端变成服务端,这样不仅减少了流量,还大大的提高了越权的门槛。它依旧会让客户端设置 Cookie,但不同于存储用户信息等数据,它现在只会存放一个 标识,通常是一个 Session ID,一串无意义的字符串,客户端第一次与服务端连接时,会收到一个设置这个 标识 的相应报头,之后每一次发起请求附带这个 标识 就可以让服务端进行身份判断了。
说完客户端的流程,我们再来看看服务端是如何工作的。首先是在本地建立一个映射关系,里面关联用户数据与这个唯一的 标识。这个映射最开始是空的,当服务端收到一个没有附带 Session ID 或者这个 Session ID 不存在于服务端的映射关系中的请求时,会生成一个唯一 标识,添加到这个映射关系中,当然因为是第一次访问,所以这个刚生成的 标识 关联的数据是空的,然后把这个 标识 返回给客户端设置到 Cookie 中,用户下次发起请求时,这个 标识 也附带在报文中,服务端收到请求后,验证用户是否合法,如果不合法,通常会返回验证页面给用户进行权限验证,反之则返回真正请求的数据,这就是 Session 的工作流程。
Web框架中的Cookie操作
1. 从请求中读取Cookie
实现Session也需要操作Cookie,Cookie存放在请求报头中,而Web框架的请求报头是由WSGI返回,WSGI 把请求传给了框架的 dispatch_request 方法,Cookie 在框架内部是由 dispatch_request 方法的 request 参数中读取:
cookies = request.cookies # 获取cookie,返回的值是一个字典
2. 于响应体中设置 Cookie
在响应报头里添加 Set-Cookie 属性可以让客户端保存 Cookie,如果客户端没有Session ID即分配一个
会话维持的实现
1. 为客户端生成会话的唯一标识
import base64
import time
# 创建 Session ID
def create_session_id():
# 首先获取当前时间戳,转换为字符串,编码为字节流,在 Base64 编码,在解码为字符串,然后去掉 Base64 编码会出现的“=”号,取到倒数第二位,最后再进行倒序排列
return base64.encodebytes(str(time.time()).encode()).decode().replace("=", '')[:-2][::-1]
2. 记录对应客户的Session值
老方法需求分析,首先明确Session数据的组成,它由两个字典嵌套组成,第一个字典是存放Session ID与数据块的映射,第二个字典就是数据块,里面也存放键值对,方便我们对Session的管理操作,既然是一个映射表,就需要由一个表作为成员变量,一个添加关联的方法,一个对关联的的数据进行添加的方法,又因为在实际交互中存在比如登出这种需要从Cookie中抹除数据的操作,所以还需要对Session ID关联的数据提供删除方法
3. Session 的本地缓存
为了减少服务器压力,设计为__某__一个会话的记录发生变化时保存一次。在本地单独把每一个Session会话从__session_map__中取出来分别保存到__storage_path__.而记录发生变化也就是当进行push/pop操作的时候,触发存储self.storage(session_id)
单例模式
单例类只能有一个实例
单例类必须自己创建自己的唯一实例
单例类必须给所有其他对象提供这一实例
主要解决:一个全局使用的类频繁地创建与销毁
何时使用:当你想控制实例数目,节省系统资源的时候
如何解决:判断系统是否已有这个单例,如果有则返回,没有则创建
关键代码:构造函数是私有的
4. Session 的启动加载
遍历会话记录存放目录session_path_list下的是所有文件(会话),文件名Session ID,内容是会话记录。遍历后添加到会话映射表__session_map__
load_local_session在框架run方法执行前调用,在框架初始化方法中绑定会话记录默认存放目录,在run方法中,先判断会话存放目录self.session_path是否存在,再设置session.set_storage_path(self.session_path),最后调用session.load_local_session()
Session 常用功能封装
1. 获取当前会话
__session_map__ 定义为私有成员,要实现一个获取当前会话内容的方法:
def map(self, request):
return self.__session_map__.get(get_session_id(request), {})
2. 从当前会话中获取某个数据项,取出指定键的值
def get(self, request, item):
return self.__session_map__.get(get_session_id(request), {}).get(item, None)
3. 校验装饰器
AuthSession 包含了三个方法,一个装饰器类方法和两个静态方法,auth_session 判断 auth_logic 中的验证是否通过,若通过则执行被装饰函数(视图)的处理逻辑,反之则执行 auth_fail_callback,通常这里面会是一个登录操作,验证是否登录成功,不成功在 auth_fail_callback 中再次重定向到登录页面中
实战会话维持
1. 实现会话视图基类
定义了一个继承Authsession的类AuthLogin,验证失败后返回登录页面
定义了一个继承BaseView的类SessionView,
类的 dispatch_request 方法前加了 AuthLogin.auth_session 装饰器,每次通过验证才会执行返回的内容
2. 使用会话视图基类实现需要验证的视图
用 SessionView 来实现需要权限验证的首页和登出页,用 BaseView 来实现不需要验证的登录页,定义在main.py下
实现URL重定向
在 HTTP 响应体中定义好 Location 参数,客户端如果收到一个 状态码 为 3xx 类型的响应包,并且报头附带有 Location 参数,客户端会马上跳转到这个参数里面对应的 URL
1. URL重定向模块
要实现重定向,只需要返回一个附带 Location 参数的响应包,把要跳转的 URL 放进去,定义在框架主体文件中。
再在框架主体的dispatch_request添加返回值判断,是Respond(重定向返回的响应包),则直接返回。
2. 实战重定向用法
修改base_view.py下AuthLogin 的 auth_fail_callback:return redirect("/login")以及main.py 下的 Login 和 Logout 视图
数据与模版分离
**为了让服务器实现返回的结果可以在每一个平台上都可以使用。**需要让数据与模板分离,服务端只返回数据,对应平台的客户端获取数据后自行解析,再把数据加工加载后返回给用户。另一方面,方便开发,即前后端分离,只需要协定好数据接口和数据格式。
要实现数据模板分离,首先先约定好数据的返回 URL,通常称为 API 接口,而 数据格式 通常情况下会选用 JSON 作为返回的数据格式。
实现 JSON 数据格式化
HTTP 协议 的响应报头中有一个 Content-Type 参数,当返回的响应体报头的 Content-Type 是 application/json 时,客户端就可以识别出这段内容为 JSON 数据。
1. 实现 JSON 模块
封装内容为 JSON 数据的响应体的模块 render_json ,逻辑和 URL 重定向 的 redirect 思路差不多,都是封装一个独特的响应包来实现,包括文件下载——服务器返回给客户端文件去下载的功能,也是通过封装一个独特的响应包来实现。
2. 实战一个返回 JSON 数据的接口
在main.py 中添加一个 API 视图,该视图的 get 方法使用 render_json 函数返回 JSON 数据,并且在 syl_url_map 中关联视图与 URL
实现基于 HTTP 协议的文件下载
要实现让客户端下载,就需要有一个通知,客户端收到这个通知标识,对响应内容就不再去展示,而是保存下来。HTTP 协议 提供了 Content-Disposition 参数,可以指定内容为附件类型:
Content-Disposition: attachment; filename="文件名"
在框架主体定义文件下载模块比通常返回的响应体只多了 Content-Disposition 参数
MySQL 数据库简介
数据�集合分为 库 -> 表 -> 行 -> 列。通常情况下,一个项目有一个对应的 库来存放项目相关数据,这些数据用多张 表 分成不同的结构保存,每一 行 和 列 就形成了 表 的结构�,而 关系型 就体现在 表与 表 之间有一定的关联。
结构化查询语言 SQL
1. 数据库的创建与删除
CREATE DATABASE 数据库名 DEFAULT CHARACTER SET utf8;
DROP DATABASE 数据库名;
USE 数据库名;
2. 数据表的创建与删除
假设我们要创建一张有以下三个字段的表
姓名
年龄
性别
为了给每一行的数据添加唯一的标识供查询做索引,还需要一个唯一标识字段,通常为一个约束了主键和自增的整型字段,起始值为整型 1
CREATE TABLE 表名 (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT "唯一标识,约束主键和自增",
f_name VARCHAR(50) COMMENT "姓名",
f_age TINTINT COMMENT "年龄",
f_gender VARCHAR(10) COMMENT "性别"
) CHARSET=utf8;
3. 插入数据
INSERT INTO 表名(f_name,f_age,f_gender) VALUES('shiyanlou001','21','性别未知')
4. 查询数据
查询表中所有数据
SELECT * FROM 表名;
查询表中指定范围数据,比如1到10行的数据
SELECT * FROM 表名 LIMIT 1, 10;
查询表中 id 小于 10 的纪录,也就是前 10 行
SELECT * FROM 表名 WHERE id < 10;
查询表中性别为未知性别,年龄小于 22 的数据
SELECT * FROM 表名 WHERE f_gender = "未知性别" AND f_age < 22;
查询表中性别为男或者性别为女并且年龄大于 20 的数据
SELECT * FROM 表名 WHERE f_gender = "男" OR f_gender = "女" AND f_age > 20;
5. 删除数据
删除所有数据
DELETE FROM 表名;
删除指定条件的数据
DELETE FROM 表明 WHERE CONDITION_1 = CAUSE_1 AND CONDITION_2 = CAUSE_2, ...;
6. 修改数据
修改每一行数据的性别为未知性别
UPDATE 表名 SET f_gender = "未知性别";
修改指定条件成立的数据性别为未知性别
UPDATE 表名 SET f_gender = "未知性别" WHERE CONDITION_1 = CAUSE_1 AND CONDITION_2 = CAUSE_2, ...;
实现 MySQL 数据库连接模块
环境部署:
打开本机mysql服务
sudo service mysql start
安装 PyMySQL 模块
sudo pip3 install pymysql
使用 Pymysql 实现数据库连接模块
1. 需求分析
在实际开发中,有这些必须操作:数据库连接、SQL 语句执行、存储过程调用和增删改查,而 Pymysql 在进行数据库操作中,如果出现异常那么程序就会抛出异常,所以为了防止应用因为一个细节而导致后续的业务逻辑都发生错误就要捕获这些异常,在需要的时候甚至作为反馈信息展示给用户,那么就要有一个数据结构来存储查询过程产生的各种数据供业务逻辑去判断。
2. 封装数据结果
首先我们需要有一个状态来判断数据库连接模块本次操作是否成功,需要有一个变量来存放成功执行的查询结果,需要一个存储异常信息的地方,需要一个变量存放操作影响的条目数,这个影响条目数量如果是针对查询操作的话,那就是返回查询出来的数据的总行数,那么它的大致结构如下:
suc
result
error
rows
除了四个变量,还添加三个操作查询结果的方法,第一个是根据指定的下标返回对应的数据,后面两个都是在它的基础上实现的获取第一条数据或者最后一条数据。
而为了方便数据库模块的捕获异常实现,要实现一个异常捕获的装饰器DBResult.handler。要求装饰的 func 函数返回两个值,分别是执行影响的行数和执行结果,所以下一步我们实现数据库模块的时候,对于被这个装饰器装饰的操作方法都需要返回两个值。
最后就是为了方便调试,还实现了一个 to_dict 方法,返回四个属性组成的字典
实现数据库模块
1. 模块设计
根据实际开发中的常用操作,可以把对数据库的操作抽象为各种方法,然后为被 DBResult.handler 装饰器装饰的方法确保有返回影响行数和结果就行。
2. 数据库连接与断开
数据库连接就是从 PyMySQL 模块中获取数据库连接对象,即把对应的参数传进去 pymysql.connect 方法中获取一个连接对象。
3. SQL 语句的执行
数据查询方法的流程是首先从数据库连接中获取输入SQL的环境,然后把要执行的语句传入输入环境中执行,再根据 DBResult 封装结果进行返回.
with 语法操作上下文,对实现了 __enter__ 和 __exit__ 方法的对象,从开始执行之前到结束执行之后,会自己做一些逻辑封装放在这两个方法中,而 pymysql.connect 返回一个 Connection 对象,这个对象的 __enter__ 返回了一个游标,而在对数据库进行增删改的时候,需要调用 commit 方法提交改动,在改动过程中如果出现错误,则需要回滚数据库到改动之前,这又是一个为什么用 with 语法的原因了,因为 __exit__ 方法里面会自动判断操作操作成功与否来执行 commit 提交更新或者 rollback回滚到这次执行之前
4. 获取最新插入数据 ID
insert 方法的逻辑,就是在内部调用一下 execute 方法,再判断成功与否封装 INSERT ID 到 DBResult 对象的 result 属性中
5. 存储过程调用方法
存储过程是一些列的数据库操作封装在一起的逻辑块,可以理解为一个函数。存储过程的名字�就是�函数名,所以也会需要参数传递,在 PyMySQL 中提供了一个 callproc 方法调用存储过程
6. 选择、创建与删除数据库
创建和删除数据库,就是提前把对应的 SQL 语句封装好;选择数据库则是调用了 PyMySQL 的 select_db 方法加上异常捕获装饰器
实战数据库模块
模拟实际开发中与数据库交互的注册与登录功能
1. 创建数据库和表
实现首次使用初始化数据库的逻辑就是知道第一次连接数据库肯定会因为数据库不存在抛出异常,然后捕获这个异常判断是否真的是因为数据库不存在才触发的之后,开始创建数据库和表,下一次连接由于数据存在就不会再跑到异常处理代码块中了。
2. 实现一个登录页面和注册页面共用模版
3. 实现注册功能
即把用户提交的信息保存在数据库里
4. 实现登录功能
把原先的Login视图里用户POST过来的数据到数据库查看有没有匹配。如果有放到Session中,没有返回错误信息和登录页面。
实现异常处理
服务端的异常有多种,在这个框架中可能出现的异常包括 URL 不存在,静态资源不存,请求方法不支持,业务逻辑中出现错误等,异常处理模块就是针对框架中可能会触发的错误进行捕获然后处理
实现异常处理模块
1. 定义常见的异常
异常基类 SYLFkException
异常基类是所有异常的父类,是为框架所抛出的异常做个分类,这样在捕获异常时,通过只捕获 SYLFkException 来过滤出框架自己抛出的异常而不需要使用 Exception 这个所有异常的父类了,方便我们自定义异常的处理。SYLFkException 有两个成员,分别是 code 和 message,异常编号和异常信息,继承它的异常通过实现不同的 code 和 message 来对不同异常进行识别并做对应的处理,编号为空的异常EndpointExistsError\URLExistsError都是对框架启动或者正常工作有致命逻辑错误的,所以只能抛出它让开发者自己在代码层中进行修复。
文件不存在异常FileNotExistsError
权限不足异常 RequireReadPermissionError
不支持的请求方法异常 InvalidRequestMethodError :当客户端发向服务端的某个 URL 发起了一个该 URL 不支持的请求方法时抛出
URL 未找到PageNotFoundError
URL 未知处理类型UnknownFuncError
2. 异常捕获处理
异常捕获的思路跟数据库模块的 DBResult中的 capture 装饰器思路是一样的,就是实现一个异常捕获装饰器,把被装饰的方法放在内部执行,捕获执行过程中的异常,不过这次多了一个捕获之后的处理
3. 自定义异常处理逻辑
4. 组合到框架中