26.1 WEB框架介绍

1. Web应用程序

1.1 应用程序有两种模式

应用程序的架构模式主要分为两种: C/S (客户端/服务器端)和B/S(浏览器/服务器端).
* 1. C/S模式, 即客户端/服务器模式(Client/Server Model): 是一种分布式计算模式.
     它将应用程序的功能划分为客户端和服务器端两部分.
     在这种模式下, 客户端通常负责提供用户界面, 处理用户输入和显示输出,
     而服务器端则负责处理核心的业务逻辑, 数据存储和访问控制等任务.

* 2. B/S模式, 即浏览器/服务器端模式(Browser/Server Model): 是一种基于Web的分布式计算框架.
     它将应用程序的功能划分为浏览器端和服务器端两部分.
     在这种框架下, 用户通过Web浏览器作为客户端来访问和交互服务器端的应用程序.

     在B/S框架中, 浏览器端主要负责提供用户界面, 展示从服务器端接收到的数据和响应.
     它通常包括HTML页面, CSS样式表和JavaScript脚本等前端技术, 以创建丰富的用户界面和交互体验.
     浏览器端处理用户输入, 例如点击按钮, 填写表单等, 并将这些输入通过HTTP请求发送到服务器端.

     服务器端则负责处理核心的业务逻辑, 数据存储和访问控制等任务.
     它接收来自浏览器端的HTTP请求, 并根据请求的内容执行相应的业务逻辑操作.
     服务器端可以与数据库进行交互, 存储和检索数据, 以满足用户请求的需求.
     然后, 服务器端将处理结果以HTML页面, JSON数据或XML数据等格式发送给浏览器端进行展示.

1.2 C/S架构优点

C/S架构, 在计算机历史的长河中持续保持其重要地位, 其优点如下:
* 1. 客户端和服务器直接相连.
     更高的安全性: 客户端和服务器直接相连, 采用点对点的通信模式, 可以减少中间环节带来的安全风险, 从而提高数据传输的安全性.
     直接操作本地资源: 客户端直接访问和操作本地资源, : 数据库等, 这大大减少了获取资源的时间和精力, 提高了工作效率.     
     减少通信流量和成本: 由于直接相连, 减少了中间转发和路由的环节, 从而降低了通信流量, 对于客户来说可以节约网络使用费用.
     响应速度快: 直接相连减少了通信延迟, 使得客户端和服务器之间的响应速度更快,
     特别是在处理大量数据或复杂业务逻辑时, 这种优势尤为明显.

* 2. 客户端可以处理一些逻辑事务.
     充分利用硬件资源: C/S架构允许客户端和服务器各自处理部分逻辑事务, 从而充分利用两者的硬件资源, 避免资源的浪费.
     分担服务器负载: 客户端可以分担一部分数据处理和数据存储的任务, 从而减轻服务器的负担, 提高系统的整体性能.
     处理复杂事务流程: 客户端可以处理复杂的业务逻辑和事务流程, 如数据验证, 用户交互等, 使得整个系统更加灵活和高效.
     强大的功能和用户体验: 客户端通常拥有完整的应用程序界面和丰富的功能, 如出错提示, 在线帮助等, 可以提供更好的用户体验和支持.

* 3. 客户端操作界面. 
     提高视觉体验: C/S架构允许客户端拥有自定义的操作界面, 可以根据用户需求进行个性化设计, 提高用户的视觉体验.
     满足个性化需求: 客户端操作界面可以随意排列和配置, 充分满足用户的个性化需求, 展现不同的特点和个性.
     丰富的交互方式: 客户端操作界面通常提供丰富的交互方式, 如菜单, 按钮, 文本框等, 使得用户可以更加方便地进行操作和控制。

1.3 B/S架构优点

B/S架构, 在现代企业中广受欢迎, 特别是在广域网环境中, 其优点如下:
* 1. 广泛的访问性.
     标准的通信协议: 采用Internet上标准的通信协议(如TCP/IP)作为客户机与服务器通信的基础,
     使得任何位置的用户都能无障碍地访问服务器资源.
     高效的数据处理与共享: 服务器提供Web服务和数据库服务, 高效处理数据, 并通过标准协议实现数据共享.
* 2. 简化的数据处理流程.
     集中处理: 数据处理主要集中在服务器端进行, 大大简化了客户端的数据处理任务.
     快速响应: 服务器根据用户请求处理数据, 生成网页供客户端直接下载和展示, 提高了系统的整体性能和响应速度.
* 3. 简化的客户端需求.
     浏览器作为客户端: 用户不再需要为客户端单独编写和安装其他类型的应用程序, 只需安装内置浏览器的软件即可访问服务器数据.
     降低复杂性: 降低了客户端的复杂性, 减少了用户的学习和维护成本.
     良好的通用性和兼容性: 由于浏览器是计算机的标准设备, B/S架构具有很好的通用性和兼容性.

2. web应用介绍

Web应用程序(Web Application): 是一种可以通过Web浏览器访问的应用程序, 它遵循浏览器/服务器(Browser/Server, 简称B/S)架构.
在开发Web应用程序时, 通常会使用后端技术(如Java, Python, Ruby, PHP等)来处理服务器端逻辑和数据存储,
以及前端技术(如HTML, CSS, JavaScript等)来构建用户界面和提供用户交互.
此外, 还会使用数据库技术(如MySQL, PostgreSQL, MongoDB等)来存储和管理应用程序的数据.
以下是Web应用程序的一些特点:
* 1. 可扩展性: Web应用程序可以通过添加更多的服务器和负载均衡器来扩展其处理能力, 以满足更多的用户请求.
* 2. 安全性: Web应用程序可以通过使用HTTPS协议, 身份验证, 授权和加密技术来确保数据传输和存储的安全性.
* 3. 交互性: Web应用程序通常使用HTML, CSS, JavaScript等前端技术来提供丰富的用户交互体验,
     如表单输入, 动态内容更新, 动画效果等.

2.1 Web应用程序的优点

以下是Web应用程序的一些优点:
* 1. 便捷性: Web应用程序无需复杂的安装过程.
     用户只需拥有一个合适的浏览器, 即可随时随地访问应用程序, 极大地提高了使用的便捷性.
* 2. 节省硬盘空间: 与传统的桌面应用程序相比, Web应用程序通常几乎不占用或仅占用极少的用户硬盘空间.
     这是因为应用程序的主要数据和功能都存储在服务器上, 用户端仅加载所需的界面和交互组件.
* 3. 自动更新: Web应用程序无需用户手动更新.
     一旦服务器上的应用程序进行了更新或添加了新特性, 用户在下一次访问时就会自动接收到这些更新.
     这不仅简化了用户操作, 也确保了所有用户都能享受到最新的功能和安全修复.
* 4. 易于集成: Web应用程序可以与其他Web服务(如数据库, API, 第三方服务等)进行集成, 以实现更复杂的功能和数据交换.
     这种集成能力使得Web应用程序能够提供更丰富, 更全面的服务, 满足用户的多样化需求.
* 5. 跨平台性: 由于Web应用程序在网络浏览器窗口中运行, 它们通常能够跨多个操作系统和平台使用.
     这意味着无论用户使用的是Windows, Mac, Linux还是其他操作系统, 
     都能获得一致的使用体验, 极大地提高了应用程序的普适性和可访问性.

2.2 Web应用程序的缺点

以下是Web应用程序的一些缺点:
* 1. 浏览器兼容性: Web应用程序高度依赖浏览器的适用性.
     如果浏览器不支持特定的功能, 或者停止支持某些较旧的平台或操作系统版本, 将直接影响大量用户的使用体验.
* 2. 网络连接依赖性: 由于Web应用程序运行在远程服务器上, 一旦网络连接出现问题, 应用程序将无法正常使用, 这给用户带来了很大的不便.
* 3. 定制化与个性化受限: 许多Web应用程序不是开源的, 用户只能依赖第三方提供的服务, 因此难以进行个性化的定制和修改.
     同时, 由于依赖在线服务, 用户通常无法离线使用这些应用程序, 限制了其使用的灵活性.
* 4. 服务商的可及性风险: Web应用程序完全依赖于应用服务商的运营.
     如果公司倒闭或服务器停止使用, 用户将无法访问以前的数据, 这带来了数据丢失的风险.
     相比之下, 传统安装的软件即使制造商倒闭, 用户仍可以继续使用, 尽管可能无法获得更新或用户支持(联网变单机).
* 5. 服务商控制权过大: 服务商对Web应用程序及其功能拥有更大的控制权.
     他们可以随意添加新特性, 即使这些特性并不符合用户的需求或期望.
     此外, 服务商还可以在不通知用户的情况下减少服务带宽以削减成本, 从而影响用户体验.
* 6. 隐私安全问题: 由于Web应用程序需要收集和处理用户数据, 服务商理论上可以检索和分析用户的任何行为.
     这引发了用户对隐私安全的担忧, 尤其是在缺乏有效监管和透明度的环境中.

3. 简单的WEB服务器

构建一个简易的HTTP服务器, 借助Python的socket库来实现网络监听功能, 并处理来自客户端的连接请求, 从而提供HTTP响应服务.

3.1 HTTP响应

要通过HTTP协议发送响应给客户端(例如一个Web浏览器), 需要确保你遵循了HTTP协议的格式.
HTTP响应由: 状态行(HTTP/1.1 200 OK), 响应头(目前空着)和响应体组成, 响应体紧跟在响应头之后, 用两个\r\n来分隔响应头和响应体.
例如: HTTP/1.1 200 OK \r\n\r\n 响应体. 
访问服务端地址: http://127.0.0.1:8008/, 返回index页面, 页面中当前时间.
import socket
import time


# 定义一个函数, 用于运行服务器
def server_run():
    # 创建一个新的socket对象
    soc = socket.socket()

    # 绑定socket到本机的8008端口, '127.0.0.1'是本地回环地址, 也就是这台计算机本身
    soc.bind(('127.0.0.1', 8008))  # 访问地址: http://127.0.0.1:8008/

    # 开始监听连接, 最大挂起连接数为5
    soc.listen(5)

    # 通信循环, 不断地监听来自客户端的连接请求
    while True:
        # 接受一个新的连接请求, 并返回一个新的socket对象和一个地址元组
        conn, addr = soc.accept()

        # 从客户端接收最多1024字节的数据
        recv_data = conn.recv(1024)
        # 这里的参数指定了接收缓冲区的大小, 即单次接收的最大字节数.
        # 但是, 这个值并不限制你可以从连接中接收的总数据量, 它只是限制了单次recv调用返回的最大数据量.

        print(recv_data)  # 打印接收到的数据(通常是一个HTTP请求)

        # 获取当前时间, 并格式化为字符串
        now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        print(now)  # 打印当前时间, 用于调试或日志记录

        # 打开名为index页面, 并读取其内容
        with open('index.html', 'r', encoding='utf-8') as f:
            data = f.read()

            # 在HTML内容中查找'@@@'字符串,并将其替换为当前时间
        data = data.replace('@@@', now)

        # 构造HTTP响应, 并发送给客户端
        # 注意: 这里简化了HTTP响应的构建, 仅作为一个简单的示例
        response = 'HTTP/1.1 200 OK\r\n\r\n' + data  # 响应行, 响应体
        conn.send(response.encode('utf-8'))  # 发送响应(确保它是字节串)

        # 关闭与客户端的连接
        conn.close()


if __name__ == '__main__':
    server_run()

