模拟django写一个简单的web框架

Django 是一个开放源代码的 Web 应用框架,由 Python 写成。Django的大名,了解python的都知道,它让我们开发web的流程变得极为简单,当然这也少不了越来越多的人为其开发的第三方包的支持,让我们无论开发什么样的网站系统,它几乎都能应对,但是问句扎心的,你了解它低层如何工作的吗?

今天来做一个简单的web框架,让大家对网上流行的网站框架低层是如何运行的。

我们知道http就是基于tcp的,客户端浏览器对服务器的一次请求和服务器对客户端浏览器的响应就是就是一次socket的发送和接收,只是http发送一次接收一次就关闭了tcp的连接。

web框架1.0

在之前的文章带你徒手撸一个web服务器程序中我们已经写好了一个socket服务端,我们今天就在此基础上进行扩展开发。

网站目录

1
2
3
4
5
6
7
main.py									socket服务端,程序启动文件
framework.py 动态web框架,用来处理请求和响应
urls.py 路由器
views.py 视图处理器
templates 模板目录
├─index.html 首页模板
├─error.html 错误页模板

暂定以上文件,接下来我们一一说明各个文件

模板文件

模板文件没啥说的,中间{}中间的就代表数据动态替换吧。

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="zh-hans">
<head>
<meta charset="UTF-8">
<title>就是首页</title>
<style>
h1{
color: #f00;
}
</style>
</head>
<body>
<h1>{% body %}</h1>
</body>
</html>

error.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="zh-hans">
<head>
<meta charset="UTF-8">
<title>ERROR</title>
<style>
h1{
color: #f00;
text-align: center;
}
</style>
</head>
<body>
<h1>{% body %}</h1>
</body>
</html>

视图文件

文件名views.py,里面只定义了两个简单的视图方法首页和错误页面,接收一个request,里面封装的是用户请求新的的一个字典,包含请求方法,请求路径,请求头和请求体,后面可以根据请求方法自己定义视图处理方式,错误页面方法接受了一个消息参数,用来替换模板中的提示语。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2020/11/24 15:34
# @Author : 托小尼
# @Email : 646547989@qq.com
# @URI : https://www.diandian100.cn
# @File : views.py

def index(request):
with open("templates/index.html") as f:
body = f.read()

return body.replace("{% body %}", '使用视图函数做的操作,我是首页')


def error(request, msg):
with open("templates/error.html") as f:
body = f.read()

return body.replace("{% body %}", msg)

路由文件

文件名:urls.py,路由我这里也之定义了一条,index页面路由

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2020/11/24 15:34
# @Author : 托小尼
# @Email : 646547989@qq.com
# @URI : https://www.diandian100.cn
# @File : urls.py
import views
route = [
("/", views.index),
]

接下来就是最关键的两个文件,请求动态处理和socket服务端。

请求动态处理文件

文件名:framework.py,该文件用来格式化请求信息和响应信息,以及分发路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2020/11/20 17:29
# @Author : 托小尼
# @Email : 646547989@qq.com
# @URI : https://www.diandian100.cn
# @File : framework.py
from urls import route
from views import error


class WebService(object):
"""动态web框架"""
def __init__(self, request):
# 格式化请求信息
self.request = self.format_request(request)

@staticmethod
def format_response(status_code, response_body):
"""格式化返回信息(响应状态码和响应体)"""
# http响应行
response_row = "HTTP/1.1 %s %s\r\n" % (status_code, "IM OK")
# 随便定义一些http响应头
header = dict()
header["Server"] = "Hello Server"
header["author"] = "Tony 于"
# 格式化响应头
response_header = ""
# 将字典格式的响应头信息格式化为拼接字符串
for key, value in header.items():
response_header += "%s: %s\r\n" % (key, value)

# 完整响应信息
return ("%s%s\r\n%s" % (response_row, response_header, response_body)).encode('utf8')

def handle_request(self):
"""请求操作,转发给路由对应的函数去处理(路由分发)"""
# 判断是否是http请求,返回空代表非法请求
if not self.request:
return self.format_response(403, error(self.request, "非法请求"))
# 循环已定义路由,找不到对应的路由交给错误页面处理
for path, view in route:
# 如果请求路径存在路由中,就交给该条路由对应的视图函数去执行
if self.request.get('path') == path:
return self.format_response(200, view(self.request))
else:
return self.format_response(404, error(self.request, '您访问的页面不存在'))

def format_request(self, request):
"""格式化请求信息"""
# 用来存放用户请求
recv = dict()
try:
# 分离http协议请求行与请求头请求体
recv_row, recv_info = request.decode('utf8').split('\r\n', maxsplit=1)
# 请求方式、请求路径、请求协议
recv['method'], recv['path'], recv['protocol'] = recv_row.split(' ')
# 请求头与请求体分离(这里分割一次的目的是为了有如post请求体里也会包含两个换行,股这里分割一次将请求体分割出来就行了)
header, recv['body'] = recv_info.split('\r\n\r\n', maxsplit=1)
# 格式化请求头
recv["meta"] = {}
# 循环每行请求头信息
for item in header.split('\r\n'):
meta_key, meta_val = item.split(': ')
recv["meta"][meta_key] = meta_val

return recv
except:
# 不能以http协议分割的请求信息统一定义为非法请求,返回空(如普通tcp请求)
return None


if __name__ == '__main__':
pass

socket服务端

文件名:main.py,该文件也是程序的主入口,用来循环接收客户端请求,并将请求转发给framework.py,然后将framework.py文件处理后的结果发送给客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import socket
import threading

import framework


