乐趣区

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

Todo List 程序介绍

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

注册页面:

登录页面:

首页:

编辑页面:

程序页面十分简洁,甚至有些 Low。但这足够咱们学习开发 Web 服务器程序原理,页面款式的问题并不是咱们本次学习的重点,所以读者不用纠结于此。

Todo List 程序性能大略分为两个局部,一部分是 todo 治理,蕴含增删改查根底性能;另一部分是用户治理,蕴含注册和登录性能。

初识 MVC

介绍了 Todo List 程序的页面和性能,接下来咱们就要思考如何设计程序。

以客户端通过浏览器向服务器发送一个获取利用首页的申请为例,来剖析下服务器在收到这个申请后都须要做哪些事件:

  1. 首先服务器须要对申请数据进行解析,发现客户端是要获取利用首页。
  2. 而后找到代表首页的 HTML 文件,读取 HTML 文件中的内容。
  3. 最初将 HTML 内容组装成合乎 HTTP 标准的数据进行返回。

这是一个较为理想的状况,因为 HTML 页面内容是固定的,咱们不须要对其进行其余解决,间接返回给浏览器即可。通常咱们管这种页面叫动态页面。

但理论状况中,Todo List 程序首页内容并不是变化无穷的,而是动态变化的。首页 HTML 文件中只定义根底构造,具体的 todo 数据须要动静填充进去。所以一个更加残缺的服务器解决申请的过程应该像上面这样:

  1. 首先服务器须要对申请数据进行解析,发现客户端是要获取利用首页。
  2. 而后从数据库中读取 todo 数据。
  3. 接着找到代表首页的 HTML 文件,读取 HTML 文件中的内容。
  4. 再将 todo 数据动静增加到 HTML 内容中。
  5. 最初将解决好的 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 程序首页为例,一个残缺的申请过程如下:

  1. 浏览器发动申请。
  2. 申请达到服务器后首先进入控制器,而后控制器从模型获取 todo 数据。
  3. 模型操作数据库,查问 todo 数据。
  4. 数据库返回 todo 数据。
  5. 模型将从数据库中查问的 todo 数据返回给控制器,控制器临时将数据保留在内存中。
  6. 控制器从视图中获取首页 HTML 模板。
  7. 控制器将从模型查出来的 todo 数据填充到首页 HTML 模板中,并组装成合乎 HTTP 标准的数据。
  8. 服务器返回响应。

其实 MVC 是一个宏观上的分层,具体细节局部还须要依据咱们设计程序的粒度来进行解决,比方有些逻辑既能够写在控制器层,也能够写在模型层,甚至咱们还能够在 MVC 的根底上扩大更多的分层。这些都须要联合具体的业务逻辑来决定。

构建 Todo List 程序

学习了 MVC 模式,咱们就能够依据 MVC 模式来试着构建 Todo List 程序了。

Todo List 程序分两局部:todo 治理、用户治理。在我的项目初期,咱们必定不会思考的太过全面。所以能够先不思考用户治理性能局部的实现,先只思考如何实现 todo 治理性能。

Todo List 程序目录结构设计如下:

todo_list
├── server.py
└── todo
    ├── __init__.py
    ├── config.py
    ├── controllers.py
    ├── db
    │   └── todo.json
    ├── models.py
    ├── templates
    │   ├── edit.html
    │   └── index.html
    └── utils.py

这里以 todo_list/ 作为程序的根目录,根目录下蕴含 server.py 文件和 todo/ 目录。其中 server.py 次要性能就是作为一个 Web Server 来接管申请和返回响应,它是 Todo List 程序的入口和进口。而 todo/ 目录则是 Todo List 程序处理业务逻辑的外围。

todo/ 目录下的 __init__.pytodo/ 文件夹标记为一个 Python 包。config.py 用于存储一些我的项目的根底配置。utils.py 是一个工具集,外面能够定义一些供其余模块调用的类和办法。db/ 目录作为 Todo List 程序存储数据的目录,db/todo.json 用来存储所有的 todo 内容。剩下还有两个 .py 文件和一个目录没有介绍,置信你曾经猜到了 models.pytemplates/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 文件代码如下:

# todo_list/server.py

import socket
import threading

from todo.config import HOST, PORT, BUFFER_SIZE


def process_connection(client):
    """解决客户端申请"""
    # 接管申请报文数据
    request_bytes = b''
    while True:
        chunk = client.recv(BUFFER_SIZE)
        request_bytes += chunk
        if len(chunk) < BUFFER_SIZE:
            break

    # 申请报文
    request_message = request_bytes.decode('utf-8')
    print(f'request_message: {request_message}')

    # TODO: 解析申请
    # TODO: 返回响应
    
    # 敞开连贯
    client.close()


def main():
    """入口函数"""
    with socket.socket() as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((HOST, PORT))
        s.listen(5)
        print(f'running on http://{HOST}:{PORT}')

        while True:
            client, address = s.accept()
            print(f'client address: {address}')
            # 创立新的线程来解决客户端连贯
            t = threading.Thread(target=process_connection, args=(client,))
            t.start()

