用 Python 撸一个 Web 服务器-第3章:使用 MVC 构建程序

打印 上一主题 下一主题

主题 981|帖子 981|积分 2943

Todo List 程序先容

我们将要编写的 Todo List 程序包含四个页面,分别是注册页面、登录页面、首页、编辑页面。以下分别为四个页面的截图。
注册页面:

注册
登录页面:

登录
首页:

首页
编辑页面:

编辑
程序页面非常简洁,乃至有些 Low。但这足够我们学习开发 Web 服务器程序原理,页面样式的题目并不是我们本次学习的重点,所以读者不必纠结于此。
Todo List 程序功能大概分为两个部分,一部分是 todo 管理,包含增删改查底子功能;另一部分是用户管理,包含注册和登录功能。
初识 MVC

先容了 Todo List 程序的页面和功能,接下来我们就要思考怎样设计程序。
以客户端通过欣赏器向服务器发送一个获取应用首页的哀求为例,来分析下服务器在收到这个哀求后都需要做哪些事情:

  • 首先服务器需要对哀求数据进行解析,发现客户端是要获取应用首页。
  • 然后找到代表首页的 HTML 文件,读取 HTML 文件中的内容。
  • 最后将 HTML 内容组装成符合 HTTP 规范的数据进行返回。
这是一个较为抱负的情况,因为 HTML 页面内容是固定的,我们不需要对其进行其他处理,直接返回给欣赏器即可。通常我们管这种页面叫静态页面。
但现实情况中,Todo List 程序首页内容并不是一成不变的,而是动态变化的。首页 HTML 文件中只定义底子结构,具体的 todo 数据需要动态填充进去。所以一个更加完整的服务器处理哀求的过程应该像下面如许:

  • 首先服务器需要对哀求数据进行解析,发现客户端是要获取应用首页。
  • 然后从数据库中读取 todo 数据。
  • 接着找到代表首页的 HTML 文件,读取 HTML 文件中的内容。
  • 再将 todo 数据动态添加到 HTML 内容中。
  • 最后将处理好的 HTML 内容组装成符合 HTTP 规范的数据进行返回。
现在已经知道了服务器处理哀求的完整过程,我们就可以设计服务器程序了。试想一下,如果 Todo List 程序都像 Hello World 程序一样把代码都写在一个 Python 文件中也不是不可以。但如许的代码显然不具备良好的扩展性和可维护性。那么更好的设计模式是什么呢?
其实对于 Web 服务器程序的设计,业界早已达成了一个普遍的共识,那就是 MVC 模式:
M(Model):模型,用来存储和处理 Web 应用数据。
V(View):视图,格式化显示 Web 应用页面。
C(Controller):控制器,负责底子逻辑,如从模型层读取数据并将数据填充到视图层,然后返回响应。
通过 MVC 的分层结构,能够让 Web 应用设计更加清晰,可以很轻易的构建可扩展、易维护的代码。模型层,说直白些其实就是用来读写数据库的 Python 代码,新增 todo 的时候,可以通过模型层的代码将数据生存到数据库中,访问首页时需要展示全部已生存的 todo,这时可以通过模型层的代码从数据库中读取全部 todo。视图层,可以将其简单的明白为 HTML 模板文件的聚集。控制器起到粘合的作用,它将从模型层读取过来的数据填充到视图层并返回给欣赏器,或者将欣赏器通过 HTML 页面提交过来的数据解析出来再通过模型层写入数据库中。
我画了一个示例图,来资助你明白 MVC 模式。图中标注了欣赏器发起一个哀求到获得响应,中心履历的完整过程。

照旧以客户端哀求 Todo List 程序首页为例,一个完整的哀求过程如下:

  • 欣赏器发起哀求。
  • 哀求到达服务器后首先辈入控制器,然后控制器从模型获取 todo 数据。
  • 模型操纵数据库,查询 todo 数据。
  • 数据库返回 todo 数据。
  • 模型将从数据库中查询的 todo 数据返回给控制器,控制器暂时将数据生存在内存中。
  • 控制器从视图中获取首页 HTML 模板。
  • 控制器将从模型查出来的 todo 数据填充到首页 HTML 模板中,并组装成符合 HTTP 规范的数据。
  • 服务器返回响应。