编写服务器读取的前端页面.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>web应用</title>
</head>
<body>
<h2>@@@</h2>
</body>
</html>
启动服务端, 访问: 127.0.0.1:8008, 访问成功后, 读取一个HTML文件(index.html), 在其中找到字符串'@@@', 并将其替换为当前时间.

image-20240612180858417

3.2 HTTP请求

# 第一次访问根路径(/).
b'GET / HTTP/1.1\r\n
Host: 127.0.0.1:8008\r\n
Connection: keep-alive\r\n
sec-ch-ua: "Google Chrome";v="125", "Chromium";v="125", "Not.A/Brand";v="24"\r\n
sec-ch-ua-mobile: ?0\r\n
sec-ch-ua-platform: "Windows"\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36\r\n
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\r\n
Sec-Fetch-Site: none\r\nSec-Fetch-Mode: navigate\r\n
Sec-Fetch-User: ?1\r\nSec-Fetch-Dest: document\r\n
Accept-Encoding: gzip, deflate, br, zstd\r\n
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8\r\n
Cookie: sessionid=xqu7m1ffngra339r34i9sh44zsk3hcll\r\n\r\n'

# 第二次访问 /favicon.ico , 获取网站图标(页面自动发送的请求).
b'GET /favicon.ico HTTP/1.1\r\n
Host: 127.0.0.1:8008\r\nConnection: keep-alive\r\n
sec-ch-ua: "Google Chrome";v="125", "Chromium";v="125", "Not.A/Brand";v="24"\r\n
sec-ch-ua-mobile: ?0\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36\r\n
sec-ch-ua-platform: "Windows"\r\n
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\n
Sec-Fetch-Site: same-origin\r\n
Sec-Fetch-Mode: no-cors\r\
nSec-Fetch-Dest: image\r\n
Referer: http://127.0.0.1:8008/\r\n
Accept-Encoding: gzip, deflate, br, zstd\r\n
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8\r\n
Cookie: sessionid=xqu7m1ffngra339r34i9sh44zsk3hcll\r\n\r\n'
请求头是从GET / HTTP/1.1\r\n开始, 一直到最后的空行\r\n\r\n之前.
空行\r\n\r\n之后是请求体, 但在GET请求中, 请求体通常是空的.
在POST或PUT等请求中, 请求体会包含发送到服务器的数据.
例子中, 服务器代码读取了请求数据(虽然它只读取了前1024个字节, 但在这个请求的情况下应该足够了), 下面是对请求信息的简单解释:
* 1. GET / HTTP/1.1: 这是一个HTTP GET请求, 请求的资源路径是根路径(/), 使用的HTTP协议版本是1.1.
* 2. Host: 127.0.0.1:8008: 请求的目标主机是127.0.0.1(本地主机), 端口是8008.
* 3. Connection: keep-alive: 客户端希望保持这个TCP连接, 以便后续可以复用, 而不是每次请求都建立一个新的连接.
* 4. sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform等:
     这些是Google Chrome和其他基于Chromium的浏览器用来发送用户代理信息的字段, 以便网站可以根据这些信息优化内容或行为.
* 5. DNT: 1: 表示客户端不希望被追踪(Do Not Track).
* 6. Upgrade-Insecure-Requests: 1: 如果服务器上有更安全的版本(比如HTTPS), 客户端希望自动升级到那个版本.
* 7. User-Agent: 这是一个字符串, 描述了发出请求的客户端(比如浏览器)的类型, 版本, 操作系统等信息.
* 8. Accept: 客户端可以接受的媒体类型(比如HTML, XML, 图片等).
* 9. Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-User, Sec-Fetch-Dest:
     这些是浏览器发送的安全相关的请求头, 用于防止跨站请求伪造(CSRF)等攻击.
* 10. Accept-Encoding: 客户端可以接受的编码方式(比如gzip压缩).
* 11. Accept-Language: 客户端首选的语言.
* 12. Cookie: 如果客户端之前与服务器建立了会话, 它可能会发送一个或多个cookie, 用于会话跟踪或身份验证.
# 如果发送的数据很长可以循环获取, 添加下面的代码
request_data = b''  
while True:  
    # 读取1024字节数据. 或者如果数据不足1024字节, 则读取剩余数据  
    data = client_socket.recv(1024)  
    if not data:  
        break  # 如果没有数据可读,说明请求已结束  
        request_data += data  
favicon.ico图标用于收藏夹图标和浏览器标签上的显示, 
一般浏览器会请求网站根目录的这个图标, 如果网站根目录也没有这图标会返回404.

网站图标(通常也被称为favicon或网站LOGO)的大小和尺寸可以根据不同的用途和设计需求而有所不同.
最小的图标尺寸通常是16x16像素. 这种尺寸的图标经常用于显示在浏览器的标签页上, 或者在某些操作系统中显示在浏览器的书签栏中.

出于优化的考虑, 要么就有这个图标, 要么就禁止产生这个请求.	
禁止方式: 在html页面的 <head> 区域, 加上如下代码实现屏蔽.
<link rel="icon" type="image/x-icon" href="data:image/x-icon;base64, 无效的Base64编码...">
在这个特定的例子中, 该标签的href属性使用了一个数据URI(Data URI)来尝试加载一个图标, 但实际上并没有提供任何有效的图标数据.

rel="icon": 指定了链接的关系类型, 即这个链接是一个图标.
href="data:;base64, 无效的Base64编码...": 这里使用了数据URI来尝试直接嵌入图标数据.
但是, base64, 后面没有跟随任何有效的Base64编码的数据, 所以实际上这个链接指向了一个空的数据源.
想要使用请查看base64作为图标:
https://blog.csdn.net/qq_46137324/article/details/139640131?spm=1001.2014.3001.5502

3.3 请求页面

基于请求的URI部分(/index/)来决定如何响应.
例如: 用户访问: 127.0.0.1:8080/index/ , 后端获取请求地址返回特定的HTML页面.
3.3.1 编写思路
思路逻辑描述:
* 1. 接收并解析请求.
     1.1 服务端从客户端接收HTTP请求.
     1.2 请求数据通常是二进制格式的, 但在处理时, 服务器会将其解析为文本格式.
     1.3 提取URI路径: 从请求行中提取URI部分(/index/).
* 2. 响应判断: 判断提取出的URI路径是否与想要响应的路径(在这种情况下是/index/)匹配.
     如果匹配, 执行相应的处理逻辑(如发送特定的HTML页面, JSON数据或其他资源).
* 3. 构建并发送响应: 根据URI路径的匹配结果, 构建HTTP响应并将响应发送回客户端.
当浏览器发送一个HTTP请求时, 它发送的是一个按照HTTP协议格式组织的文本字符串.
然而, 当这个字符串通过TCP/IP协议栈在网络中传输时, 它确实以字节(即二进制形式)发送的.

在服务器端, 套接字(socket)读取数据时, 会得到一个字节串(bytes), 这个字节串是HTTP请求的原始二进制表示.
需要将这些字节解码为字符串, 以便能够解析和处理HTTP请求的各个部分.

对于HTTP/1.1, 规范(RFC 2616)明确提到:
请求行, 状态行和头部字段中的字段名(Field Name)必须是ASCII字符.
头部字段中的字段值(Field Value)可以是任何字符集, 但通常建议使用ASCII或ISO-8859-1, 除非有特定的内容类型需要其他编码(如UTF-8).
请求体和响应体中的编码方式取决于Content-Type头部字段中的charset参数.
如果没有指定charset参数, 则默认可能依赖于上下文或应用层协议(对于许多现代Web应用, 默认使用UTF-8编码是常见的做法). 

涉及两种编码: ASCII与UTF-8.
为了兼容性和健壮性, 使用UTF-8解码是可行的, 因为UTF-8是ASCII的超集, 并且可以正确地解码所有ASCII字符.
# http请求的二进制数
b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\n.....'

# 解码后的字符串数据
GET / HTTP/1.1  # 这里/r/n生效呈现换行效果
Host: 127.0.0.1:8000
...
3.3.2 前端页面
在Web开发中, "index页面", "home页面""/"页面经常会被提到, 它们通常指的是网站的主要入口点或起始页.
虽然这些术语有时可以互换使用, 但它们在某些上下文中可能有细微的差别.
以下是对这三个术语的解释:
* 1. index页面: 通常指的是一个网站目录中的默认页面, 当访问一个目录时没有指定具体文件名时, 服务器会自动提供该目录下的index页面.      例如, 在Apache或Nginx等Web服务器上, 默认的index页面可能是index.html, index.htm, index.php等
     在很多情况下, index页面就是一个网站的home页面, 但并非总是如此.
* 2. home页面: 指的是网站的主页, 即用户访问网站时看到的第一个页面.
     它通常是网站的起点, 提供了网站内容的概览, 导航到其他页面的链接以及网站的主要功能和信息.
     在很多网站架构中, home页面与index页面是同一个页面, 但也有可能不是.
     例如, 一个复杂的网站可能有一个单独的home页面设计, 而index页面只是一个简单的引导页或者登录页.
* 3. "/"页面: 在Web开发中, "/"通常代表网站的根URL. 当用户访问网站的根URL时, 服务器通常会提供index页面或home页面.
     在许多Web框架和CMS(内容管理系统), "/"路由或路径默认映射到home页面或index页面.
目录结构如下:
Project:
  |--404.html
  |--favicon.ico
  |--music.html
  |--index.html
  |--main.py
  |--movie.html
图标:

image-20240614000543880

<!-- 404.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>网页不存在</title>
</head>
<body>
<h1>网页不存在</h1>
</body>
</html>
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>网页不存在</title>
</head>
<body>
<h1>网页不存在</h1>
</body>
</html>
<!-- music.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>音乐</title>
</head>
<body>
<h1>音乐列表</h1>
</body>
</html>
<!-- movie.html -->
<!DOCTYPE html> 
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>电影</title>
</head>
<body>
<h1>电影列表</h1>
</body>
</html>
3.3.3 web服务器
# main.py
import socket


# 2. 获取请求路径
def get_path(binary_response):
    # 打印接收HTTP请求(二进制数据, b'')
    # print(f'请求数据>>>:\n{binary_response}')

    # 对请求信息进行解码
    str_request = binary_response.decode('UTF-8')
    # 打印解码后的请求信息
    # print(f'请求数据>>>:\n{str_request}')

    # 观察数据特性, 对数据进行切分, 获取请求地址
    request_line = str_request.split('\r\n')[0]  # 得到请求行, 例:'GET / HTTP/1.1'
    print(f'请求头信息>>>:{request_line}')  # 查看请求行

    # 分割请求行以获取方法, 路径和协议版本
    _, path, _ = request_line.split(' ')
    return path


# 响应正文
def response_body(file_name):
    # 打开名为index页面, 并以二进制模式读取内容
    with open(file_name, 'rb') as f:
        binary_html = f.read()

    return binary_html


# 匹配路径
def match_path(path, conn):
    # 定义页面字典
    html_dict = {
        '/': 'index.html',  # 主页
        '/music': 'music.html',  # 音乐
        '/movie': 'movie.html',  # 电影
        '/favicon.ico/': 'favicon.ico'  # 头像
    }
    if path in html_dict:
        binary_html = response_body(html_dict.get(path))
    else:
        binary_html = response_body('404.html')

    """
    先发响应头再发信息
    http特性:当数据量比较小且时间间隔比较短的多次数据,
    那么TCP会自动打包成一个数据包发送.
    """
    conn.send(b'HTTP/1.1 200 ok\r\n\r\n')
    # 发送或接收数据的代码
    conn.send(binary_html)
    # 关闭与客户端的连接
    conn.close()


