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
|
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
|
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
|
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): """格式化返回信息(响应状态码和响应体)""" response_row = "HTTP/1.1 %s %s\r\n" % (status_code, "IM OK") 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): """请求操作,转发给路由对应的函数去处理(路由分发)""" 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: recv_row, recv_info = request.decode('utf8').split('\r\n', maxsplit=1) recv['method'], recv['path'], recv['protocol'] = recv_row.split(' ') 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: 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) self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) 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() web_service = framework.WebService(recv_data_byte) client_socket.send(web_service.handle_request()) client_socket.close()
def start(self): """开始循环等待客户端的连接""" while True: client_socket, client_ip_port = self.socket.accept() 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
|
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
|
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
|
from urls import route
def route_url(path): def decorator(func): 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测试
![]()
以上我们使用路有装饰器的方式完成了我们的需求。