其实 MVC 是一个宏观上的分层,具体细节部分还需要根据我们设计程序的粒度来进行处理,好比有些逻辑既可以写在控制器层,也可以写在模型层,乃至我们还可以在 MVC 的底子上扩展更多的分层。这些都需要联合具体的业务逻辑来决定。
构建 Todo List 程序

学习了 MVC 模式,我们就可以根据 MVC 模式来试着构建 Todo List 程序了。
Todo List 程序分两部分:todo 管理、用户管理。在项目初期,我们肯定不会思量的太过全面。所以可以先不思量用户管理功能部分的实现,先只思量怎样实现 todo 管理功能。
Todo List 程序目录结构设计如下:
  1. todo_list  
  2. ├── server.py  
  3. └── todo  
  4. ├── \_\_init\_\_.py  
  5. ├── config.py  
  6. ├── controllers.py  
  7. ├── db  
  8. │   └── todo.json  
  9. ├── models.py  
  10. ├── templates  
  11. │   ├── edit.html  
  12. │   └── index.html  
  13. └── utils.py
复制代码
这里以 todo_list/ 作为程序的根目录,根目录下包含 server.py 文件和 todo/ 目录。此中 server.py 主要功能就是作为一个 Web Server 来接收哀求和返回响应,它是 Todo List 程序的入口和出口。而 todo/ 目录则是 Todo List 程序处理业务逻辑的核心。
todo/ 目录下的 __init__.py 将 todo/ 文件夹标记为一个 Python 包。 config.py 用于存储一些项目的底子设置。utils.py 是一个工具集,里面可以定义一些供其他模块调用的类和方法。db/ 目录作为 Todo List 程序存储数据的目录,db/todo.json 用来存储全部的 todo 内容。剩下还有两个 .py 文件和一个目录没有先容,相信你已经猜到了 models.py、templates/、controllers.py 分别对应了 MVC 模式中的模型、视图、控制器。models.py 中编写操纵 todo 数据的代码,templates/ 目录用来存放 HTML 模板文件,templates/index.html 是首页,templates/edit.html 是编辑页面,controllers.py 编写负责程序控制的底子逻辑代码。
我们对项目的目录结构有了一个概览,这里我要夸大一下 db/ 目录的作用。我们在开发整个 Todo List 程序的过程中都不会使用现实的数据库程序,项目中全部需要存储的数据都生存在 db/ 目录下的文件中。在开发 Web 程序时,需要用到数据库的目的就是为了存储数据,对于 Todo List 程序来说使用文件同样能满意需求,同时能够照顾到对数据库不了解的读者。
Todo List 首页开发

Todo List 程序目录结构构建完成后就可以动手开发程序了,我们可以从一个哀求履历的过程来着手。
一个哀求发送到服务器,首先服务器需要有一个能够接收哀求的入口,在程序根目录 todo_list/ 下的 server.py 就是这个入口。server.py 文件代码如下:
  1. \# todo_list/server.py
  2. import socket  
  3. import threading
  4. from todo.config import HOST, PORT, BUFFER_SIZE
  5. def process_connection(client):  
  6. """处理客户端请求"""  
  7. \# 接收请求报文数据  
  8. request_bytes = b''  
  9. while True:  
  10. chunk = client.recv(BUFFER_SIZE)  
  11. request_bytes += chunk  
  12. if len(chunk) < BUFFER_SIZE:  
  13. break
  14. \# 请求报文  
  15. request_message = request_bytes.decode('utf-8')  
  16. print(f'request_message: {request_message}')
  17. \# TODO: 解析请求  
  18. \# TODO: 返回响应
  19. \# 关闭连接  
  20. client.close()
  21. def main():  
  22. """入口函数"""  
  23. with socket.socket() as s:  
  24. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  
  25. s.bind((HOST, PORT))  
  26. s.listen(5)  
  27. print(f'running on http://{HOST}:{PORT}')
  28. while True:  
  29. client, address = s.accept()  
  30. print(f'client address: {address}')  
  31. \# 创建新的线程来处理客户端连接  
  32. t = threading.Thread(target=process_connection, args=(client,))  
  33. t.start()