# 1. 定义一个函数, 用于运行服务器
def server_run():
    soc = socket.socket()
    soc.bind(('127.0.0.1', 8000))
    soc.listen(5)
    print(
        """      
        访问主页: http://127.0.0.1:8000
        聆听音乐: http://127.0.0.1:8000/music
        观赏电影: http://127.0.0.1:8000/movie
        """)
    # 通信循环
    while True:
        conn, addr = soc.accept()
        request_data_binary = conn.recv(1024)
        print(f'请求数据长度>>>:{len(request_data_binary)}')

        path = get_path(request_data_binary)  # 获取请求地址
        match_path(path, conn)  # 返回请求页面


# 0. 启动函数
if __name__ == '__main__':
    server_run()

遇到以下两种情况:
* 1. 正常访问的时候多出一个空请求.
* 2. 关闭网页的时候多出一个空请求.

GIF 2024-6-13 23-46-40

GIF 2024-6-13 23-43-27

TCP 连接已关闭: 当客户端正常关闭TCP连接时, 服务器端的recv()调用可能会返回一个空字节串(b''),
表示没有更多的数据可以读取, 并且连接已经被关闭.
但是, 这并不是一个可靠的信号, 因为recv()也可以在其他情况下返回空字节串, 比如当没有数据可读时.

客户端(可能是浏览器)在发送了/favicon.ico请求后关闭了连接, 通样可能会出现上述问题.
(是可能, 不是百分百出现)
解决方案: 获取请求数据的时候, 对数据进行判断, 如果请求行为空字节串, 那么可以确认连接已经被对方关闭.
则关闭连接, 跳过本次循环.
request_data_binary = conn.recv(1024)
if not len(request_data_binary):  # 正常情况下, 730多个字符
    conn.close()  # 关闭连接
    continue  # 下次通信 
import socket


def get_path(binary_response):
    str_request = binary_response.decode('UTF-8')
    request_line = str_request.split('\r\n')[0]  # 得到请求行, 例:'GET / HTTP/1.1'
    print(f'请求头信息>>>:{request_line}')  # 查看请求行
    _, path, _ = request_line.split(' ')
    return path


# 响应正文
def response_body(file_name):
    with open(file_name, 'rb') as f:
        binary_html = f.read()

    return binary_html


# 匹配路径
def match_path(path, conn):
    html_dict = {
        '/': 'index.html',  # 主页
        '/music': 'music.html',  # 音乐
        '/movie': 'movie.html',  # 电影
        '/favicon.ico/': 'favicon.ico'  # 头像
    }
    if path in html_dict:
        binary_html = response_body(html_dict.get(path))
    else:
        binary_html = response_body('404.html')

    conn.send(b'HTTP/1.1 200 ok\r\n\r\n')
    conn.send(binary_html)
    conn.close()


def server_run():
    soc = socket.socket()
    soc.bind(('127.0.0.1', 8000))
    soc.listen(5)
    print(
        """      
        访问主页: http://127.0.0.1:8000
        聆听音乐: http://127.0.0.1:8000/music
        观赏电影: http://127.0.0.1:8000/movie
        """)

    while True:
        conn, addr = soc.accept()
        request_data_binary = conn.recv(1024)
        if not len(request_data_binary):  # 正常情况下, 730多个字符
            conn.close()  # 关闭连接
            continue  # 下次通信
        print(f'请求数据长度>>>:{len(request_data_binary)}')

        path = get_path(request_data_binary)  # 获取请求地址
        match_path(path, conn)  # 返回请求页面


if __name__ == '__main__':
    server_run()

4. wsgiref模块

4.1 wsgiref模块介绍

wsgiref是Python标准库中的一个模块, 它实现了Web服务器网关接口(WSGI, Web Server Gateway Interface)的规范和实用程序.
WSGI是一个为Python语言定义的Web服务器和Web应用程序或框架之间的简单而通用的接口.
wsgiref模块提供了WSGI工具, 服务器和中间件组件, 它们对于开发, 测试或理解WSGI应用程序和服务器非常有用.

wsgiref模块的主要作用:
* 1. 解析HTTP请求: 当HTTP请求到达时, wsgiref或其他WSGI服务器会解析该请求(包括方法, 路径, 查询参数, 头部等),
     并将这些解析后的信息封装成一个字典(通常称为environ字典)和一个输入流(通常用于读取请求体).
     这个字典和流会作为参数传递给WSGI应用程序.

     environ字典包含了大量的关于请求的信息, 如客户端的IP地址, 请求的路径, 请求头, 查询参数等.
     这使得开发者可以轻松地访问这些信息, 而无需直接处理原始的HTTP数据.

* 2. 响应打包成HTTP格式: 当WSGI应用程序处理完请求并准备返回响应时, 
     它会返回一个状态码, 一个响应头字典和一个输出流(用于写入响应体).
     wsgiref服务器或其他WSGI服务器会负责将这些信息打包成一个符合HTTP格式的响应, 并发送给客户端(如浏览器).

* 3. 提供简单的HTTP服务器: wsgiref.simple_server提供了一个简单的HTTP服务器实现, 它允许你快速运行和测试WSGI应用程序.
     虽然这个服务器在生产环境中可能不够健壮和高效, 但对于开发和测试来说非常方便.

* 4. 中间件支持: WSGI还支持中间件组件, 这些组件可以插入到应用程序和服务器之间, 
     用于执行各种任务, 如身份验证, 日志记录, URL重写等.
     wsgiref提供了一些用于构建中间件的实用程序.

总的来说, wsgiref模块使得Python开发者能够更容易地理解和使用WSGI接口, 从而开发出符合标准的Web应用程序和服务器.
虽然它可能不是生产环境中首选的解决方案, 但对于学习, 开发和测试来说, 它是一个非常有价值的工具.
下列代码示例展示了如何使用wsgiref.simple_server创建一个简单的WSGI服务器, 并定义了一个处理请求的run函数.

4.2 wsgiref模块使用

添加Content-Type响应头部来指明返回的内容类型是纯文本, 这样做更符合HTTP协议的标准实践.
# 首先导入make_server函数, 这个函数用于创建一个简单的WSGI服务器.
from wsgiref.simple_server import make_server


# 接着, 定义wsgi_app的函数, 这个函数接受两个参数: environ和start_response.
# environ是一个包含请求相关数据的字典, 而start_response是一个用于设置响应状态码和响应头的回调函数.
def wsgi_app(environ, start_response):
    # 处理请求逻辑...  
    print(environ)
    # 设置响应状态码和头部  
    status = '200 OK'
    headers = [('Content-Type', 'text/html')]
    start_response(status, headers)  # 在这里调用start_response, 设置响应头

    # 返回响应体  
    return [b'Hello, World!']  # 返回一个可迭代对象, 包含响应体数据


# 创建了服务器实例, 并指定了它应该监听的地址('127.0.0.1', 即本地主机)和端口(8080).
if __name__ == '__main__':
    server = make_server('127.0.0.1', 8080, wsgi_app)  # 创建服务器实例  
    print(type(server))  # 打印服务器实例的类型, 应该是 wsgiref.simple_server.WSGIServer  
    server.serve_forever()  # 启动服务器, 并让它一直运行, 直到手动停止
    
由于WSGI要求响应体是字节串(byte strings), 所以需要确保字符串被正确编码为字节串.
start_response函数的第一个参数是状态码和状态消息, 它们之间应该用空格隔开.
状态消息是"200 OK" (即OK大写), 虽然HTTP状态消息对大小写不敏感, 但按照惯例, 它们通常是大写的.

在WSGI协议中, wsgi_app函数(通常称为WSGI应用程序函数)需要返回一个可迭代对象, 该对象包含要发送给客户端的响应体(body)的内容.
这个可迭代对象通常是一个字节序列(byte sequence)的列表或生成器.

在WSGI协议中, 直接调用类似start_response('200 OK', [])并不会立刻发送HTTP响应给客户端.
start_response函数的调用是为了设置HTTP响应的状态码和头部, 但它本身并不负责发送任何数据.
start_response函数是一个由WSGI服务器提供的回调函数, 用于通知WSGI应用程序如何开始一个HTTP响应.
当WSGI应用程序调用response函数时, 它实际上是在告诉WSGI服务器:'我准备好了响应的状态码和头部, 你可以开始发送它们了.'
但是, 这并不意味着响应体也会被立刻发送.

4.3 wsgiref模块请求信息

启动服务端, 在浏览器中输入: 127.0.0.1:8080 .

image-20240614031320023

# 查看终端的信息:
127.0.0.1 - - [14/Jun/2024 01:58:54] "GET / HTTP/1.1" 200 12
127.0.0.1 - - [14/Jun/2024 01:58:54] "GET /favicon.ico HTTP/1.1" 200 12