class HttpServer(object):
def __init__(self, ip="localhost", port=8000):
self.ip_port = (ip, port)
# 创建socket套接字ipv4和tcp协议
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 复用端口,程序退出理解释放端口
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定ip和端口
self.socket.bind(self.ip_port)
# 开始监听客户端连接
self.socket.listen(128)

@staticmethod
def connect_client(client_socket):
"""与客户端交互"""
# 接收客户端发来的数据
recv_data_byte = client_socket.recv(1024)
# 获取客户端发来的数据长度
recv_data_byte_len = len(recv_data_byte)
# 判断客户端是否主动关闭了与服务端的链接
if recv_data_byte_len == 0:
client_socket.close()
# 将请求转给framework去处理
web_service = framework.WebService(recv_data_byte)
# 将处理后的结果返回客户端
client_socket.send(web_service.handle_request())
# 关闭客户端socket连接
client_socket.close()

def start(self):
"""开始循环等待客户端的连接"""
while True:
# 等待客户端socket连接
client_socket, client_ip_port = self.socket.accept()
# # 打印连接上的客户端
# print('已有客户端连接:', client_ip_port, client_socket)
# 开启线程执行客户端连接,并开启主线程守护,主线程关闭所有子线程关闭
socket_thread = threading.Thread(target=self.connect_client, args=(client_socket, ), daemon=True)
# 运行子线程
socket_thread.start()


if __name__ == '__main__':
http = HttpServer()
http.start()

测试框架

我们测试分三步,普通get,普通post,普通tcp。

tcp请求

我们使用tcp客户端请求给服务端发送了一串数据,因为服务端按照http协议就分割接收的数据时发现解不开,出现了异常,所以交给了错误视图去处理,最终返回的结果如图所示,

get请求
访问首页

以raw显示

访问不存在的页面

post请求

视图函数里没做请求方法处理,所以显示没有任何区别。

总结

以上就是简单的web框架内容,在此基础上如果继续扩大功能,总有一天会变成Django的,哈哈哈。

分支

现在来看我们的框架已经是个雏形了,乍一看,还真有点Django的感觉,我就在想,Django如果这么处理,Flask呢?Flask不用定义文件路由,貌似使用了一个路由装饰器就做到了路由转发,我们如果想这么操作该如何做呢?

这个问题要聊到你对装饰器的了解程度了,python装饰器详解文章之前演示过关于装饰器的用法,忘记当时有没有说装饰器执行的时机是什么时候,我们这里再演示下。

我们正常来运行一个带装饰器的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2020/11/25 13:39
# @Author : 托小尼
# @Email : 646547989@qq.com
# @URI : https://www.diandian100.cn
# @File : test.py
import time


def decorator(func):
print("我用来计算函数用时")
def inner():
start = time.time()
func()
print("用时:", time.time()-start)

return inner

@decorator
def run():
for item in range(100000):
print(item)

if __name__ == '__main__':
run()

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
我用来计算函数用时
0
1
2
3
……
99996
99997
99998
99999
用时: 0.691594123840332

可以看到程序最开始就打印了装饰器里输出的内容,这样可以理解,代码中也是在函数执行前打印的,我们稍微改下看看,我们不执行run方法,看会是一个什么结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2020/11/25 13:39
# @Author : 托小尼
# @Email : 646547989@qq.com
# @URI : https://www.diandian100.cn
# @File : test.py
import time


def decorator(func):
print("我用来计算函数用时")
def inner():
start = time.time()
func()
print("用时:", time.time()-start)

return inner

@decorator
def run():
for item in range(100000):
print(item)

if __name__ == '__main__':
pass

这段代码上面跟原来一模一样,只是我们在加载模块时没有执行run方法,也就是说我们自始至终没有执行过run方法,看下输出结果:

1
2
3
我用来计算函数用时

Process finished with exit code 0

装饰器依然输出了,所以我们可以得出装饰器的运行时机是:装饰器的执行时间是加载模块时立即执行

通过以上对装饰器的回顾我们是不是对路由装饰器如何做有了想法?

我们这里将之前的路由route数组清空然后修改views.py文件即可

views.py路由装饰器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2020/11/24 15:34
# @Author : 托小尼
# @Email : 646547989@qq.com
# @URI : https://www.diandian100.cn
# @File : views.py
from urls import route


def route_url(path):
# 装饰器
def decorator(func):
# 添加url路径和对应视图方法至路由
route.append((path, func))
def inner():
return func()
return inner
return decorator

@route_url('/index.html')
def index(request):
with open("templates/index.html") as f:
body = f.read()

return body.replace("{% body %}", '使用视图函数做的操作,我是首页')

@route_url('/login.html')
def login(request):
with open("templates/index.html") as f:
body = f.read()

return body.replace("{% body %}", '使用视图函数做的操作,这里是登录页面')


def error(request, msg):
with open("templates/error.html") as f:
body = f.read()

return body.replace("{% body %}", msg)

if __name__ == '__main__':
print(route)

我们代码调整的很少,只是加了一个装饰器,装饰器里通过route.append((path, func))将当前方法和对应的请求路径添加到了route路由中,我们单独来打印下该文件吧

运行结果:

1
2
3
[('/index.html', <function index at 0x10c59c700>), ('/login.html', <function login at 0x10c6659d0>)]

Process finished with exit code 0

可以看到跟刚才我们举例一样,只要加载了这个模块,装饰器就已经执行了。

访问测试

再次访问下,我们路由为空,也只有以上两个函数加了装饰器。

访问首页

这个很明显了吧,我们没有这个路由

其他页面

tcp测试

以上我们使用路有装饰器的方式完成了我们的需求。