复制代码
server.py 程序险些就是之前实现的多线程版 Hello World 服务器程序照搬过来的。为了程序代码更加清晰,这里将服务器的 IP 地点、端口、接收哀求的缓冲区巨细定义为变量写在了设置文件 todo/config.py 中,所以需要在 server.py 文件顶部从设置文件中导入 HOST 、PORT 、BUFFER_SIZE。在 main 函数中,实例化 socket 对象部分的代码也有所改变,这里接纳了 with 语句来实例化 socket 对象,如许能够保证任何情况下退出程序时 socket 都能够被正确关闭。此处 with 语句的用法可以类比文件操纵时的 with 语句。处理客户端连接哀求的 process_connection 函数内部基本逻辑没有改变,此中有两个 TODO 注释表示解析哀求和返回响应的功能暂未实现。
从 server.py 入口程序接收到客户端的哀求以后,需要解析哀求报文,并根据解析出来的哀求报文来决定怎样处理哀求并返回响应。所以接下来我们需要编写解析哀求的代码。
不过在这之前,我先给出 todo/config.py 设置文件的代码,毕竟之后还会用到:
  1. \# todo_list/todo/config.py
  2. import os
  3. \# todo/ 目录绝对路径  
  4. BASE_DIR = os.path.dirname(os.path.abspath(\_\_file\_\_))
  5. \# IP  
  6. HOST = '127.0.0.1'  
  7. \# 端口  
  8. PORT = 8000
  9. \# 缓冲大小  
  10. BUFFER_SIZE = 1024
复制代码
设置文件中除了包含前面先容过的表示 IP 地点、端口、接收哀求的缓冲区巨细的几个变量,还有一个 BASE_DIR 变量用来表示 todo/ 目录的绝对路径,方便在程序中获取项目路径。
现在来看下怎样解析哀求,我们可以定义一个 Request 类用来专门解析哀求报文,代码写在 todo/utils.py 文件中:
  1. \# todo_list/todo/utils.py
  2. class Request(object):  
  3. """请求类"""
  4. def \_\_init\_\_(self, request_message):  
  5. method, path, headers = self.parse_data(request_message)  
  6. self.method = method \# 请求方法 GET、POST  
  7. self.path = path \# 请求路径 /index  
  8. self.headers = headers \# 请求头 {'Host': '127.0.0.1:8000'}
  9. def parse_data(self, data):  
  10. """解析请求报文数据"""  
  11. \# 用请求报文中的第一个 '\\r\\n\\r\\n' 做分割,将得到请求头和请求体  
  12. \# 请求体暂时用不到先不处理  
  13. header, body = data.split('\\r\\n\\r\\n', 1)  
  14. method, path, headers = self.\_parse_header(header)  
  15. return method, path, headers
  16. def \_parse_header(self, data):  
  17. """解析请求头"""  
  18. \# 拆分请求行和请求首部  
  19. request_line, request_header = data.split('\\r\\n', 1)
  20. \# 请求行拆包 'GET /index HTTP/1.1' -> \['GET', '/index', 'HTTP/1.1'\]  
  21. \# 因为 HTTP 版本号没什么用,所以用一个下划线 _ 变量来接收  
  22. method, path, _ = request_line.split()
  23. \# 解析请求首部所有的键值对,组装成字典  
  24. headers = {}  
  25. for header in request_header.split('\\r\\n'):  
  26. k, v = header.split(': ', 1)  
  27. headers\[k\] = v
  28. return method, path, headers
复制代码
Request 类的初始化方法 __init__ 接收哀求报笔墨符串作为参数。在其内部调用 parse_data 方法将哀求报笔墨符串解析成我们需要的结构化数据。
解析完哀求报文,我们需要根据哀求报文信息来判定怎样返回响应。底子逻辑判定部分的代码可以写在 todo/controllers.py 中:
  1. \# todo_list/todo/controllers.py
  2. from todo.utils import render_template
  3. def index():  
  4. """首页视图函数"""  
  5. return render_template('index.html')
复制代码
定义在控制器层的函数也叫视图函数,因为它们通常返回视图层的 HTML 内容。index 视图函数用来处理哀求首页的逻辑,它返回 render_template 函数的调用效果,render_template 函数的作用是将 HTML 内容读取成字符串并返回,其定义如下:
  1. \# todo_list/todo/utils.py
  2. import os
  3. from todo.config import BASE_DIR
  4. def render_template(template):  
  5. """读取 HTML 内容"""  
  6. \# 读取 'todo_list/todo/templates' 目录下的 HTML 文件内容  
  7. template_dir = os.path.join(BASE_DIR, 'templates')  
  8. path = os.path.join(template_dir, template)  
  9. with open(path, 'r', encoding='utf-8') as f:  
  10. html = f.read()  
  11. return html