{'ALLUSERSPROFILE': 'C:\\ProgramData',
'APPDATA': 'C:\\Users\\blue\\AppData\\Roaming', 
'COMMONPROGRAMFILES': 'C:\\Program Files\\Common Files', 
'COMMONPROGRAMFILES(X86)': 'C:\\Program Files (x86)\\Common Files', 
'COMMONPROGRAMW6432': 'C:\\Program Files\\Common Files', 
'COMPUTERNAME': 'BLUE', 
'COMSPEC': 'C:\\Windows\\system32\\cmd.exe',
'DRIVERDATA': 'C:\\Windows\\System32\\Drivers\\DriverData', 
'FPS_BROWSER_APP_PROFILE_STRING': 'Internet Explorer', 
'FPS_BROWSER_USER_PROFILE_STRING': 'Default', 
'GETTEXTCLDRDIR': 'd:\\Program Files\\gettext-iconv\\lib\\gettext', 
'HOMEDRIVE': 'C:',
'HOMEPATH': '\\Users\\blue', 
'IDEA_INITIAL_DIRECTORY': 'C:\\Users\\blue\\Desktop', 
'LOCALAPPDATA': 'C:\\Users\\blue\\AppData\\Local',
'LOGONSERVER': '\\\\BLUE', 
'NUMBER_OF_PROCESSORS': '20', 
'OS': 'Windows_NT', 
'PATH': 'D:\\Python\\Python3.11\\Scripts\\;D:\\Python\\Python3.11\\;
D:\\Python\\Python38\\Scripts\\;
D:\\Python\\Python38\\;
D:\\Python\\Python36\\Scripts\\;
D:\\Python\\Python36\\;
C:\\Windows\\system32;
C:\\Windows;
C:\\Windows\\System32\\Wbem;
C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;
C:\\Windows\\System32\\OpenSSH\\;
;d:\\Program Files\\gettext-iconv\\bin;
D:\\Program Files\\Redis\\', 
'PATHEXT': '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY;.PYW', 
'PROCESSOR_ARCHITECTURE': 'AMD64', 
'PROCESSOR_IDENTIFIER': 'Intel64 Family 6 Model 154 Stepping 3,
GenuineIntel',
'PROCESSOR_LEVEL': '6',
'PROCESSOR_REVISION': '9a03', 
'PROGRAMDATA': 'C:\\ProgramData', 
'PROGRAMFILES': 'C:\\Program Files',
'PROGRAMFILES(X86)': 'C:\\Program Files (x86)',
'PROGRAMW6432': 'C:\\Program Files', 
'PSMODULEPATH': 'C:\\Program Files\\WindowsPowerShell\\Modules;
C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules', 
'PUBLIC': 'C:\\Users\\Public', 
'PYCHARM_DISPLAY_PORT': '63342', 
'PYCHARM_HOSTED': '1', '
PYTHONIOENCODING': 'UTF-8', 
'PYTHONPATH': 'D:\\pythonProject;D:/Program Files/JetBrains/PyCharm 2023.1.4/plugins/python/helpers/pycharm_matplotlib_backend;
D:/Program Files/JetBrains/PyCharm 2023.1.4/plugins/python/helpers/pycharm_display', 
'PYTHONUNBUFFERED': '1',
'SESSIONNAME': 'Console', 
'SYSTEMDRIVE': 'C:', 
'SYSTEMROOT': 'C:\\Windows',
'TEMP': 'C:\\Windows\\TEMP',
'TMP': 'C:\\Windows\\TEMP',
'USERDOMAIN': 'BLUE', 
'USERDOMAIN_ROAMINGPROFILE': 'BLUE', 
'USERNAME': 'blue', 
'USERPROFILE': 'C:\\Users\\blue',
'WINDIR': 'C:\\Windows', 
'ZES_ENABLE_SYSMAN': '1', 
'SERVER_NAME': 'blue', 
'GATEWAY_INTERFACE': 'CGI/1.1',
'SERVER_PORT': '8080', 
'REMOTE_HOST': '', 
'CONTENT_LENGTH': '', 
'SCRIPT_NAME': '', 
'SERVER_PROTOCOL': 'HTTP/1.1', 
'SERVER_SOFTWARE': 'WSGIServer/0.2',
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/', 
'QUERY_STRING': '', 
'REMOTE_ADDR': '127.0.0.1', 
'CONTENT_TYPE': 'text/plain',
'HTTP_HOST': '127.0.0.1:8080',
'HTTP_CONNECTION': 'keep-alive',
'HTTP_CACHE_CONTROL': 'max-age=0',
'HTTP_SEC_CH_UA': '"Microsoft Edge";v="125", 
"Chromium";v="125", "Not.A/Brand";v="24"', 
'HTTP_SEC_CH_UA_MOBILE': '?0',
'HTTP_SEC_CH_UA_PLATFORM': '"Windows"',
'HTTP_DNT': '1', 
'HTTP_UPGRADE_INSECURE_REQUESTS': '1',
'HTTP_USER_AGENT': 
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) A
ppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0', 
'HTTP_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', 
'HTTP_SEC_FETCH_SITE': 'none', 
'HTTP_SEC_FETCH_MODE': 'navigate', 
'HTTP_SEC_FETCH_USER': '?1',
'HTTP_SEC_FETCH_DEST': 'document',
'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br, zstd', 
'HTTP_ACCEPT_LANGUAGE': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 
'HTTP_COOKIE':
'csrftoken=PqreiKjPPgOJq09xzoB6hdkt5IDEOIr1; 
sessionid=tmuiigt635exaogsh3c2k5fxgecvgcmk', 
'wsgi.input': <_io.BufferedReader name=504>, 
'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>, 
'wsgi.version': (1, 0), 
'wsgi.run_once': False, 
'wsgi.url_scheme': 'http', 
'wsgi.multithread': False, 
'wsgi.multiprocess': False, 
'wsgi.file_wrapper': <class 'wsgiref.util.FileWrapper'>}
以下是对这些环境变量的一些简要解释:
ALLUSERSPROFILE: 指向所有用户共享的配置文件的目录.
APPDATA: 当前用户的应用程序数据目录.
COMMONPROGRAMFILES: 指向程序文件(公共)的目录, 通常是32位和64位应用共享的.
COMMONPROGRAMFILES(X86): 指向32位应用程序的公共文件目录.
COMPUTERNAME: 计算机的名称.
COMSPEC: 指定命令解释器(通常是cmd.exe)的位置.
DRIVERDATA: 与驱动程序数据相关的目录.
FPS_BROWSER_APP_PROFILE_STRING  FPS_BROWSER_USER_PROFILE_STRING:
与某种浏览器配置相关的字符串(可能是特定于某个软件或库的).
GETTEXTCLDRDIR: gettext库(一个国际化和本地化库)的目录.
HOMEDRIVE  HOMEPATH: 定义用户的主目录( C:\Users\blue).
IDEA_INITIAL_DIRECTORY: 与某种IDE(可能是IntelliJ IDEA或PyCharm)的初始目录有关.
LOCALAPPDATA: 当前用户的应用程序本地数据目录.
LOGONSERVER: 登录到的服务器名称(在单用户系统中, 这通常是计算机的名称).
NUMBER_OF_PROCESSORS: 计算机上的处理器数量。
OS: 操作系统类型(这里是Windows_NT).
PATH: 定义操作系统查找可执行文件的目录列表.
PATHEXT: 可执行文件的扩展名列表.
PROCESSOR_ARCHITECTURE: 处理器架构(这里是AMD64, 64).
PROCESSOR_IDENTIFIER: 处理器的标识符, 包括制造商, 型号等信息.
PROGRAMDATA, PROGRAMFILES, PROGRAMFILES(X86), PROGRAMW6432: 与程序文件和数据相关的目录.
PSMODULEPATH: PowerShell模块的路径.
PUBLIC: 所有用户的公共目录.
PYCHARM_DISPLAY_PORT  PYCHARM_HOSTED: 与PyCharm IDE的远程开发或调试相关的设置.
PYTHONIOENCODING: Python的输入/输出编码设置.
PYTHONPATH: Python解释器搜索模块的目录列表.
SERVER_NAME  SERVER_PORT: 这两个变量分别表示接收请求的Web服务器的名称和端口号.
对于本地开发环境, SERVER_NAME可能是'localhost''127.0.0.1, 而SERVER_PORT通常是您指定的端口, 如'8080'.
REQUEST_METHOD: 表示HTTP请求的方法, 'GET', 'POST', 'PUT'. 这个信息对于确定如何响应请求至关重要.
PATH_INFO: 表示请求的路径信息, 即URL中服务器名称和端口号之后的部分(不包括查询字符串).
例如, 在URL 'http://localhost:8080/hello?name=world' , PATH_INFO的值是'/hello'.
QUERY_STRING: 表示URL中的查询字符串部分, 即?之后的部分.
例如, 在上面的URL中, QUERY_STRING的值是'name=world'.
REMOTE_ADDR: 表示发出请求的客户端的IP地址.
对于本地开发, 这通常是'127.0.0.1', 表示请求来自本地机器.
HTTP_* 开头的变量: 这些变量包含了HTTP请求头的所有信息.
例如,HTTP_HOST表示请求的主机名和端口号, HTTP_USER_AGENT表示发出请求的浏览器或客户端的类型和版本.
wsgi.* 开头的变量: 这些是WSGI规范定义的特殊变量, 提供了有关WSGI环境和请求的其他信息.
例如, wsgi.input是一个可读的输入流, 用于读取HTTP请求的主体部分;
wsgi.errors是一个可写的输出流, 用于记录错误信息;
wsgi.url_scheme表示URL的协议部分('http''https').

这些环境变量在编写脚本, 配置软件或进行系统级操作时都非常有用,
它们允许您引用特定的目录, 配置或系统资源, 而无需硬编码具体的路径或值.
当浏览器发送一个HTTP请求时, 无论是通过Socket直接接收还是通过WSGI服务器接收, 底层传输的都是相同的字节流.
但是, Socket和WSGI在如何处理这些字节流上有所不同, 这导致了它们'解析'出的信息在表现形式和易用性上的差异.

Socket的解析: 使用Socket直接接收数据时, 会接收到一个原始的字节流, 这个字节流包含了完整的HTTP请求.
这需要自己编写代码来解析这个字节流, 识别出HTTP请求的各个部分(如请求行, 请求头部, 请求体等), 并将这些信息提取出来.
这个过程比较底层, 要您了解HTTP协议的细节.

WSGI的解析: WSGI服务器(如wsgiref)在接收到Socket传来的字节流后, 
会负责解析这个字节流, 并将其转换为一个WSGI环境字典(也称为environ字典).
这个环境字典包含了HTTP请求的所有相关信息, 如请求方法, 路径, 头部, 查询参数等, 
以及服务器的一些环境信息(如服务器的名称, 端口号, 客户端的IP地址等).
这些信息都被封装在字典的键值对中, 方便Web应用程序通过标准的接口来访问.

WSGI服务器之所以能够解析出环境变量的信息, 是因为它在解析HTTP请求时, 不仅提取了HTTP请求本身的信息,
还根据服务器的配置和上下文环境生成了一些额外的信息, 并将这些信息也包含在环境字典中.
这些额外的信息对于Web应用程序来说是非常有用的, 比如它们可以帮助应用程序确定请求的来源, 目标地址, 使用的协议等.
以下是最应该关注的一些键:
* 1. REQUEST_METHOD: 表示HTTP请求的方法, 如GET, POST, PUT, DELETE等. 
     这个信息决定了应用程序应该如何处理请求.
* 2. PATH_INFO: 指示了请求的资源路径.
     这通常是URL中服务器名称和端口号之后的部分, 并且不包括查询字符串.
     应用程序将使用这个信息来决定应该执行哪个操作或返回哪个资源.
* 3. QUERY_STRING: 包含了URL中的查询字符串部分, 即?之后的部分. 
     这通常包含了一些键值对, 用于传递参数给应用程序.
* 4. REMOTE_ADDR: 表示发出请求的客户端的IP地址. 
     这对于日志记录, 安全审计或限制来自特定IP地址的请求等操作非常有用.
* 5. HTTP_HOST: 客户端请求的主机名和端口号.
     这有助于确定应用程序应该如何响应, 特别是在一个服务器上托管多个域名或应用程序时.
* 6. wsgi.input: 对于POST或PUT请求, 这个可读的输入流包含了请求的主体数据, 需要读取这个流来获取客户端发送的数据.
* 7. wsgi.errors: 这是一个可写的输出流, 通常用于记录错误信息或调试信息.
* 8. HTTP_USER_AGENT: 包含了发出请求的客户端的信息, 如浏览器类型, 版本等.
     这对于定制响应或进行用户代理特定的行为(如重定向到移动版网站)可能很有用.
* 9. CONTENT_TYPE和CONTENT_LENGTH: 对于解析请求主体非常有用.
     CONTENT_TYPE指示了请求主体的媒体类型(如application/json或text/plain).
     CONTENT_LENGTH 提供了请求主体的长度(以字节为单位).
* 10. HTTP_ACCEPT: 客户端告诉服务器它可以接受哪些类型的响应内容(如HTML, XML, JSON等).
    这有助于服务器根据客户端的偏好返回适当的响应.
    
了解这些键的信息可以帮助您编写出更加健壮和灵活的Web应用程序, 因为它们提供了关于HTTP请求的重要上下文信息.

4.4 WSGI应用程序1

使用wsgiref.simple_server模块来创建一个基本的Web服务器.
这个服务器会监听本地地址127.0.0.18080端口, 并将接收到的HTTP请求交给run函数处理.
run函数根据的请求路径的匹配结果, 构建HTTP响应并将响应发送回客户端.
from wsgiref.simple_server import make_server


def wsgi_app(environ, start_response):
    # 设置响应状态码和头部
    status_200 = '200 OK'
    status_404 = '404 File Not Found'
    headers = [('Content-Type', 'text/html')]

    # 获取路径, 键PATH_INFO的值
    path = environ.get('PATH_INFO')

    if path == '/index':
        start_response(status_200, headers)  # 设置响应头
        return [b'index']
    elif path == '/login':
        start_response(status_200, headers)
        return [b'login']
    else:
        start_response(status_404, headers)
        return [b'404']


