Files
python-flask/project10/01.web框架的本质.md
2025-09-11 16:13:52 +08:00

10 KiB
Raw Blame History

HTTP的请求头和响应头

a856a010-ba01-45f7-8e59-8234ac2e9488

浏览器与服务器通信,主要是依靠请求报文来向服务器表示想要获取到什么样的资源,服务器会用响应报文来表达服务器会员的数据的情况

自定义web框架

web应用本质上就是一个socket服务端浏览器是socket客户端基于请求做出响应客户都先请求服务端做出对应的响应按照http协议的请求协议发送请求服务端按照http协议的响应协议来响应请求这样的网络通信我们就可以自己实现Web框架了。

准备一个html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试页面</title>
</head>
<body>
<h1>标题1</h1>
<img src="https://iproute.cn/medias/logo.png" alt="头像">
</body>
</html>

编写python的socket服务端

import socket
sk = socket.socket()
# 允许地址复用
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1',8080))
sk.listen()
conn,addr = sk.accept()
b_msg = conn.recv(1024)
str_msg = b_msg.decode('utf-8')
conn.send(b'HTTP/1.1 200 ok \r\n\r\n')
conn.send(b'hello')
conn.close()
sk.close()

运行服务端之后,浏览器访问http://127.0.0.1:8080浏览器传给socket的内容如下

image-20241210163939143

GET / HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8
Cache-Control: max-age=0
Connection: keep-alive
Host: 127.0.0.1:8080
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"

修改socket服务端让其返回网页内容

import socket
sk = socket.socket()
# 允许地址复用
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1',8080))
sk.listen()
conn,addr = sk.accept()
b_msg = conn.recv(1024)
str_msg = b_msg.decode('utf-8')
print(str_msg)
conn.send(b'HTTP/1.1 200 ok \r\n\r\n')
with open('test.html','rb') as f:
    f_data = f.read()
conn.send(f_data)
conn.close()
sk.close()

这样就可以将网页内容返回给浏览器了

如果想要在网页中携带本地路径的图片那么修改html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试页面</title>
</head>
<body>
<h1>标题1</h1>
<img src="logo.png" alt="头像">
</body>
</html>

然后修改socket服务端

import socket
sk = socket.socket()
# 允许地址复用
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1',8080))
sk.listen()
while 1:	# 浏览器多次访问所以需要while
    conn,addr = sk.accept()
    b_msg = conn.recv(1024)
    str_msg = b_msg.decode('utf-8')
    path = str_msg.split('\r\n')[0].split(' ')[1]
    print('path>>>',path)
    conn.send(b'HTTP/1.1 200 ok \r\n\r\n')
    if path == '/':
        with open('test.html','rb') as f:
            f_data = f.read()
        conn.send(f_data)
        conn.close()	# HTTP协议是短链接的一次请求对应一次响应这个请求就结束了所以我们需要写上close不然浏览器自己断了
    elif path == '/logo.png':
        with open('logo.png','rb') as f:
            f_data = f.read()
        conn.send(f_data)
        conn.close()
sk.close()

可以改成使用函数的版本

import socket
sk = socket.socket()
# 允许地址复用
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1',8080))
sk.listen()
def func1(conn):
    with open('test.html', 'rb') as f:
        f_data = f.read()
    conn.send(f_data)
    conn.close()
def func2(conn):
    with open('logo.png', 'rb') as f:
        f_data = f.read()
    conn.send(f_data)
    conn.close()

while 1:
    conn,addr = sk.accept()
    b_msg = conn.recv(1024)
    str_msg = b_msg.decode('utf-8')
    path = str_msg.split('\r\n')[0].split(' ')[1]
    print('path>>>',path)
    conn.send(b'HTTP/1.1 200 ok \r\n\r\n')
    if path == '/':
        func1(conn)
    elif path == '/logo.png':
        func2(conn)
sk.close()

现在还不支持高并发的情况,可以加上多线程

import socket
from threading import Thread

sk = socket.socket()
# 允许地址复用
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1',8080))
sk.listen()
def func1(conn):
    with open('test.html', 'rb') as f:
        f_data = f.read()
    conn.send(f_data)
    conn.close()
def func2(conn):
    with open('logo.png', 'rb') as f:
        f_data = f.read()
    conn.send(f_data)
    conn.close()

while 1:
    conn,addr = sk.accept()
    b_msg = conn.recv(1024)
    str_msg = b_msg.decode('utf-8')
    path = str_msg.split('\r\n')[0].split(' ')[1]
    print('path>>>',path)
    conn.send(b'HTTP/1.1 200 ok \r\n\r\n')
    if path == '/':
        t = Thread(target=func1,args=(conn,))
        t.start()
    elif path == '/logo.png':
        t = Thread(target=func2,args=(conn,))
        t.start()
sk.close()

替换字符串,实现不同的时间访问返回时间戳模拟动态内容