复制代码
在 todo/controllers.py 文件底部还定义了一个 routes 字典,字典的键为哀求路径,值为一个元组,元组的第一个元素作为处理哀求的函数,第二个元素是一个列表,里面定义处理哀求的函数所答应的哀求方法。index 视图函数能够同时匹配两个路径:/、/index,因为这两个路径通常都代表首页。
  1. \# todo_list/todo/controllers.py
  2. routes = {  
  3. '/': (index, \['GET'\]),  
  4. '/index': (index, \['GET'\]),  
  5. }
复制代码
读取出 HTML 内容以后,我们就可以构造响应报文并返回给欣赏器了。在 utils.py 文件下,编写一个 Response 类用来构造响应:
  1. \# todo_list/todo/utils.py
  2. class Response(object):  
  3. """响应类"""
  4. \# 根据状态码获取原因短语  
  5. reason_phrase = {  
  6. 200: 'OK',  
  7. 405: 'METHOD NOT ALLOWED',  
  8. }
  9. def \_\_init\_\_(self, body, headers=None, status=200):  
  10. \# 默认响应首部字段,指定响应内容的类型为 HTML  
  11. \_headers = {  
  12. 'Content-Type': 'text/html; charset=utf-8',  
  13. }
  14. if headers is not None:  
  15. \_headers.update(headers)  
  16. self.headers = \_headers \# 响应头  
  17. self.body = body \# 响应体  
  18. self.status = status \# 状态码
  19. def \_\_bytes\_\_(self):  
  20. """构造响应报文"""  
  21. \# 状态行 'HTTP/1.1 200 OK\\r\\n'  
  22. header = f'HTTP/1.1 {self.status} {self.reason_phrase.get(self.status, "")}\\r\\n'  
  23. \# 响应首部  
  24. header += ''.join(f'{k}: {v}\\r\\n' for k, v in self.headers.items())  
  25. \# 空行  
  26. blank_line = '\\r\\n'  
  27. \# 响应体  
  28. body = self.body
  29. response_message = header + blank_line + body  
  30. return response_message.encode('utf-8')
复制代码
Response 类的初始化方法 __init__ 接收三个参数,分别为响应体、响应首部字段、状态码。此中响应体为 str 范例,首页的响应体现实上就是 index.html 文件内容。响应首部字段为 dict 范例,在构造响应报文时,全部的响应首部字段最终按照 HTTP 规范拼接到一起作为响应首部。状态码为数值范例,现在只思量了状态码为 200 正常响应和 405 哀求方法不被答应。
需要注意的是,Response 类定义了 __bytes__ 邪术方法作为构造响应报文的方法。当使用 Python 内置的 bytes 方法转换 Response 实例对象时(bytes(Response())),会主动调用 __bytes__ 邪术方法。
从解析哀求到构造响应报文的代码现在已经基本编写完成。接下来我们将整个处理哀求的流程串联起来,回到 server.py 文件,继续完善代码:
  1. \# todo_list/server.py
  2. import socket  
  3. import threading
  4. from todo.config import HOST, PORT, BUFFER_SIZE  
  5. from todo.utils import Request, Response  
  6. from todo.controllers import routes
  7. def process_connection(client):  
  8. """处理客户端请求"""  
  9. \# 接收请求报文数据  
  10. request_bytes = b''  
  11. while True:  
  12. chunk = client.recv(BUFFER_SIZE)  
  13. request_bytes += chunk  
  14. if len(chunk) < BUFFER_SIZE:  
  15. break
  16. \# 请求报文  
  17. request_message = request_bytes.decode('utf-8')  
  18. print(f'request_message: {request_message}')
  19. \# 解析请求报文,构造请求对象  
  20. request = Request(request_message)  
  21. \# 根据请求对象构造响应报文  
  22. response_bytes = make_response(request)  
  23. \# 返回响应  
  24. client.sendall(response_bytes)
  25. \# 关闭连接  
  26. client.close()
  27. def make_response(request, headers=None):  
  28. """构造响应报文"""  
  29. \# 默认状态码为 200  
  30. status = 200  
  31. \# 获取匹配当前请求路径的处理函数和函数所接收的请求方法  
  32. \# request.path 等于 '/' 或 '/index' 时,routes.get(request.path) 将返回 (index, \['GET'\])  
  33. route, methods = routes.get(request.path)
  34. \# 如果请求方法不被允许,返回 405 状态码  
  35. if request.method not in methods:  
  36. status = 405  
  37. data = 'Method Not Allowed'  
  38. else:  
  39. \# 请求首页时 route 实际上就是我们在 controllers.py 中定义的 index 视图函数  
  40. data = route()
  41. \# 获取响应报文  
  42. response = Response(data, headers=headers, status=status)  
  43. response_bytes = bytes(response)  
  44. print(f'response_bytes: {response_bytes}')
  45. return response_bytes
  46. def main():  
  47. """入口函数"""  
  48. with socket.socket() as s:  
  49. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  
  50. s.bind((HOST, PORT))  
  51. s.listen(5)  
  52. print(f'running on http://{HOST}:{PORT}')
  53. while True:  
  54. client, address = s.accept()  
  55. print(f'client address: {address}')  
  56. \# 创建新的线程来处理客户端连接  
  57. t = threading.Thread(target=process_connection, args=(client,))  
  58. t.start()
  59. if \_\_name\_\_ == '\_\_main\_\_':  
  60. main()