if __name__ == '__main__':
    server = make_server('127.0.0.1', 8080, wsgi_app)
    server.serve_forever()

启动服务器, 使用浏览器分别访问:
127.0.0.1:8080
127.0.0.1:8080/index
127.0.0.1:8080/login

image-20240614032259418

4.5 urllib.parse模块

在Python中, urllib.parse模块提供了用于解析URLs的实用函数, 包括将查询字符串(query strings)解析为Python字典的parse_qs函数.

查询字符串是URL中跟在?后面的部分, 它包含了一系列由&分隔的键值对(key-value pairs),
这些键值对通常用于表示HTTP GET或POST请求中的参数.

例如, : https://example.com/search?q=python&page=1, 查询字符串是q=python&page=1.

parse_qs函数可以将这样的查询字符串解析为一个Python字典.
但是, 请注意, 由于一个键可能有多个值(如使用表单提交多个复选框时), 
所以parse_qs返回的字典的键对应的值是列表(list)而不是单个值.
#  简单示例:
from urllib.parse import parse_qs  
  
# 模拟查询字符串  
query_string = "q=python&page=1&color=red&color=blue"  
  
# 解析查询字符串  
params = parse_qs(query_string)  
  
# 输出结果  
print(params)  # 输出:{'q': ['python'], 'page': ['1'], 'color': ['red', 'blue']}  
  
# 访问单个参数的值(注意列表)  
print(params['q'][0])  # 输出:'python'  
  
# 遍历所有参数  
for key, values in params.items():  
    for value in values:  
        print(f"{key} = {value}")  
        # 输出:
        # q = python  
        # page = 1  
        # color = red  
        # color = blue
在上面的示例中, 可以看到parse_qs如何将查询字符串解析为一个字典, 并处理具有多个值的键.
当处理POST请求时, 特别是当请求体包含表单数据时, 需要从wsgi.input中读取数据, 并根据Content-Type头部来决定如何解析这些数据.
对于application/x-www-form-urlencoded类型的数据, 可以使用urllib.parse.parse_qs或urllib.parse.parse_qsl来解析.
POST中的数据结构也是键值对结构, 并使用&分隔, 如下图所示:

image-20240615133341438

from wsgiref.simple_server import make_server
from urllib.parse import parse_qs


# 获取表单数据
def get_from_data(environ):
    # 获取请求体大小
    content_length = int(environ.get('CONTENT_LENGTH', 0))

    # 读取请求体n个字符
    if content_length:
        request_body = environ['wsgi.input'].read(content_length)

        # 解析请求体
        form_data = parse_qs(request_body.decode('utf-8'))
        return form_data

    else:
        return None


def wsgi_app(environ, start_response):
    print('有人来了!!')
    # 设置响应状态码和头部
    status_200 = '200 OK'
    headers = [('Content-Type', 'text/html')]

    # 处理GET请求
    if environ.get('REQUEST_METHOD') == 'GET':
        params = parse_qs(environ.get('QUERY_STRING'))
        print(params)

    # 处理POST请求
    if environ.get('REQUEST_METHOD') == 'POST':
        form_data = get_form_data(environ)
        print(form_data)

    start_response(status_200, headers)  # 设置响应行和响应头
    # 返回响应体
    return [b'ok']


if __name__ == '__main__':
    server = make_server('127.0.0.1', 8080, wsgi_app)
    server.serve_forever()

GET请求访问, 数据在url中: http://127.0.0.1:8080/?name=123&pwd=123

image-20240615140153280

表单提交数据使用POST请求访问, 数据不会出现在url中, 设置form标签为<form action="http://127.0.0.1:8080" method="post">.
action属性为请求地址, method属性为请求方式.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册页面</title>
</head>
<body>
<form action="http://127.0.0.1:8080" method="post">
    <label for="username">用户名称:</label>
    <input type="text" id="username" name="username">
    <br>
    <label for="password">用户密码:</label>
    <input type="text" id="password" name="password">
    <br>
    <input type="submit" value="提交">
</form>
</body>
</html>

image-20240615141020330

4.6 WSGI应用程序2

使用wsgiref.simple_server模块来创建一个基本的Web服务器, 实现登录注册功能.
目录结构:
Project
  |--main.py  主程序
  |--test.db  数据库文件
  |--template.html  模板
  |--table.html  表单
  |--favicon.ico  头像
<!-- template.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>标题</title>
</head>
<body>
内容
</body>
</html>
<!-- table.html -->
<form action="" method="post">
    <label for="username">用户名称:</label>
    <input type="text" id="username" name="username">
    <br>
    <label for="password">用户密码:</label>
    <input type="text" id="password" name="password">
    <br>
    <input type="submit" value="按键">
</form>
# main.py
from wsgiref.simple_server import make_server
import sqlite3
from urllib.parse import parse_qs


# 获取表单数据
def get_form_data(environ):
    # 获取请求体大小
    content_length = int(environ.get('CONTENT_LENGTH', 0))

    # 读取请求体n个字符
    if content_length:
        request_body = environ['wsgi.input'].read(content_length)

        # 解析请求体
        form_data = parse_qs(request_body.decode('utf-8'))
        return form_data

    else:
        return None


# 读取页面 read_file
def read_file(file_name):
    with open(file_name, 'r', encoding='utf8') as f:
        file_data = f.read()

    return file_data


def template(title, content=None, button=None):
    # 读取模板文件
    template_html = read_file('template.html')
    # 没有设置内容则使用表单作为内容
    if not content:
        content = read_file('table.html')
        content = content.replace('按键', button)  # 替换按键名称
    # 替换html页面内容
    response_html = template_html.replace('标题', title)
    response_html = response_html.replace('内容', content)
    return response_html


# 提交数据
def get_submit_data(environ):
    # 获取表单数据, 例: {'username': ['kid'], 'password': ['123']}
    form_data = get_form_data(environ)
    if form_data:
        # 获取用户姓名和密码
        username = form_data.get('username')
        if username:
            username = username[0]
        password = form_data.get('password')
        if password:
            password = password[0]
        return username, password

    return None, None


# 数据库操作
def database_operations(sql, commit=False):
    conn = sqlite3.connect('test.db')  # 返回连接对象
    cur = conn.cursor()  # 创建一个游标对象(用于执行SQL语句并获取结果)
    # 执行sql语句
    res = cur.execute(*sql)  # 解包元组

    if commit:  # 数据写入需要二次提交
        conn.commit()  # 提交
    # 返回所有查询结果, 数据不存在会返回一个空列表
    fetchall = cur.fetchall()
    # 在结束数据库操作后, 最好关闭游标和连接
    cur.close()
    conn.close()
    # 返回所有查询结果
    return res, fetchall


# 注册
def register(environ):
    # 处理GET请求
    if environ.get('REQUEST_METHOD') == 'GET':
        # 获取提交的账户和密码
        response_html = template(title='注册页面', button='注册')
        return response_html.encode('utf8')

    # 处理POST请求:
    else:
        username, password = get_submit_data(environ)
        # 非空判断:
        if username and password:
            # 编写sql语句, 通过用户名称查询记录
            sql = "SELECT username FROM user WHERE username = ?", (username,)  # 数组
            _, fetchall = database_operations(sql)  # 开启二次提交

            # 判断用户是否存在
            if fetchall:
                response_html = template(title='注册页面', content='用户已经存在')
                return response_html.encode('utf8')

            # 创建用户
            else:
                # 编写sql语句
                sql = "INSERT INTO user VALUES (?, ?)", (username, password)  # 数组
                res, _ = database_operations(sql, commit=True)  # 开启二次提交
                if res:  # res为插入的总行数
                    response_html = template(title='注册页面', content='用户创建成功')
                    return response_html.encode('utf8')

        else:
            response_html = template(title='注册页面', content='账号或密码为空!')
            return response_html.encode('utf8')


# 登录
def login(environ):
    # 处理GET请求
    if environ.get('REQUEST_METHOD') == 'GET':
        response_html = template(title='登录页面', button='登录')
        return response_html.encode('utf8')

    else:
        # 获取提交的账户和密码
        username, password = get_submit_data(environ)

        # 非空判断:
        if username and password:
            # 编写sql通过用户名称查询记录
            sql = "SELECT username, password FROM user WHERE username = ?", (username,)  # 取出名称和密码
            _, fetchall = database_operations(sql, commit=True)
            # 判断用户是否存在,
            if fetchall:
                user_info = fetchall[0]  # 获取用户信息
                # print(user_info,  (username, password))  # ('kid', '123') ('kid', '123')
                if user_info == (username, password):
                    response_html = template(title='登录页面', content='登录成功')
                    return response_html.encode('utf8')

                else:
                    response_html = template(title='登录页面', content='密码错误')
                    return response_html.encode('utf8')

            else:

                response_html = template(title='登录页面', content='用户不存在, 请注册!')
                return response_html.encode('utf8')
        else:
            response_html = template(title='登录页面', content='账号或密码为空!')
            return response_html.encode('utf8')


def favicon(environ):
    with open('favicon.ico', 'rb') as f:
        favicon_date = f.read()

    return favicon_date


def error(environ):
    response_html = template(title='404页面', content='404 访问资源部存在')
    return response_html.encode('utf8')


# 定义一个字典存放所有函数
urls = {
    '/register': register,
    '/login': login,
    '/favicon.ico': favicon,
}


def wsgi_app(environ, start_response):
    # 设置响应状态码和头部
    status_200 = '200 OK'
    status_404 = '404 File Not Found'
    headers = [('Content-Type', 'text/html')]

    # 获取路径, 键PATH_INFO的值
    path = environ.get('PATH_INFO')

    # 获取处理函数
    func = urls.get(path, None)

    # 判断func是否为空, 只要不存在于url字典中,它的值就是None
    if func:
        res = func(environ)
        start_response(status_200, headers)
    else:
        res = error(environ)
        start_response(status_404, headers)

    # 返回响应体
    return [res]


if __name__ == '__main__':
    server = make_server('127.0.0.1', 8080, wsgi_app)
    print("""
    注册页面: http://127.0.0.1:8080/register
    登录页面: http://127.0.0.1:8080/login
    """)

    server.serve_forever()


这个Python程序定义了一个简单的WSGI应用程序, 它使用字典来映射URL路径到相应的处理函数.
以下是该程序的大致描述:
* 1. 定义URL映射字典: 使用一个名为urls的字典来映射URL路径到处理函数.
     例如, 当客户端请求/register路径时, register函数将被调用;
     当请求/login时, login函数将被调用, 依此类推.
* 2. 定义WSGI应用程序: wsgi_app函数是WSGI应用程序的核心.
     它接受两个参数: environ(一个包含CGI环境变量的字典)和start_response(一个可调用的对象, 用于设置HTTP响应的状态码和头部).
* 3. 设置响应状态码和头部: 定义了两个状态码: status_200(成功响应)和status_404(未找到文件).
     定义了一个HTTP头部列表headers, 其中包含一个'Content-Type'头部, 其值为'text/html'.
* 4. 获取请求路径: 从environ字典中获取'PATH_INFO'键的值, 该值表示客户端请求的URL路径.
* 5. 获取处理函数: 使用urls.get(path, None)从urls字典中根据请求路径获取相应的处理函数.
     如果路径不存在于字典中, 则返回None.
* 5. 处理请求: 如果找到了处理函数(即func不为None), 则调用该函数并传入environ作为参数.
     然后将响应状态码设置为status_200, 并调用start_response来设置HTTP响应的状态码和头部.
     如果没有找到处理函数(即func为None), 则调用一个名为error的函数, 然后将响应状态码设置为status_404, 并调用start_response.