server.py 程序简直就是之前实现的多线程版 Hello World 服务器程序照搬过去的。为了程序代码更加清晰,这里将服务器的 IP 地址、端口、接管申请的缓冲区大小定义为变量写在了配置文件 todo/config.py 中,所以须要在 server.py 文件顶部从配置文件中导入 HOSTPORTBUFFER_SIZE。在 main 函数中,实例化 socket 对象局部的代码也有所扭转,这里采纳了 with 语句来实例化 socket 对象,这样可能保障任何状况下退出程序时 socket 都可能被正确敞开。此处 with 语句的用法能够类比文件操作时的 with 语句。解决客户端连贯申请的 process_connection 函数外部根本逻辑没有扭转,其中有两个 TODO 正文示意解析申请和返回响应的性能暂未实现。

server.py 入口程序接管到客户端的申请当前,须要解析申请报文,并依据解析进去的申请报文来决定如何解决申请并返回响应。所以接下来咱们须要编写解析申请的代码。

不过在这之前,我先给出 todo/config.py 配置文件的代码,毕竟之后还会用到:

# todo_list/todo/config.py

import os

# todo/ 目录绝对路径
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# IP
HOST = '127.0.0.1'
# 端口
PORT = 8000

# 缓冲大小
BUFFER_SIZE = 1024

配置文件中除了蕴含后面介绍过的示意 IP 地址、端口、接管申请的缓冲区大小的几个变量,还有一个 BASE_DIR 变量用来示意 todo/ 目录的绝对路径,不便在程序中获取我的项目门路。

当初来看下如何解析申请,咱们能够定义一个 Request 类用来专门解析申请报文,代码写在 todo/utils.py 文件中:

# todo_list/todo/utils.py

class Request(object):
    """申请类"""

    def __init__(self, request_message):
        method, path, headers = self.parse_data(request_message)
        self.method = method  # 申请办法 GET、POST
        self.path = path  # 申请门路 /index
        self.headers = headers  # 申请头 {'Host': '127.0.0.1:8000'}

    def parse_data(self, data):
        """解析申请报文数据"""
        # 用申请报文中的第一个 '\r\n\r\n' 做宰割,将失去申请头和申请体
        # 申请体临时用不到先不解决
        header, body = data.split('\r\n\r\n', 1)
        method, path, headers = self._parse_header(header)
        return method, path, headers

    def _parse_header(self, data):
        """解析申请头"""
        # 拆分申请行和申请首部
        request_line, request_header = data.split('\r\n', 1)

        # 申请行拆包 'GET /index HTTP/1.1' -> ['GET', '/index', 'HTTP/1.1']
        # 因为 HTTP 版本号没什么用,所以用一个下划线 _ 变量来接管
        method, path, _ = request_line.split()
        
        # 解析申请首部所有的键值对,组装成字典
        headers = {}
        for header in request_header.split('\r\n'):
            k, v = header.split(':', 1)
            headers[k] = v
        
        return method, path, headers

Request 类的初始化办法 __init__ 接管申请报文字符串作为参数。在其外部调用 parse_data 办法将申请报文字符串解析成咱们须要的结构化数据。

解析完申请报文,咱们须要依据申请报文信息来判断如何返回响应。根底逻辑判断局部的代码能够写在 todo/controllers.py 中:

# todo_list/todo/controllers.py

from todo.utils import render_template


def index():
    """首页视图函数"""
    return render_template('index.html')

定义在控制器层的函数也叫视图函数,因为它们通常返回视图层的 HTML 内容。index 视图函数用来解决申请首页的逻辑,它返回 render_template 函数的调用后果,render_template 函数的作用是将 HTML 内容读取成字符串并返回,其定义如下:

# todo_list/todo/utils.py

import os

from todo.config import BASE_DIR


def render_template(template):
    """读取 HTML 内容"""
    # 读取 'todo_list/todo/templates' 目录下的 HTML 文件内容
    template_dir = os.path.join(BASE_DIR, 'templates')
    path = os.path.join(template_dir, template)
    with open(path, 'r', encoding='utf-8') as f:
        html = f.read()
    return html

todo/controllers.py 文件底部还定义了一个 routes 字典,字典的键为申请门路,值为一个元组,元组的第一个元素作为解决申请的函数,第二个元素是一个列表,外面定义解决申请的函数所容许的申请办法。index 视图函数可能同时匹配两个门路://index,因为这两个门路通常都代表首页。

# todo_list/todo/controllers.py

routes = {'/': (index, ['GET']),
    '/index': (index, ['GET']),
}

读取出 HTML 内容当前,咱们就能够结构响应报文并返回给浏览器了。在 utils.py 文件下,编写一个 Response 类用来结构响应:

# todo_list/todo/utils.py