复制代码
首先完成之前未写完的 process_connection 函数。将原来标记 TODO 注释的地方更换成了如下代码:
  1. \# 解析请求报文,构造请求对象  
  2. request = Request(request_message)  
  3. \# 根据请求对象构造响应报文  
  4. response_bytes = make_response(request)  
  5. \# 返回响应  
  6. client.sendall(response_bytes)
复制代码
新增了一个 make_response 函数,方便用来根据哀求对象构造响应报文。函数中我写了比力详细的注释,你可以根据注释内容读懂代码逻辑。
最后给出首页 todo/templates/index.html 的 HTML 代码:
  1. <!--todo_list/todo/templates/index.html-->
  2. <!DOCTYPE html>
  3. <html>  
  4. <head>  
  5. <meta charset="UTF-8">  
  6. <title>Todo List</title>  
  7. <style>  
  8. \* {  
  9. margin: 0;  
  10. padding: 0;  
  11. }  
  12.   
  13. ul {  
  14. list-style: none;  
  15. }  
  16.   
  17. a {  
  18. text-decoration: none;  
  19. outline: none;  
  20. color: #000000;  
  21. }  
  22.   
  23. h1 {  
  24. margin: 20px auto;  
  25. }  
  26.   
  27. .container {  
  28. display: flex;  
  29. justify-content: center;  
  30. align-items: center;  
  31. }  
  32.   
  33. .container ul {  
  34. width: 100%;  
  35. max-width: 600px;  
  36. }  
  37.   
  38. .container ul li {  
  39. height: 40px;  
  40. line-height: 40px;  
  41. margin-bottom: 4px;  
  42. padding: 0 6px;  
  43. display: flex;  
  44. justify-content: space-between;  
  45. background-color: #d2d2d2;  
  46. }  
  47. </style>  
  48. </head>  
  49. <body>  
  50. <h1 class="container">Todo List</h1>  
  51. <div class="container">  
  52. <ul>  
  53. <li>  
  54. <div>Hello World</div>  
  55. </li>  
  56. </ul>  
  57. </div>  
  58. </body>  
  59. </html>
复制代码
HTML 代码比力简单,此中顶部写了一些底子的 CSS 样式,都很轻易看懂,这里不再解说。
接下来在终端中,进入 todo_list/目录下,使用 Python 运行 server.py 文件,看到如下打印效果说明程序已经正常启动:

运行 Todo List 服务器
打开欣赏器,地点栏输入 http://127.0.0.1:8000/ 或者 http://127.0.0.1:8000/index,你将看到 Todo List 程序首页:

Todo List 首页
至此,Todo List 程序首页初步完成。不过我想许多读者看到这里会产生疑惑,说好的 MVC 呢,现在为止我们并没有编写一行模型层的代码,并且首页的 HTML 内容也不是动态填充的。没错,为了能够尽快让 Todo List 程序跑起来,我有意的避开了这两个题目,下一章我们再来办理这两个题目。
   本章源码:chapter3
  原文出处: https://jianghushinian.cn

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

伤心客

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表