* 6. 返回响应体: 将处理函数返回值作为响应体返回.
     注意, 由于WSGI规范要求响应体必须是一个可迭代对象, 因此即使是一个字符串, 也需要将其放入一个列表中返回.
GET请求: 当用户通过浏览器访问注册/登录页面时, 通常会发送一个GET请求.
在这个函数中, 如果接收到的是GET请求, 那么会调用template的函数生成的HTML页面返回给客户端.

POST请求: 当用户在注册/登录页面填写了用户名和密码, 并点击了登录按钮后, 浏览器会向服务器发送一个POST请求, 包含用户提交的数据.
首先, 通过调用get_submit_data(environ)函数从POST请求中提取用户提交的用户名和密码.
接着, 进行非空判断, 确保用户名和密码都已填写.
如果用户名和密码都不为空, 那么会连接到SQLite数据库, 并查询用户表中是否存在匹配的用户名和密码.
如果数据库中存在匹配的用户名和密码, 那么会返回一个登录成功的页面;
如果不匹配, 会返回一个账号密码错误的页面.
如果用户名或密码为空, 则会返回一个提示账号或密码为空的页面.
启动程序访问注册页面: http://127.0.0.1:8080/register , 填写数据提交.

image-20240615142023884

image-20240615142143474

再次访问注册页面: http://127.0.0.1:8080/register , 填写存在的数据(模拟用户名称存在的场景).

image-20240615142323165

程序访问登录页面: http://127.0.0.1:8080/register , 填写正确的数据并提交.

image-20240615142424228

再次程序访问登录页面: http://127.0.0.1:8080/register , 填写错误的数据并提交.
情况1: 用户不存在
情况2: 账号存在密码错误
情况3: 提交空数据

5. 网页生成与呈现策略

5.1 动态网页和静态网页

动态网页和静态网页是网页设计和开发中的两个基本概念, 它们的主要区别在于网页内容是如何生成和显示的.
静态网页: 静态网页是预先创建并存储在服务器上的HTML文件.
这些文件包含了固定的内容, 如文本, 图片, 视频等, 这些内容在发布到服务器后通常不会改变(除非手动更新文件).
当用户请求静态网页时, 服务器会直接发送存储的HTML文件给用户的浏览器进行渲染和显示.

特点如下:
* 1. 内容固定: 一旦发布, 内容就不会自动改变.
* 2. 交互有限: 用户与静态网页的交互通常是通过点击链接跳转到其他页面或表单提交来实现的.
* 3. 服务器负担轻: 因为服务器只是发送预先存储的文件, 所以处理请求的效率很高.
* 4. 易于优化: 因为内容固定, 所以搜索引擎优化(SEO)和缓存优化相对容易.
动态网页: 动态网页则是根据用户的请求或特定的条件动态生成的HTML内容.
这些网页通常包含了服务器端脚本(如PHP, ASP.NET, Node.js等)和数据库(如MySQL, MongoDB等),
用于处理用户输入, 存储和检索数据, 并根据这些数据动态生成HTML内容.

特点如下:
* 1. 内容可变: 根据用户请求或数据库中的数据动态生成内容.
* 2. 交互丰富: 通过服务器端脚本和数据库, 可以实现更复杂的用户交互, 如在线购物, 用户登录, 数据搜索等.
* 3. 服务器负担重: 因为服务器需要处理用户请求, 执行脚本, 访问数据库等操作, 所以处理请求的效率相对较低.
* 4. 安全性要求高: 因为涉及到用户输入和数据库操作, 所以需要采取更多的安全措施来防止SQL注入, 跨站脚本攻击(XSS)等安全问题.
总结: 静态网页和动态网页各有优缺点, 适用于不同的场景.
对于内容固定, 交互简单的网站, 静态网页可能更合适; 而对于需要动态生成内容, 实现复杂交互的网站, 动态网页则更具优势

5.2 页面静态化

页面静态化是一种将动态网页转换为静态HTML页面的技术.
将原本由服务器端动态生成的网页内容(如JSP, PHP等), 转化为静态的HTML文件, 并存储在服务器上, 供用户直接访问.

页面静态化的方法有多种, 其中常见的包括:
* 1. 直接生成: 在服务器端直接生成HTML文件, 并保存到服务器的文件系统中.
     当用户请求该页面时, 服务器直接发送静态的HTML文件给用户, 而不再执行动态脚本.
* 2. 通过HTTP请求获取: 使用编程语言(如PHP)中的相关函数(如file_get_contents或curl), 
     向服务器发送HTTP请求, 获取动态页面的HTML内容, 然后将这些内容保存到本地文件中.

页面静态化具有以下优点:
* 1. 提高速度: 由于静态页面不需要在服务器端执行动态脚本和查询数据库, 因此访问速度更快.
     对于访问量大的网站, 这可以显著提高用户体验.
* 2. 搜索引擎友好: 搜索引擎更喜欢静态网页, 因为它们更容易被抓取和索引. 这有助于提高网站的SEO排名.
* 3. 安全性: 静态网页不容易被黑客攻击, 因为它们不包含敏感数据和执行代码.
* 4. 稳定性: 如果后台程序或数据库出现问题, 静态网页仍然可以正常访问, 这保证了网站的稳定性.


页面静态化也存在一些缺点:
* 1. 交互性差: 由于静态页面不包含动态内容, 因此无法实现复杂的用户交互功能.
* 2. 开发复杂度: 增加了网站的开发复杂度, 因为需要同时维护动态和静态两种页面.
* 3. 占用硬盘空间: 静态页面会占用更多的服务器硬盘空间, 尤其是在页面数量庞大的情况下.
* 4. 不灵活: 静态化操作需要手动或自动触发, 不能实时反映数据的变化.

页面静态化适用于以下场景:
内容更新频率低: 如果网站的内容更新频率不高, 或者大部分内容都是固定的, 那么可以考虑使用页面静态化来提高访问速度.
搜索引擎优化: 对于需要提高SEO排名的网站, 页面静态化是一个有效的手段.
提高安全性: 对于安全性要求较高的网站, 如金融, 电商等, 可以使用页面静态化来减少潜在的安全风险.
总之, 页面静态化是一种在特定场景下提高网站性能和安全性的有效技术.
在实际应用中, 需要根据网站的具体需求来选择是否使用页面静态化技术.

5.3 页面伪静态

页面伪静态是一种技术, 它使得动态页面在外观上和行为上类似于静态页面, 但实际上仍然是动态生成的.
这种技术主要用于提高网站的搜索引擎优化(SEO)效果, 改善用户体验和安全性.

页面伪静态是指将动态页面(如ASP, PHP, JSP等脚本程序生成的页面)通过服务器端的配置和规则, 模拟出静态页面的效果.
虽然从URL结构和页面名称上看, 伪静态页面和静态页面相似, 但实际上它们仍然是动态生成的.

页面伪静态的实现方法有多种, 包括但不限于以下几种:
* 1. URL重写: 通过修改服务器(如Apache, Nginx等)的配置文件, 使用URL重写技术将动态的URL转换成静态的URL.
     例如, 将动态URL: https://www.example.com/index.php?id=1 转换成静态URL https://www.example.com/article/1.html.
* 2. Apache服务器: .htaccess文件中添加RewriteRule规则来实现伪静态.
* 3. Nginx服务器: 在Nginx的配置文件中定义location规则, 使用正则表达式匹配URL并进行重写.
* 4. 框架伪静态: 利用一个静态框架页面, 将所有的动态页面内容添加到框架页面内.
     这种方法可以从地址栏中看出, 如果网站全部的URL地址都一样, 那就说明可能采用了框架伪静态.
* 5. 文件组件或组件转换: 例如使用FSO文件组件或Intelligencia.UrlRewriter.dll组件,
     将动态页面(.aspx)的后缀显示为静态页面的后缀(.htm或.html).
* 6. URL参数解析: 在服务器端脚本(如PHP), 通过解析URL参数的方式实现伪静态页面.
     获取URL中的参数, 并根据参数值来动态加载相应的页面内容.

优点:
* 1. 提高SEO效果: 伪静态页面易于被搜索引擎收录, 因为它们具有类似于静态页面的URL结构.
* 2. 改善用户体验: 伪静态页面具有更简洁, 易记的URL, 提高了用户体验.
* 3. 安全性提升: 通过隐藏或加密URL地址, 使黑客难以找到真实的动态页面, 同时降低了动态文件所需的权限, 减少了木马注入的风险.
* 4. 维护方便: 伪静态页面具有自动更新和变化的能力, 减少了维护量.

缺点:
* 1. CPU使用率上升: 由于伪静态页面需要通过服务器端的规则进行转换, 因此会增加服务器的CPU使用率.
* 2. 可能增加响应时间: 在高流量的情况下, 伪静态页面的转换可能会导致服务器响应时间的增加.

总结: 页面伪静态是一种将动态页面模拟成静态页面的技术, 旨在提高网站的SEO效果, 改善用户体验和安全性.
通过URL重写, 框架伪静态, 文件组件或组件转换以及URL参数解析等方法可以实现页面伪静态.
虽然伪静态页面具有诸多优点, 但也需要注意其可能带来的服务器负担和响应时间增加的问题.

6. Jinja2模块

Jinja2是一个Python的模板引擎, 它用于将动态内容嵌入到静态文件中, 生成HTML, XML或其他任何基于文本的格式.
Jinja2提供了很多控制结构和过滤器, 帮助你轻松地创建复杂的模板.
(这里只简单讲解, 后续会重新讲解.)
如果还没有安装Jinja2, 可以使用pip来安装: pip install Jinja2 .
以下是Jinja2模板的一些基本语法:
* 1. 变量: 使用双大括号{{ variable_name }}来在模板中插入变量.
     Jinja2会将变量的值转换为字符串(除非在过滤器中指定了不同的行为).
<p>Hello, {{ name }}!</p>
* 2. 使用 if, elif  else 来创建条件语句, 结构如下:
{% if user %}  
  Hello, {{ user }}!  
{% elif guest %}  
  Hello, Guest!  
{% else %}  
  Hello, world!  
{% endif %}
* 3. 使用 for 循环来遍历列表, 元组, 字典或集合.
<ul>  
{% for item in items %}  
  <li>{{ item }}</li>  
{% endfor %}  
</ul>

{% for key, value in my_dict.items() %}  
  <dt>{{ key }}</dt>  
  <dd>{{ value }}</dd>  
{% endfor %}  
</dl>

6.1 Environment模块

以下是如何使用Jinja2模块的基本步骤:
* 1. 编写test模板, 在模板中可以使用Jinja2的特殊语法来插入变量和逻辑.
<!-- test.html -->
<!doctype html>
<html lang="en">
<head>
    <title>{{ title }}</title>
</head>
<body>
<h1>{{ heading }}</h1>
<ul>
    {% for item in items %}
    <li>{{ item }}</li>
    {% endfor %}
</ul>
</body>
</html>
在上面的模板中, {{ title }}, {{ heading }}, {{ paragraph }}{{ item }}是变量, 
{% for item in items %}{% endfor %}是控制结构.
* 2. 在Python中使用Jinja2, 在Python脚本中加载模板, 并使用数据来渲染模板.
from jinja2 import Environment  # 环境
from jinja2 import FileSystemLoader  # 文件系统加载程序

# 加载模板文件
env = Environment(loader=FileSystemLoader('./'))  # 假设模板文件在当前文件夹中
template = env.get_template('test.html')  # 获取模板