在网页中,用特殊的符号@@666@@表示需要被替换掉的地方修改html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试页面</title>
</head>
<body>
<h1>标题1</h1>
<img src="logo.png" alt="头像">
<h2>@@666@@</h2>
</body>
</html>

修改socket服务端

import socket
from threading import Thread
import time

sk = socket.socket()
# 允许地址复用
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1',8080))
sk.listen()
def func1(conn):
    with open('test.html', 'r',encoding="utf-8") as f:
        f_data = f.read()
        now = str(time.time())
        f_data = f_data.replace("@@666@@",now).encode('utf-8')
    conn.send(f_data)
    conn.close()
def func2(conn):
    with open('logo.png', 'rb') as f:
        f_data = f.read()
    conn.send(f_data)
    conn.close()

while 1:
    conn,addr = sk.accept()
    b_msg = conn.recv(1024)
    str_msg = b_msg.decode('utf-8')
    path = str_msg.split('\r\n')[0].split(' ')[1]
    print('path>>>',path)
    conn.send(b'HTTP/1.1 200 ok \r\n\r\n')
    if path == '/':
        t = Thread(target=func1,args=(conn,))
        t.start()
    elif path == '/logo.png':
        t = Thread(target=func2,args=(conn,))
        t.start()
sk.close()

WSGI

WSGIWeb Server Gateway Interface是一种规范它定义了使用Python编写的web应用程序与web服务器程序之间的接口格式实现web应用程序与web服务器程序间的解耦。

常用的WSGI服务器有uwsgi、Gunicorn。而Python标准库提供的独立WSGI服务器叫wsgirefDjango开发环境用的就是这个模块来做服务器。

模板渲染JinJa2

上面的过程中用@@666@@替换了动态的内容, 其实模板渲染有个现成的工具: jinja2

安装jinja2

pip install jinja2 -i https://pypi.tuna.tsinghua.edu.cn/simple

编写html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>jinja2</title>
</head>
<body>
    <h1>姓名:{{name}}</h1>
    <h1>爱好:</h1>
    <ul>
        {% for hobby in hobby_list %}
        <li>{{hobby}}</li>
        {% endfor %}
    </ul>
</body>
</html>

编写wsgi服务端

from wsgiref.simple_server import make_server
from jinja2 import Template


def index():
    with open("test.html", "r", encoding='utf-8') as f:
        data = f.read()
    template = Template(data)
    ret = template.render({"name": "张三", "hobby_list": ["抽烟", "喝酒", "烫头"]})
    return [bytes(ret, encoding="utf-8"), ]


URL_LIST = [
    ("/index/", index),
]


def run_server(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ])
    url = environ['PATH_INFO']
    func = None
    for i in URL_LIST:
        if i[0] == url:
            func = i[1]
            break
    if func:
        return func()
    else:
        return [bytes("404没有该页面", encoding="utf8"), ]


if __name__ == '__main__':
    http = make_server('', 8000, run_server)
    print("Serving HTTP on port 8000...")
    http.serve_forever()

下面演示从数据库中提取内容进行替换

-- 创建数据库
create database pywebdb;

-- 进入数据库中
use pywebdb

-- 创建表
CREATE TABLE user(
  id int auto_increment PRIMARY KEY,
  name CHAR(10) NOT NULL,
  hobby CHAR(20) NOT NULL
)engine=innodb DEFAULT charset=UTF8;

-- 插入一个数据
INSERT into user (name, hobby) VALUES ('张三', '抽烟,喝酒,烫头');

-- 查看插入的数据
SELECT * FROM user;

修改socket服务端

import pymysql
from wsgiref.simple_server import make_server
from jinja2 import Template

def index(name,hobby):
    with open("index.html", "r", encoding='utf-8') as f:
        data = f.read()
    template = Template(data)
    ret = template.render({"name":name, "hobby_list":hobby})
    return [bytes(ret, encoding="utf-8"),]

URL_LIST = [
    ("/index/", index),
]

def run_server(environ, start_response):
    conn = pymysql.connect(
        host="127.0.0.1",
        port=3306,
        user="root",
        passwd="test123",
        db="pywebdb",
        charset="utf8"
    )
    cursor = conn.cursor()
    cursor.execute('select * from user')
    data = cursor.fetchone()
    name=data[1]
    hobby=data[2]
    conn.close()
    start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'),])
    url = environ['PATH_INFO']
    func = None
    for i in URL_LIST:
        if i[0] == url:
            func = i[1]
            break
    if func:
        return func(name,hobby)
    else:
        return [bytes("404没有该页面", encoding="utf8"),]

if __name__ == '__main__':
    http = make_server('',8000,run_server)
    print("Serving HTTP on port 8000...")
    http.serve_forever()

模板的原理就是字符串替换我们只要在HTML页面中遵循jinja2的语法规则写上其内部就会按照指定的语法进行相应的替换从而达到动态的返回内容。