自定义web框架
知识内容:
1.web框架综述
2.自定义web框架独立功能实现
3.完整自定义web框架
参考:http://www.cnblogs.com/wupeiqi/articles/5237672.html
本节所有代码:https://github.com/15387062910/web_framework
一、web框架综述
1.什么是web框架
web框架:一组包能使开发者专注于网站应用业务逻辑开发,而无须处理网络应用底层的协议、线程、进程等方面
2.MVC架构
MVC把web应用系统分成3个基本部分:
- 模型model: 封装与应用程序的业务逻辑相关的数据及对数据的处理方法
- 试图view: 负责数据的显示和呈现
- 控制器controller: 负责从用户端收集用户输入
3.GET请求和POST请求
GET请求:
- 浏览器请求一个页面
- 搜索引擎检索关键字的时候
POST请求:浏览器向服务端提交数据,比如登录/注册等
4.web框架分类
python中的web框架可以这样分:
根据框架的组成来分:
- a\b\c都有 -> tornado
- [第三方a]\b\c -> django(a使用wsgiref)
- [第三方a]\b\[第三方c] -> flask
注: a、b、c分别指socket服务端、路由、模板渲染
根据框架的体系来分:
- django框架 -> 重武器(几乎提供了所有你想要的功能)
- 其他框架(很多功能要你自己去写)
python的常见web框架:
- django: 现在python最著名的web框架,企业级web框架,解决方案丰富,文档齐全,功能强大
- tornado: 强大的、支持协程、高效并发且可拓展的web服务器,tornado的强项在于可以利用它的异步协程机制开发高并发的服务器系统
- flask: 主要针对微小型web项目,灵活度高
- twisted: 适用于从传输层到自定义协议的所有类型的网络程序的开发,并能在不同的操作系统上提供很高的运行效率
二、自定义web框架独立功能实现
1.访问web网站的本质
以京东为例:
1 用户的浏览器(socket客户端) 2 3. 客户端往服务端发消息 3 4 6. 客户端接收消息 5 6 7. 关闭 7 8 9 JD的服务器(socket服务端) 10 1. 启动,监听 11 2. 等待客户端连接 12 13 4. 服务端收消息 14 5. 服务端回消息 15 16 7. 关闭 17
2.web服务端
(1)不完善的web服务端
1 import socket 2 3 # 生成socket实例对象 4 sk = socket.socket() 5 # 绑定IP和端口 6 sk.bind(("127.0.0.1", 8888)) 7 # 监听 8 sk.listen() 9 10 # 写一个死循环,一直等待客户端来连我 11 while 1: 12 # 获取与客户端的连接 13 conn, _ = sk.accept() 14 # 接收客户端发来消息 15 data = conn.recv(8096) 16 print(data) 17 # 给客户端回复消息 18 conn.send(b'<h1>hello!</h1>') 19 # 关闭 20 conn.close() 21 sk.close()
当在浏览器中输入http://127.0.0.1:8888/回车后会发现无法访问,程序输出并报错如下:
1 b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8888\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: csrftoken=Q3reVy5mLFbPgUMMbKozEIEvzaZaRFm7OiHNNmo0awgQ2pApoVJMy9SAv9aMR27S\r\n\r\n' 2 Traceback (most recent call last): 3 File "D:/wyb/python/oldboy/9.web框架(django,flask,tornado)/自定义web框架/不完善的web服务端.py", line 15, in <module> 4 conn, _ = sk.accept() 5 File "D:\python36\lib\socket.py", line 205, in accept 6 fd, addr = self._accept() 7 OSError: [WinError 10038] 在一个非套接字上尝试了一个操作。
原因是浏览器和服务器之间的通信要遵循HTTP协议(消息的格式请求),上述输出第一行即是HTTP协议中的请求内容,也就是说浏览器的请求是基于HTTP协议的,但是上面的web服务端并不认识这些请求
(2)完善的web服务端
若想让上面的web服务端返回的信息能在浏览器上显示,就要将返回的格式改成HTTP中响应的格式,具体实施如下所示:
1 import socket 2 3 # 生成socket实例对象 4 sk = socket.socket() 5 # 绑定IP和端口 6 sk.bind(("127.0.0.1", 8888)) 7 # 监听 8 sk.listen() 9 10 # 写一个死循环,一直等待客户端来连我 11 while 1: 12 # 获取与客户端的连接 13 conn, _ = sk.accept() 14 # 接收客户端发来消息 15 data = conn.recv(8096) 16 print(data) 17 # 给客户端回复消息 18 conn.send(b'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n') 19 # 想让浏览器在页面上显示出来的内容都是响应正文 20 conn.send(b'<h1>hello!</h1>') 21 # 关闭 22 conn.close() 23 sk.close()
运行上述程序,然后在浏览器中输入http://127.0.0.1:8888回车后显示如下:
(3)根据不同的路径返回不同的内容
普通版:
1 # __author__ = "wyb" 2 # date: 2018/5/15 3 import socket 4 5 # 生成socket实例对象 6 sk = socket.socket() 7 # 绑定IP和端口 8 sk.bind(("127.0.0.1", 8001)) 9 # 监听 10 sk.listen() 11 12 # 写一个死循环,一直等待客户端来连我 13 while 1: 14 # 获取与客户端的连接 15 conn, _ = sk.accept() 16 # 接收客户端发来消息 17 data = conn.recv(8096) 18 # 把收到的数据转成字符串类型 19 data_str = str(data, encoding="utf-8") 20 # print(data_str) 21 22 l1 = data_str.split("\r\n") # 用\r\n去切割上面的字符串 23 l2 = l1[0].split() # 按照空格切割上面的字符串 24 url = l2[1] # 取得url 25 # 给客户端回复消息 26 conn.send(b'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n') 27 28 # 根据不同的url返回不同的内容 29 if url == "/index": 30 response = b'<h1>hello welcome to index!</h1>' 31 print(response) 32 elif url == "/about": 33 response = b'<h1>write by wyb</h1>' 34 print(response) 35 else: 36 response = b'<h1>404! not found!</h1>' 37 print(response) 38 conn.send(response) 39 break 40 conn.send(response) 41 42 # 关闭连接但是不关闭服务器 43 conn.close()
函数版:
1 # __author__ = "wyb" 2 # date: 2018/5/16 3 import socket 4 5 # 生成socket实例对象 6 sk = socket.socket() 7 # 绑定IP和端口 8 sk.bind(("127.0.0.1", 8001)) 9 # 监听 10 sk.listen() 11 12 13 # 定义一个处理/index的函数 14 def index(now_url): 15 res = "hello, welcome to the %s" % now_url 16 return bytes(res, encoding="utf-8") 17 18 19 # 定义一个处理/about的函数 20 def about(now_url): 21 res = "hello, welcome to the %s" % now_url 22 return bytes(res, encoding="utf-8") 23 24 25 # 定义一个专门用来处理404的函数 26 def f404(cur_url): 27 ret = "你访问的这个{} 找不到".format(cur_url) 28 return bytes(ret, encoding="utf-8") 29 30 31 # 写一个死循环,一直等待客户端来连我 32 while 1: 33 # 获取与客户端的连接 34 conn, _ = sk.accept() 35 # 接收客户端发来消息 36 data = conn.recv(8096) 37 # 把收到的数据转成字符串类型 38 data_str = str(data, encoding="utf-8") 39 # print(data_str) 40 41 l1 = data_str.split("\r\n") # 用\r\n去切割上面的字符串 42 l2 = l1[0].split() # 按照空格切割上面的字符串 43 url = l2[1] # 取得url 44 # 给客户端回复消息 45 conn.send(b'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n') 46 47 # 根据不同的url返回不同的内容 48 if url == "/index": 49 response = index(url) 50 elif url == "/about": 51 response = about(url) 52 else: 53 response = f404(url) 54 conn.send(response) 55 break 56 conn.send(response) 57 58 # 关闭连接但是不关闭服务器 59 conn.close()
进阶函数版:
1 # __author__ = "wyb" 2 # date: 2018/5/16 3 # 将路由对应关系写在列表中,每次查路由对应的函数到这个列表中去查 4 # 就不用写if判断了,直接用url名字去找对应的函数名 5 import socket 6 7 # 生成socket实例对象 8 sk = socket.socket() 9 # 绑定IP和端口 10 sk.bind(("127.0.0.1", 8001)) 11 # 监听 12 sk.listen() 13 14 15 # 定义一个处理/index的函数 16 def index(cur_url): 17 ret = 'hello welcome to the {}'.format(cur_url) 18 return bytes(ret, encoding="utf-8") 19 20 21 # 定义一个处理/about的函数 22 def about(cur_url): 23 ret = 'hello welcome to the {}'.format(cur_url) 24 return bytes(ret, encoding="utf-8") 25 26 27 # 定义一个专门用来处理404的函数 28 def f404(cur_url): 29 ret = "你访问的这个{} 找不到".format(cur_url) 30 return bytes(ret, encoding="utf-8") 31 32 33 url_func = [ 34 ("/index", index), 35 ("/about", about), 36 ] 37 38 39 # 写一个死循环,一直等待客户端来连我 40 while 1: 41 # 获取与客户端的连接 42 conn, _ = sk.accept() 43 # 接收客户端发来消息 44 data = conn.recv(8096) 45 # 把收到的数据转成字符串类型 46 data_str = str(data, encoding="utf-8") 47 # print(data_str) 48 # 用\r\n去切割上面的字符串 49 l1 = data_str.split("\r\n") 50 # print(l1[0]) 51 # 按照空格切割上面的字符串 52 l2 = l1[0].split() 53 url = l2[1] 54 # 给客户端回复消息 55 conn.send(b'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n') 56 # 想让浏览器在页面上显示出来的内容都是响应正文 57 58 # 根据不同的url返回不同的内容 59 # 去url_func里面找对应关系 60 for i in url_func: 61 if i[0] == url: 62 func = i[1] 63 break 64 # 找不到对应关系就默认执行f404函数 65 else: 66 func = f404 67 # 拿到函数的执行结果 68 response = func(url) 69 # 将函数返回的结果发送给浏览器 70 conn.send(response) 71 # 关闭连接而不关闭服务器 72 conn.close()
(4)返回静态页面和动态页面
返回静态页面:
1 # __author__ = "wyb" 2 # date: 2018/5/17 3 # 将路由对应关系写在列表中,每次查路由对应的函数到这个列表中去查 4 # 就不用写if判断了,直接用url名字去找对应的函数名 5 import socket 6 7 # 生成socket实例对象 8 sk = socket.socket() 9 # 绑定IP和端口 10 sk.bind(("127.0.0.1", 8001)) 11 # 监听 12 sk.listen() 13 14 15 # 定义一个处理/index的函数 16 def index(): 17 with open("template/index.html", "rb") as f: 18 res = f.read() 19 return res 20 21 22 # 定义一个处理/about的函数 23 def about(): 24 with open("template/about.html", "rb") as f: 25 res = f.read() 26 return res 27 28 29 # 定义一个专门用来处理404的函数 30 def f404(): 31 with open("template/404.html", "rb") as f: 32 res = f.read() 33 return res 34 35 36 url_func = [ 37 ("/index", index), # 前面一个参数是路由 后面一个参数是处理路由的函数 38 ("/about", about), 39 ] 40 41 42 # 写一个死循环,一直等待客户端来连我 43 while 1: 44 # 获取与客户端的连接 45 conn, _ = sk.accept() 46 # 接收客户端发来消息 47 data = conn.recv(8096) 48 # 把收到的数据转成字符串类型 49 data_str = str(data, encoding="utf-8") 50 # print(data_str) 51 # 用\r\n去切割上面的字符串 52 l1 = data_str.split("\r\n") 53 # print(l1[0]) 54 # 按照空格切割上面的字符串 55 l2 = l1[0].split() 56 url = l2[1] 57 # 给客户端回复消息 58 conn.send(b'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n') 59 # 想让浏览器在页面上显示出来的内容都是响应正文 60 61 # 根据不同的url返回不同的内容 62 # 去url_func里面找对应关系 63 for i in url_func: 64 if i[0] == url: 65 func = i[1] 66 break 67 # 找不到对应关系就默认执行f404函数 68 else: 69 func = f404 70 # 拿到函数的执行结果 71 response = func() 72 # 将函数返回的结果发送给浏览器 73 conn.send(response) 74 # 关闭连接而不关闭服务器 75 conn.close()
返回动态页面:
1 # __author__ = "wyb" 2 # date: 2018/5/17 3 # 将路由对应关系写在列表中,每次查路由对应的函数到这个列表中去查 4 # 就不用写if判断了,直接用url名字去找对应的函数名 5 import socket 6 import time 7 import re 8 9 # 生成socket实例对象 10 sk = socket.socket() 11 # 绑定IP和端口 12 sk.bind(("127.0.0.1", 8001)) 13 # 监听 14 sk.listen() 15 16 17 # 定义一个处理/index的函数 18 def index(): 19 with open("template/index.html", "rb") as f: 20 res = f.read() 21 pattern = "<p></p>" 22 res2 = re.sub(pattern, str(time.strftime("%Y-%m-%d %H:%M:%S")), str(res, encoding="utf-8")) # 替换内容 23 return bytes(res2, encoding="utf-8") 24 25 26 # 定义一个处理/about的函数 27 def about(): 28 with open("template/about.html", "rb") as f: 29 res = f.read() 30 return res 31 32 33 # 定义一个专门用来处理404的函数 34 def f404(): 35 with open("template/404.html", "rb") as f: 36 res = f.read() 37 return res 38 39 40 # 路由对应关系 41 url_func = [ 42 ("/index", index), # 前面一个参数是路由 后面一个参数是处理路由的函数 43 ("/about", about), 44 ] 45 46 47 # 写一个死循环,一直等待客户端来连我 48 while 1: 49 # 获取与客户端的连接 50 conn, _ = sk.accept() 51 # 接收客户端发来消息 52 data = conn.recv(8096) 53 # 把收到的数据转成字符串类型 54 data_str = str(data, encoding="utf-8") 55 # print(data_str) 56 # 用\r\n去切割上面的字符串 57 l1 = data_str.split("\r\n") 58 # print(l1[0]) 59 # 按照空格切割上面的字符串 60 l2 = l1[0].split() 61 url = l2[1] 62 # 给客户端回复消息 63 conn.send(b'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n') 64 # 想让浏览器在页面上显示出来的内容都是响应正文 65 66 # 根据不同的url返回不同的内容 67 # 去url_func里面找对应关系 68 for i in url_func: 69 if i[0] == url: 70 func = i[1] 71 break 72 # 找不到对应关系就默认执行f404函数 73 else: 74 func = f404 75 # 拿到函数的执行结果 76 response = func() 77 # 将函数返回的结果发送给浏览器 78 conn.send(response) 79 # 关闭连接而不关闭服务器 80 conn.close()
(5)wsgiref版web框架
wsgiref是什么?
WSGI(Web Server Common Interface)是专门为Python语言制定的web服务器与应用程序之间的网关接口规范,通俗的来说,只要一个服务器拥有一个实现了WSGI标准规范的模块(例如apache的mod_wsgi模块),那么任意的实现了WSGI规范的应用程序都能与它进行交互。因此,WSGI也主要分为两个程序部分:服务器部分和应用程序部分。
wsgiref则是官方给出的一个实现了WSGI标准用于演示用的简单Python内置库,它实现了一个简单的WSGI Server和WSGI
Application(在simple_server模块中),主要分为五个模块:simple_server, util, headers,
handlers, validate。
wsgiref源码地址:https://pypi.python.org/pypi/wsgiref
wsgiref版web框架实现:
1 # __author__ = "wyb" 2 # date: 2018/5/17 3 import time 4 import re 5 from wsgiref.simple_server import make_server 6 7 # 将返回不同的内容部分封装成函数 8 def index(): 9 with open("template/index.html", "rb") as f: 10 res = f.read() 11 pattern = "<p></p>" 12 res2 = re.sub(pattern, str(time.strftime("%Y-%m-%d %H:%M:%S")), str(res, encoding="utf-8")) # 替换内容 13 return bytes(res2, encoding="utf-8") 14 15 def about(): 16 with open("template/about.html", "r", encoding="utf8") as f: 17 s = f.read() 18 return bytes(s, encoding="utf8") 19 20 # 定义一个专门用来处理404的函数 21 def f404(): 22 with open("template/404.html", "rb") as f: 23 res = f.read() 24 return res 25 26 # 定义一个url和实际要执行的函数的对应关系 27 list1 = [ 28 ("/index", index), 29 ("/about", about), 30 ] 31 32 def run_server(environ, start_response): 33 start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 设置HTTP响应的状态码和头信息 34 url = environ['PATH_INFO'] # 取到用户输入的url 35 func = None 36 for i in list1: 37 if i[0] == url: 38 func = i[1] 39 break 40 if func: 41 response = func() 42 else: 43 response = f404() 44 return [response, ] 45 46 if __name__ == '__main__': 47 httpd = make_server('127.0.0.1', 8001, run_server) # 启服务(监听端口然后执行run_server函数) 48 httpd.serve_forever() # 让服务一直启动
(6)jinja2版web框架
jinja2是什么?
jinja2是Flask作者开发的一个模板系统,起初是仿django模板的一个模板引擎,为Flask提供模板支持,由于其灵活,快速和安全等优点被广泛使用
jinja2详细教程:https://www.cnblogs.com/dachenzi/p/8242713.html
jinja2版web框架基础实现:
1 # __author__ = "wyb" 2 # date: 2018/5/17 3 from wsgiref.simple_server import make_server 4 from jinja2 import Template 5 6 7 def index(): 8 with open("template/jinja2_test.html", "r", encoding="utf-8") as f: 9 data = f.read() 10 template = Template(data) # 生成模板文件 11 ret = template.render({"name": "wyb", "hobby_list": ["写代码", "骑车"]}) # 把数据填充到模板中 12 return [bytes(ret, encoding="utf-8"), ] 13 14 15 def about(): 16 with open("template/about.html", "rb") as f: 17 data = f.read() 18 return [data, ] 19 20 21 def home(): 22 with open("template/home.html", "rb") as f: 23 data = f.read() 24 return [data, ] 25 26 27 # 定义一个url和函数的对应关系 28 URL_LIST = [ 29 ("/index", index), 30 ("/about", about), 31 ("/home", home), 32 ] 33 34 35 def run_server(environ, start_response): 36 start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 设置HTTP响应的状态码和头信息 37 url = environ['PATH_INFO'] # 取到用户输入的url 38 func = None # 将要执行的函数 39 for i in URL_LIST: 40 if i[0] == url: 41 func = i[1] # 去之前定义好的url列表里找url应该执行的函数 42 break 43 if func: # 如果能找到要执行的函数 44 return func() # 返回函数的执行结果 45 else: 46 return [bytes("404没有该页面", encoding="utf8"), ] 47 48 49 if __name__ == '__main__': 50 httpd = make_server('', 8001, run_server) # 启动服务 51 print("Serving HTTP on port 8001...") 52 httpd.serve_forever() # 让服务一直运行
jinja2版web框架进阶实现(数据库):
1 # __author__ = "wyb" 2 # date: 2018/5/17 3 # 进阶: 使用MYSQL数据库 4 from wsgiref.simple_server import make_server 5 from jinja2 import Template 6 7 def index(): 8 with open("template/jinja2_test2.html", "r", encoding="utf-8") as f: 9 data = f.read() 10 template = Template(data) # 生成模板文件 11 # 从数据库中取数据 12 import pymysql 13 14 conn = pymysql.connect( 15 host="127.0.0.1", 16 port=3306, 17 user="root", 18 password="root", 19 database="test", 20 charset="utf8", 21 ) 22 cursor = conn.cursor(cursor=pymysql.cursors.DictCursor) 23 cursor.execute("select * from userinfo;") 24 user_list = cursor.fetchall() 25 # print(user_list) 26 # 实现字符串的替换 27 ret = template.render({"user_list": user_list}) # 把数据填充到模板里面 28 return [bytes(ret, encoding="utf8"), ] 29 30 def about(): 31 with open("template/about.html", "rb") as f: 32 data = f.read() 33 return [data, ] 34 35 def home(): 36 with open("template/home.html", "rb") as f: 37 data = f.read() 38 return [data, ] 39 40 # 定义一个url和函数的对应关系 41 URL_LIST = [ 42 ("/index", index), 43 ("/about", about), 44 ("/home", home), 45 ] 46 47 def run_server(environ, start_response): 48 start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 设置HTTP响应的状态码和头信息 49 url = environ['PATH_INFO'] # 取到用户输入的url 50 func = None # 将要执行的函数 51 for i in URL_LIST: 52 if i[0] == url: 53 func = i[1] # 去之前定义好的url列表里找url应该执行的函数 54 break 55 if func: # 如果能找到要执行的函数 56 return func() # 返回函数的执行结果 57 else: 58 return [bytes("404没有该页面", encoding="utf8"), ] 59 60 61 if __name__ == '__main__': 62 httpd = make_server('', 8001, run_server) # 启动服务 63 print("Serving HTTP on port 8001...") 64 httpd.serve_forever() # 让服务一直运行
3.web框架总结
(1)关于动态网页:
动态网页本质就是字符串的替换,但是该替换都是在服务器上完成然后再将替换完的数据返回给浏览器
(2)关于web本质:
web框架的本质说简单点就是socket服务端与浏览器之间的通信
(3)socket服务端功能总结:
a. 负责与浏览器收发消息(socket通信) --> wsgiref/uWsgi/gunicorn...
b. 根据用户访问不同的路径执行不同的函数
c. 从HTML读取出内容,并且完成字符串的替换 --> jinja2(模板语言)
(4)python中web框架的分类:
按上面三个功能划分:
- 框架自带a,b,c --> Tornado
- 框架自带b和c,使用第三方的a --> Django
- 框架自带b,使用第三方的a和c --> Flask
按另一个维度来划分:
- Django --> 大而全(你做一个网站能用到的它都有)
- 其他 --> Flask 轻量级
三、完整自定义web框架
用原生python实现web框架:https://github.com/15387062910/web_framework
项目:todo程序
实现功能:用户登录注册(密码未加密)、todo程序:一条信息的发布删除修改和用户所有的信息展示、admin程序:管理员用户可以看到所有用户的信息(id username password)、未使用数据库,数据直接存储在文件中
项目实行代码:https://github.com/15387062910/web_framework/tree/master/todo%E7%A8%8B%E5%BA%8F