# 定义要传递给模板的数据
data = {
    'title': '网站名称',
    'heading': '标题: 欢迎来到我的网站',
    'items': ['段落 1', '段落 2', '段落 3'],
}

# 使用数据渲染模板
output = template.render(data)

# 输出渲染后的 HTML
print(output)

在这个例子中, 首先从jinja2模块导入Environment和FileSystemLoader.
然后, 创建一个Environment对象, 并指定模板文件的加载器.
接下来, 使用get_template()方法加载模板文件.
然后, 定义了一个包含要传递给模板的数据的字典.
最后, 使用render()方法将数据传递给模板, 并打印出渲染后的HTML.
<!-- 终端打印 -->
<!doctype html>
<html lang="en">
<head>
    <title>网站名称</title>
</head>
<body>
<h1>标题: 欢迎来到我的网站</h1>
<ul>
    
    <li>段落 1</li>
    
    <li>段落 2</li>
    
    <li>段落 3</li>
    
</ul>
</body>
</html>

6.2 Template模块

Template模块是Python开发中一个常用的模板系统.
Jinja2基于Python模板引擎, 主要用于生成配置文件或HTML页面等基于文本的格式.

使用方式: 
* 1. 创建一个Template对象, 通常是通过传递一个字符串(这个字符串就是模板的内容)给Template类的构造函数. 
* 2. 使用render()方法来渲染模板, 这个方法接受一个字典作为参数, 
     字典的键对应于模板中的变量名, 值则会被插入到模板中以替换对应的变量.
* 3. render()方法返回一个字符串, 这个字符串是模板渲染后的结果.
from jinja2 import Template  # 加载模板字符串或模板文件

data_dic = {
    'title': '网站名称',
    'heading': '标题: 欢迎来到我的网站',
    'username': 'kid',
    'age': 18,
    'hobby': 'read'
}
with open('test2.html', mode='rt', encoding='utf-8') as f:
    str_html = f.read()

    # 生成模板对象
    tmp = Template(str_html)
    res = tmp.render(data=data_dic)
    # 给test2.html传递一个值, 页面上通过data就能够拿到data_dic

print(res)

render()方法: 来填充模板中的变量.
可以将一个字典, 字典子类或关键字参数传递给该方法, 以在模板渲染时提供上下文数据.
在模板中, 变量可以按字典取值, 对象获取属性的方式使用.
<!-- test2.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>data.title</title>
</head>
<body>
<h1>data.heading</h1>
<p>{{data.get('username')}}</p>
<p>{{data.age}}</p>
<p>{{data['hobby']}}</p>
<p>{{data}}</p>
</body>
</html>
<!-- 终端打印 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>data.title</title>
</head>
<body>
<h1>data.heading</h1>
<p>kid</p>
<p>18</p>
<p>read</p>
<p>{'title': '网站名称', 'heading': '标题: 欢迎来到我的网站', 'username': 'kid', 'age': 18, 'hobby': 'read'}</p>
</body>
</html>
render()方法推荐的使用方式: res = tmp.render(**data_dic)
使用**data_dic是更简洁, 更常用的方法, 因为它会自动将字典的键-值对解包为函数的关键字参数.
from jinja2 import Template  # 加载模板字符串或模板文件

data_dic = {
    'title': '网站名称',
    'heading': '标题: 欢迎来到我的网站',
    'username': 'kid',
    'age': 18,
    'hobby': 'read'
}
with open('test2.html', mode='rt', encoding='utf-8') as f:
    str_html = f.read()

    tmp = Template(str_html)
    res = tmp.render(**data_dic)

print(res)

<!-- test2.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>title</title>
</head>
<body>
<h1>heading</h1>
<p>{{username}}</p>
<p>{{age}}</p>
<p>{{hobby}}</p>
</body>
</html>
<!-- 终端打印 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>title</title>
</head>
<body>
<h1>heading</h1>
<p>kid</p>
<p>18</p>
<p>read</p>
</body>
</html>

7. 简易web框架

将原本集中在一个或多个文件中的代码, 按照功能, 模块或组件进行分离, 形成多个独立的代码文件.
每个模块都包含完成其功能的必要代码, 并且与其他模块通过接口进行交互.

拆分后, 每个文件只包含特定的功能或模块, 使得代码结构更加清晰, 易于理解和修改.

7.1 目录规范

将Web应用程序的不同部分进行拆分和封装, 程序结构如下:
* 1. 控制层 (Control Layer): 使用control.py文件来编写控制逻辑.
     这个文件通常包含处理HTTP请求和响应的控制函数或类.
* 2. 路由层 (Routing Layer): 使用urls.py文件来定义URL模式与视图函数之间的映射关系.
     根据URL中的'路径'部分, 返回不同的信息或调用相应的视图函数.
* 3. 视图层 (View Layer): 使用views.py文件来编写业务逻辑.
     这个文件包含处理特定请求并返回响应数据(可能是HTML, JSON等)的视图函数.
* 4. 模板层 (Template Layer): 在templates文件夹中存储HTML前端页面.
     这些页面通常由视图函数渲染并返回给客户端.

当需要为Web应用程序添加新功能时, 只需按照以下步骤进行:
* 1. 在urls.py中定义新的路由: 为新功能定义一个新的URL模式, 并将其映射到相应的视图函数.
* 2. 在views.py中编写新的业务逻辑: 编写一个新的视图函数来处理与新功能相关的业务逻辑.
     这个函数应该根据请求执行必要的操作, 并返回一个适当的响应.
* 3. 在template下创建模板: 如果新功能需要渲染HTML页面, 可以在templates文件夹中创建一个新的模板文件.
     这个模板文件将包含HTML标记和模板变量, 用于在视图函数中动态插入数据.

通过这种方式, 可以保持Web应用程序的结构清晰和模块化, 便于后续的开发和维护.
对面条版(spaghetti code, 即混乱, 难以维护的代码)进行重构, 并引入适当的目录规范.
Project  项目的根目录
  |--bin  存放可执行脚本的目录
  |    |--create_table.py  数据库创建, 表格创建(第一次启动项目前执行, 创建user表)
  |    |--start.py  启动项目的脚本 
  |--conf  存放配置文件的目录
  |    |--settings.py  项目的主要配置文件,包含数据库连接信息
  |--core 存放项目核心逻辑的目录
  |    |--control.py  包含项目的路由控制逻辑或其他核心功能
  |--lib  存放项目所需的各种库或工具的目录
  |    |--db.py  数据库操作的封装
  |    |--file.py  文件操作的封装
  |    |-- path.py 处理文件路径操作的模块
  |    |--request_data.py  处理HTTP请求数据的逻辑
  |    |--template_renderer.py  模板渲染的逻辑
  |--static  存放静态文件的目录(通常还会包含CSS, JavaScript, 图片等其他静态资源)
  |    |--favicon.ico  网站的图标
  |--template  存放HTML模板的目录
  |    |--table.html  表单模板
  |    |--template.html 主模板
  |--url  存放URL路由配置的目录
  |    |--urls.py  定义URL到视图函数的映射
  |--view  存放视图函数的目录
  |    |--views.py  包含处理HTTP请求并返回响应的视图函数
  |--test.db  项目的数据库文件

image-20240616173756773

7.2 可执行脚本目录

# create_table.py
import sqlite3  
  
# 连接到SQLite数据库(如果不存在, 它将被创建)  
# 数据库文件是test.db  
# 如果数据库文件与Python脚本在同一目录下
conn = sqlite3.connect('test.db')
 
cur = conn.cursor()  # 连接到一个 SQLite 数据库并创建一个游标对象(用于执行SQL语句并获取结果)


# 创建了一个名为user的表, 其中包含了两个个字段: username, password.
# username和password字段被定义为varchar(12)类型, 这意味着它们可以存储最多12个字符的变长字符串.
cur.execute('''CREATE TABLE user  
             (username varchar(12), password varchar(12)''')  
  
# 关闭连接  
conn.close()
# start.py
import os
import sys
from wsgiref.simple_server import make_server
from core.control import wsgi_app

# pycharm中会自动替我们完成下面两步.
# 1.获取当前项目路径
BASE_PATH = os.path.dirname(os.path.dirname(__file__))

# 2.把 当前项目路径添加到搜索模块路径当中
sys.path.append(BASE_PATH)

# 启动程序
if __name__ == '__main__':
    server = make_server('127.0.0.1', 8080, wsgi_app)
    print("""
    注册页面: http://127.0.0.1:8080/register
    登录页面: http://127.0.0.1:8080/login
    """)

    server.serve_forever()

7.3 配置文件目录

# setting.py
import os

# 获取项目的路径
BATH_PATH = os.path.dirname(os.path.dirname(__file__))

# 设置静态文件夹路径
static_path = os.path.join(BATH_PATH, 'static')

# 设置模板文件的路径
templates_path = os.path.join(BATH_PATH, 'template')

# 设置数据库位置
db_path = os.path.join(BATH_PATH, 'test.db')

7.4 项目核心逻辑目录

# control.py
from url.urls import urls  # 导入路由字典
from view.views import error  # 导入视图函数


def wsgi_app(environ, start_response):
    # 设置响应状态码和头部
    status_200 = '200 OK'
    status_404 = '404 File Not Found'
    headers = [('Content-Type', 'text/html')]

    # 获取路径, 键PATH_INFO的值
    path = environ.get('PATH_INFO')

    # 获取处理函数
    func = urls.get(path, None)

    # 判断func是否为空, 只要不存在于url字典中, 它的值就是None
    if func:
        res = func(environ)
        start_response(status_200, headers)  # 设置状态行, 响应头
    else:
        res = error(environ)
        start_response(status_404, headers)

    # 返回响应体
    return [res]

7.5 项目库目录

# db.py
import sqlite3
from conf.settings import db_path


# 数据库操作
def database_operations(sql, commit=False):
    conn = sqlite3.connect(db_path)  # 返回连接对象
    cur = conn.cursor()  # 创建一个游标对象(用于执行SQL语句并获取结果)

    print()
    # 执行sql语句
    res = cur.execute(*sql)  # 解包元组

    if commit:  # 数据写入需要二次提交
        conn.commit()  # 提交
    # 返回所有查询结果, 数据不存在会返回一个空列表
    fetchall = cur.fetchall()
    # 在结束数据库操作后, 最好关闭游标和连接
    cur.close()
    conn.close()
    # 返回所有查询结果
    return res, fetchall

# file.py
from conf.settings import os, templates_path, static_path


# 拼接template下的html文件
def get_html_path(html_name):
    html_path = os.path.join(templates_path, html_name)
    return html_path


# 拼接static下的头像文件
def get_favicon_path():
    favicon_path = os.path.join(static_path, 'favicon.ico')
    return favicon_path

# path.py
from conf.settings import os, templates_path, static_path


# 拼接template下的html文件
def get_html_path(html_name):
    html_path = os.path.join(templates_path, html_name)
    return html_path


# 拼接static下的头像文件
def get_favicon_path():
    favicon_path = os.path.join(static_path, 'favicon.ico')
    return favicon_path

# request_data.py
from urllib.parse import parse_qs


# 获取表单数据
def get_form_data(environ):
    # 获取请求体大小
    content_length = int(environ.get('CONTENT_LENGTH', 0))

    # 读取请求体n个字符
    if content_length:
        request_body = environ['wsgi.input'].read(content_length)

        # 解析请求体
        form_data = parse_qs(request_body.decode('utf-8'))
        return form_data

    else:
        return None