class Response(object):
    """响应类"""

    # 依据状态码获取起因短语
    reason_phrase = {
        200: 'OK',
        405: 'METHOD NOT ALLOWED',
    }

    def __init__(self, body, headers=None, status=200):
        # 默认响应首部字段,指定响应内容的类型为 HTML
        _headers = {'Content-Type': 'text/html; charset=utf-8',}

        if headers is not None:
            _headers.update(headers)
        self.headers = _headers  # 响应头
        self.body = body  # 响应体
        self.status = status  # 状态码

    def __bytes__(self):
        """结构响应报文"""
        # 状态行 'HTTP/1.1 200 OK\r\n'
        header = f'HTTP/1.1 {self.status} {self.reason_phrase.get(self.status,"")}\r\n'
        # 响应首部
        header += ''.join(f'{k}: {v}\r\n' for k, v in self.headers.items())
        # 空行
        blank_line = '\r\n'
        # 响应体
        body = self.body

        response_message = header + blank_line + body
        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 文件,持续欠缺代码:

# todo_list/server.py

import socket
import threading

from todo.config import HOST, PORT, BUFFER_SIZE
from todo.utils import Request, Response
from todo.controllers import routes


def process_connection(client):
    """解决客户端申请"""
    # 接管申请报文数据
    request_bytes = b''
    while True:
        chunk = client.recv(BUFFER_SIZE)
        request_bytes += chunk
        if len(chunk) < BUFFER_SIZE:
            break

    # 申请报文
    request_message = request_bytes.decode('utf-8')
    print(f'request_message: {request_message}')

    # 解析申请报文,结构申请对象
    request = Request(request_message)
    # 依据申请对象结构响应报文
    response_bytes = make_response(request)
    # 返回响应
    client.sendall(response_bytes)

    # 敞开连贯
    client.close()


def make_response(request, headers=None):
    """结构响应报文"""
    # 默认状态码为 200
    status = 200
    # 获取匹配以后申请门路的处理函数和函数所接管的申请办法
    # request.path 等于 '/' 或 '/index' 时,routes.get(request.path) 将返回 (index, ['GET'])
    route, methods = routes.get(request.path)

    # 如果申请办法不被容许,返回 405 状态码
    if request.method not in methods:
        status = 405
        data = 'Method Not Allowed'
    else:
        # 申请首页时 route 实际上就是咱们在 controllers.py 中定义的 index 视图函数
        data = route()

    # 获取响应报文
    response = Response(data, headers=headers, status=status)
    response_bytes = bytes(response)
    print(f'response_bytes: {response_bytes}')

    return response_bytes


def main():
    """入口函数"""
    with socket.socket() as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((HOST, PORT))
        s.listen(5)
        print(f'running on http://{HOST}:{PORT}')

        while True:
            client, address = s.accept()
            print(f'client address: {address}')
            # 创立新的线程来解决客户端连贯
            t = threading.Thread(target=process_connection, args=(client,))
            t.start()


if __name__ == '__main__':
    main()

首先实现之前未写完的 process_connection 函数。将原来标记 TODO 正文的中央替换成了如下代码:

# 解析申请报文,结构申请对象
request = Request(request_message)
# 依据申请对象结构响应报文
response_bytes = make_response(request)
# 返回响应
client.sendall(response_bytes)

新增了一个 make_response 函数,不便用来依据申请对象结构响应报文。函数中我写了比拟具体的正文,你能够依据正文内容读懂代码逻辑。

最初给出首页 todo/templates/index.html 的 HTML 代码:

<!--todo_list/todo/templates/index.html-->

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Todo List</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        ul {list-style: none;}

        a {
            text-decoration: none;
            outline: none;
            color: #000000;
        }

        h1 {margin: 20px auto;}

        .container {
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .container ul {
            width: 100%;
            max-width: 600px;
        }

        .container ul li {
            height: 40px;
            line-height: 40px;
            margin-bottom: 4px;
            padding: 0 6px;
            display: flex;
            justify-content: space-between;
            background-color: #d2d2d2;
        }
    </style>
</head>
<body>
<h1 class="container">Todo List</h1>
<div class="container">
    <ul>
        <li>
            <div>Hello World</div>
        </li>
    </ul>
</div>
</body>
</html>

HTML 代码比较简单,其中顶部写了一些根底的 CSS 款式,都很容易看懂,这里不再解说。

接下来在终端中,进入 todo_list/ 目录下,应用 Python 运行 server.py 文件,看到如下打印后果阐明程序曾经失常启动:

关上浏览器,地址栏输出 http://127.0.0.1:8000/ 或者 http://127.0.0.1:8000/index,你将看到 Todo List 程序首页:

至此,Todo List 程序首页初步实现。不过我想很多读者看到这里会产生纳闷,说好的 MVC 呢,目前为止咱们并没有编写一行模型层的代码,并且首页的 HTML 内容也不是动静填充的。没错,为了可能尽快让 Todo List 程序跑起来,我无意的避开了这两个问题,下一章咱们再来解决这两个问题。

本章源码:chapter3

分割我:

  • 微信:jianghushinian
  • 邮箱:jianghushinian007@outlook.com
  • 博客地址:https://jianghushinian.cn/
退出移动版