# 提交数据
def get_submit_data(environ):
    # 获取表单数据, 例: {'username': ['kid'], 'password': ['123']}
    form_data = get_form_data(environ)
    if form_data:
        # 获取用户姓名和密码
        username = form_data.get('username')
        if username:
            username = username[0]
        password = form_data.get('password')
        if password:
            password = password[0]
        return username, password

    return None, None

# template_renderer.py
from lib.file import read_file


def template(title, content=None, button=None):
    # 读取模板文件
    template_html = read_file('template.html')
    # 没有设置内容则使用表单作为内容
    if not content:
        content = read_file('table.html')
        content = content.replace('按键', button)  # 替换按键名称
    # 替换html页面内容
    response_html = template_html.replace('标题', title)
    response_html = response_html.replace('内容', content)
    return response_html

7.6 静态文件目录

头像文件名字: favicon.ico

7.7 HTML模板目录

<!-- table.html -->
<form action="" method="post">
    <label for="username">用户名称:</label>
    <input type="text" id="username" name="username">
    <br>
    <label for="password">用户密码:</label>
    <input type="text" id="password" name="password">
    <br>
    <input type="submit" value="按键">
</form>
<!-- template.html -->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>标题</title>
</head>
<body>
内容
</body>
</html>

7.8 URL路由配置目录

# urls.py
# 导入视图函数
from view.views import register, login, favicon

# 定义一个字典存放所有路由和视图函数的对应关系
urls = {
    '/register': register,
    '/login': login,
    '/favicon.ico': favicon,
}

7.9 视图函数目录

# views.py
from lib.template_renderer import template
from lib.request_data import get_submit_data
from lib.db import database_operations
from lib.file import read_favicon


# 注册
def register(environ):
    # 处理GET请求
    if environ.get('REQUEST_METHOD') == 'GET':
        # 获取提交的账户和密码
        response_html = template(title='注册页面', button='注册')
        return response_html.encode('utf8')

    # 处理POST请求:
    else:
        username, password = get_submit_data(environ)
        # 非空判断:
        if username and password:
            # 编写sql语句, 通过用户名称查询记录
            sql = "SELECT username FROM user WHERE username = ?", (username,)  # 数组
            _, fetchall = database_operations(sql)  # 开启二次提交

            # 判断用户是否存在
            if fetchall:
                response_html = template(title='注册页面', content='用户已经存在')
                return response_html.encode('utf8')

            # 创建用户
            else:
                # 编写sql语句
                sql = "INSERT INTO user VALUES (?, ?)", (username, password)  # 数组
                res, _ = database_operations(sql, commit=True)  # 开启二次提交
                if res:  # res为插入的总行数
                    response_html = template(title='注册页面', content='用户创建成功')
                    return response_html.encode('utf8')

        else:
            response_html = template(title='注册页面', content='账号或密码为空!')
            return response_html.encode('utf8')


# 登录
def login(environ):
    # 处理GET请求
    if environ.get('REQUEST_METHOD') == 'GET':
        response_html = template(title='登录页面', button='登录')
        return response_html.encode('utf8')

    else:
        # 获取提交的账户和密码
        username, password = get_submit_data(environ)

        # 非空判断:
        if username and password:
            # 编写sql通过用户名称查询记录
            sql = "SELECT username, password FROM user WHERE username = ?", (username,)  # 取出名称和密码
            _, fetchall = database_operations(sql, commit=True)
            # 判断用户是否存在,
            if fetchall:
                user_info = fetchall[0]  # 获取用户信息
                # print(user_info,  (username, password))  # ('kid', '123') ('kid', '123')
                if user_info == (username, password):
                    response_html = template(title='登录页面', content='登录成功')
                    return response_html.encode('utf8')

                else:
                    response_html = template(title='登录页面', content='密码错误')
                    return response_html.encode('utf8')

            else:

                response_html = template(title='登录页面', content='用户不存在, 请注册!')
                return response_html.encode('utf8')
        else:
            response_html = template(title='登录页面', content='账号或密码为空!')
            return response_html.encode('utf8')


def favicon(environ):
    favicon_date = read_favicon()
    return favicon_date


def error(environ):
    response_html = template(title='404页面', content='404 访问资源部存在')
    return response_html.encode('utf8')

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/714352.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

几种经典排序算法

几种经典排序算法 插入排序折半插入排序法 选择排序冒泡排序希尔排序堆排序二路归并排序快速排序 在介绍排序之前&#xff0c;先来说说&#xff0c;研究不同的排序主要是要研究他们的哪些不同&#xff1a; 时间性能。即排序过程中元素之间的比较次数与元素移动次数。我们此次讨…

【最新鸿蒙应用开发】——鸿蒙中的“Slot插槽”?@BuilderParam

构建函数-BuilderParam 传递 UI 1. 引言 BuilderParam 该装饰器用于声明任意UI描述的一个元素&#xff0c;类似slot占位符。 简而言之&#xff1a;就是自定义组件允许外部传递 UI Entry Component struct Index {build() {Column({ space: 15 }) {SonCom() {// 直接传递进来…

IPv6 ND 协议功能概述

ND 协议功能概述 ND&#xff08;Neighbor Discovery&#xff0c;邻居发现&#xff09;协议是 IPv6 的一个关键协议&#xff0c;它综合了 IPv4 中的 ARP&#xff0c;ICMP 路由发现和 ICMP 重定向等协议&#xff0c;并对它们做了改进。 作为 IPv6 的基础性协议&#xff0c;ND 协…

ppt添加圆角矩形,并调整圆角弧度方法

一、背景 我们看的论文&#xff0c;许多好看的图都是用PPT做的&#xff0c;下面介绍用ppt添加圆角矩形&#xff0c;并调整圆角弧度方法。 二、ppt添加圆角矩形&#xff0c;并调整圆角弧度 添加矩形&#xff1a; 在顶部工具栏中&#xff0c;点击“插入”选项卡。 在“插图”…

冒泡排序知识点

排序的基本概念 排序是计算机内经常进行的一种操作&#xff0c;其目的是将一组“无序”的记录调整为“有序”的记录序列。 常用的排序例子 8 7 1 5 4 2 6 3 9 把上面的这个无序序列变为有序&#xff08;升序或者降序&#xff09;序列的过程。 1 2 3 4 5 6 7 8 9&#xff0…

Spring运维之boo项目表现层测试加载测试的专用配置属性以及在JUnit中启动web服务器发送虚拟请求

测试表现层的代码如何测试 加载测试的专用属性 首先写一个测试 假定我们进行测试的时候要加一些属性 要去修改一些属性 我们可以写一个只在本测试有效的测试 写在配置里 测试 打印输出 我们把配置文件里面的配置注释掉后 我们同样可以启动 package com.example.demo;impo…

代码随想录——组合总和Ⅱ(Leetcode 40)需要回顾

题目链接 回溯 本题的难点在于&#xff1a;集合&#xff08;数组candidates&#xff09;有重复元素&#xff0c;但还不能有重复的组合。 思想&#xff1a;元素在同一个组合内是可以重复的&#xff0c;怎么重复都没事&#xff0c;但两个组合不能相同。所以要去重的是同一树…

购物车店铺列表查询流程

购物车店铺列表查询流程 购物车结算流程图

嵌入式门槛高不高,工资怎么样?

一般来说&#xff0c;嵌入式岗位的准入门槛其实并不是特别高。通常情况下&#xff0c;只要能够熟练掌握 C 语言编程以及单片机相关知识&#xff0c;就能够去制作一些较为简单的电子产品&#xff0c;由此可见其门槛相对而言是比较低的&#xff0c;相应的薪水可能也不会特别高。 …

I2C 总线通信技术基础

1.0 I2C 技术基础 使用总线的目的&#xff1a;采用串行总线技术可以使系统的硬件设计大大简化、系统的体积减小、可靠性提高&#xff0c;同时&#xff0c;系统的更改和扩充变的极为容易。 通信中常用的串行拓展总线 I2C&#xff08;Inter-Integrated Circuit &#xff09;总线…

C语言程序设计-6 循环控制

C语言程序设计-6 循环控制 循环结构是程序中一种很重要的结构。其特点是&#xff0c;在给定条件成立时&#xff0c;反复执行某程序 段&#xff0c;直到条件不成立为止。给定的条件称为循环条件&#xff0c;反复执行的程序段称为循环体。&#xff23;语 言提供了多种循环语句&a…

计算机网络知识点全面总结回顾

物理层 OSI模型&#xff1a;数据链路层&#xff08;流量控制&#xff09;&#xff0c;从传输层开始端到端&#xff1b;每一层的元素都称为实体&#xff0c;同一层的是对等实体&#xff1b;三个重要概念&#xff1a;服务&#xff08;下层为上层提供调用&#xff09;&#xff0c…

【Linux】进程间通信1——管道概念,匿名管道

1.进程间通信介绍 进程是计算机系统分配资源的最小单位&#xff08;严格说来是线程&#xff09;。每个进程都有自己的一部分独立的系统资源&#xff0c;彼此是隔离的。为了能使不同的进程互相访问资源并进行协调工作&#xff0c;才有了进程间通信。 进程间通信&#xff0c;顾名…

1055 集体照(测试点3, 4, 5)

solution 从后排开始输出&#xff0c;可以先把所有的学生进行排序&#xff08;身高降序&#xff0c;名字升序&#xff09;&#xff0c;再按照每排的人数找到中间位置依次左右各一个进行排列测试点3&#xff0c; 4&#xff0c; 5&#xff1a;k是小于10的正整数&#xff0c;则每…

记录一次root过程

设备: Redmi k40s 第一步&#xff0c; 解锁BL&#xff08;会重置手机系统&#xff01;&#xff01;&#xff01;所有数据都会没有&#xff01;&#xff01;&#xff01;&#xff09; 由于更新了澎湃OS系统, 解锁BL很麻烦, 需要社区5级以上还要答题。 但是&#xff0c;这个手机…

人工智能历史与现状

1 人工智能历史与现状 1.1 人工智能的概念和起源 1.1.1 人工智能的概念 人工智能 (Artificial Intelligence ,AI)是一门研究如何使计算机 能够模拟人类智能行为的科学和技术,目标在于开发能够感知、理解、 学习、推理、决策和解决问题的智能机器。人工智能的概念主要包含 以…

理解DDD设计

DDD的理解 领域驱动设计&#xff08;Domain-Driven Design&#xff0c;DDD&#xff09;是一种软件开发方法论&#xff0c;强调将业务领域作为软件设计的核心&#xff0c;以便更好地满足业务需求。DDD认为&#xff0c;软件开发的核心是理解业务&#xff0c;而不是实现技术。在D…

容器镜像外网同步方案

目录 一、目的 二、安装nexus 1、购买香港云主机​编辑 2、安装nexus 3、启动nexus 服务 4、放行安全组 三、配置nexus 1、登录nexus管理页面 2、修改nexus密码 3、创建 Blob 存储空间(可选) 4、创建 镜像代理仓库 5、Realms配置 四、拉取镜像 1、配置docker 2、…

【Python】Python实现解压rar文件

Python实现解压rar文件 零、需求 最近在开发一个填分数的应用&#xff0c;需要用到selenium&#xff0c;那么自然需要用到浏览器&#xff0c;浏览器内置到应用中&#xff0c;但是上传到GitCode的时候被限制了&#xff0c;单个文件大小只能是10M以内。所以只能压缩&#xff0c…

免费个人站 独立站 wordpress 自建网站

制作免费网站 | 免费网站构建器 | WordPress.com https://bioinformatics7.wordpress.com WordPress.com