python3-02-Net_Concurrency

python3-02-Net_Concurrency

27、TCPUDP_Socket_计算器

全文总结

核心模块 核心知识点 关键流程/特点
TCP/UDP协议 传输层通信协议;Socket(套接字)是网络通信的“数据拆包拼接工具” TCP:面向连接、可靠(三次握手/四次挥手)、速度慢;
UDP:无连接、不可靠、速度快
Socket基础通信(TCP) 客户端与服务端的“一发一收”单向通信 服务端:创建Socket→绑定IP+端口→监听→建立连接→收发数据→关闭;
客户端:创建Socket→连接服务端→收发数据→关闭
Socket循环通信(TCP) 客户端与服务端的“多次收发”双向通信 基于基础Socket,增加while循环实现持续收发,通过“发送q”触发退出
带括号的表达式计算器 解析含括号、加减乘除的数学表达式 1. 匹配最内层括号;2. 计算括号内表达式(先乘除后加减);3. 替换括号为计算结果;4. 循环至无括号;5. 计算最终加减

一、TCP/UDP协议 & Socket网络通信(一步步理解)

1.1 TCP/UDP协议核心

  • TCP:像“打电话”——必须先接通(三次握手),说话能确认对方听到(可靠),挂电话要确认(四次挥手),但速度慢。适合传文件、邮件、网页数据(要保证完整)。
  • UDP:像“发短信”——不用接通,直接发(无连接),对方可能收不到(不可靠),但速度快。适合语音通话、视频会议、聊天消息(允许少量丢包)。
  • Socket(套接字):网络通信的“工具”——负责把数据拆成小包发出去,或把收到的小包拼起来,是TCP/UDP通信的“桥梁”。

1.2 Socket基础通信(一发一收)

步骤1:服务端代码(1-socket基本语法_server.py)拆解
import socket
# 1. 创建Socket对象(默认是TCP类型)
sk = socket.socket()
# 2. 绑定IP和端口(让客户端能找到自己)
# 127.0.0.1=本机IP,9000=端口号(一个程序占一个端口)
sk.bind(("127.0.0.1", 9000))
# 3. 开启监听(等待客户端连接)
sk.listen()
# 4. 建立连接(三次握手,阻塞状态——直到有客户端连过来)
conn, addr = sk.accept()  # conn=连接对象,addr=客户端IP+端口
print("客户端地址:", addr)
# 5. 接收客户端数据(最多收1024字节)
msg = conn.recv(1024)
print("收到客户端消息:", msg.decode("utf-8"))  # 二进制转字符串
# 6. 给客户端发消息(字符串转二进制)
conn.send("我爱你嗯~".encode("utf-8"))
# 7. 关闭连接(四次挥手)
conn.close()  # 断开和客户端的连接
sk.close()    # 释放端口
步骤2:客户端代码(1-socket基本语法_client.py)拆解
import socket
# 1. 创建Socket对象
sk = socket.socket()
# 2. 连接服务端(指定服务端IP+端口)
sk.connect(("127.0.0.1", 9000))
# 3. 给服务端发消息(必须转二进制)
sk.send("你爱我么~".encode("utf-8"))
# 4. 接收服务端消息(阻塞——直到收到消息)
res = sk.recv(1024)
print("收到服务端消息:", res.decode("utf-8"))
# 5. 关闭连接
sk.close()
执行过程
  1. 先运行服务端代码(此时服务端卡在sk.accept(),等待客户端连接);
  2. 再运行客户端代码,客户端触发sk.connect(),服务端完成三次握手,代码继续执行;
  3. 客户端发送“你爱我么~”→ 服务端接收并打印;
  4. 服务端发送“我爱你嗯~”→ 客户端接收并打印;
  5. 双方关闭连接,程序结束。

1.3 Socket循环通信(多次收发)

步骤1:循环服务端(2-socket循环发送消息_server.py)
import socket
sk = socket.socket()
sk.bind(("127.0.0.1", 9001))
sk.listen()
while True:
    # 持续等待新客户端连接
    conn, addr = sk.accept()
    while True:
        # 接收客户端消息
        res = conn.recv(1024)
        print("客户端:", res.decode("utf-8"))
        # 服务端回复消息
        strvar = input("请输入回复:")
        conn.send(strvar.encode("utf-8"))
        if strvar == "q":  # 发送q则退出循环
            break
    conn.close()
sk.close()
步骤2:循环客户端(2-socket循环发送消息_client.py)
import socket
sk = socket.socket()
sk.connect(("127.0.0.1", 9001))
while True:
    # 客户端输入并发送消息
    strvar = input("请输入发送内容:")
    sk.send(strvar.encode("utf-8"))
    # 接收服务端回复
    res = sk.recv(1024)
    if res == b"q":  # 收到b"q"则退出
        break
    print("服务端:", res.decode("utf-8"))
sk.close()
执行过程
  1. 服务端运行后,卡在sk.accept()等待连接;
  2. 客户端连接后,进入循环:
    • 客户端输入消息→发送→等待服务端回复;
    • 服务端接收消息→输入回复→发送;
  3. 当服务端输入“q”并发送,客户端收到b"q",触发break,关闭连接;
  4. 服务端也触发break,回到外层循环,等待下一个客户端连接。

二、带括号的数学表达式计算器(一步步拆运算)

2.1 核心思路

计算-30+(40+5*-2)-8这类表达式,核心是“先拆括号、再算乘除、最后算加减”,步骤:

  1. 找到最内层的括号(比如(40+5*-2));
  2. 计算括号内的表达式(先算5*-2=-10,再算40-10=30);
  3. 把括号替换成计算结果(原式变成-30+30-8);
  4. 计算最终的加减(-30+30-8=-8)。

2.2 分步拆解(以-30+(40+5*-2)-8为例)

步骤1:匹配最内层括号
import re
strvar = "-30+(40+5*-2)-8"
# 正则匹配:找没有嵌套的括号(最内层)
obj = re.search(r"\([^()]+\)", strvar)
res = obj.group()  # 结果:(40+5*-2)
print("匹配到的最内层括号:", res)
步骤2:计算括号内的乘除(先算5*-2
# 正则匹配:找乘除表达式(比如5*-2)
obj = re.search(r"\d+(\.\d+)?[*/][+-]?\d+(\.\d+)?", res)
res2 = obj.group()  # 结果:5*-2

# 定义乘除计算函数
def calc_exp(strvar):
    if "*" in strvar:
        a, b = strvar.split("*")
        return str(float(a) * float(b))  # 5*-2=-10.0
    if "/" in strvar:
        a, b = strvar.split("/")
        return str(float(a) / float(b))

res3 = calc_exp(res2)  # 结果:-10.0
print("乘除计算结果:", res3)
步骤3:替换括号内的乘除结果,整理符号
# 把括号内的5*-2替换成-10.0 → (40+-10.0)
res4 = res.replace(res2, res3)

# 整理符号(比如把+-换成-)
def parse_exp(strvar):
    strvar = strvar.replace("+-", "-")
    strvar = strvar.replace("--", "+")
    return strvar

res5 = parse_exp(res4)  # 结果:(40-10.0)
print("整理符号后:", res5)
步骤4:计算括号内的加减
# 提取所有数字(带正负)→ ['40', '-10.0']
res6 = re.findall("[+-]?\d+(?:\.\d+)?", res5)
total = 0
for i in res6:
    total += float(i)  # 40 -10.0 = 30.0
print("括号内最终结果:", total)
步骤5:替换括号,计算最终结果
# 把原式的(40+5*-2)替换成30.0 → -30+30.0-8
strvar = strvar.replace(res, str(total))
# 计算最终加减
final_list = re.findall("[+-]?\d+(?:\.\d+)?", strvar)
final_total = 0
for i in final_list:
    final_total += float(i)  # -30 +30 -8 = -8
print("表达式最终结果:", final_total)

2.3 完整计算器代码(支持复杂表达式)

import re

# 计算乘除
def calc_exp(strvar):
    if "*" in strvar:
        a, b = strvar.split("*")
        return str(float(a) * float(b))
    if "/" in strvar:
        a, b = strvar.split("/")
        return str(float(a) / float(b))

# 整理符号
def parse_exp(strvar):
    strvar = strvar.replace("+-", "-")
    strvar = strvar.replace("--", "+")
    strvar = strvar.replace("-+", "-")
    strvar = strvar.replace("++", "+")
    return strvar

# 计算加减乘除(无括号)
def calc(strvar):
    # 先算乘除
    while True:
        obj = re.search(r"\d+(\.\d+)?[*/][+-]?\d+(\.\d+)?", strvar)
        if obj:
            res = obj.group()
            res2 = calc_exp(res)
            strvar = strvar.replace(res, res2)
        else:
            break
    # 再算加减
    strvar = parse_exp(strvar)
    lst = re.findall("[+-]?\d+(?:\.\d+)?", strvar)
    total = 0
    for i in lst:
        total += float(i)
    return total

# 移除所有括号
def remove_bracket(strvar):
    while True:
        obj = re.search(r"\([^()]+\)", strvar)
        if obj:
            res = obj.group()
            res2 = calc(res)
            strvar = strvar.replace(res, str(res2))
        else:
            return strvar

# 主函数
def main(strvar):
    res = remove_bracket(strvar)
    return calc(res)

# 测试复杂表达式
strvar = '1-2*((60-30+(-40/5)*(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))'
final_res = main(strvar)
print("复杂表达式结果:", final_res)
复杂表达式运算逻辑(核心)
  1. 循环匹配最内层括号→计算括号内(先乘除后加减)→替换括号;
  2. 重复步骤1,直到表达式无括号;
  3. 计算最终的加减乘除,得到结果。

三、应用场景 & 案例 & 详细过程

3.1 TCP/Socket的应用场景

场景1:简易聊天工具(对应循环Socket代码)
  • 案例:实现双人实时聊天,A(客户端)和B(服务端)互相发消息,输入“q”退出。
  • 详细过程
    1. 服务端启动:绑定9001端口,监听客户端连接;
    2. 客户端启动:连接服务端的127.0.0.1:9001;
    3. 客户端输入“吃饭了吗?”→发送→服务端接收并打印;
    4. 服务端输入“还没,你呢?”→发送→客户端接收并打印;
    5. 任意一方输入“q”→发送后,对方收到b"q",触发退出,连接关闭。
场景2:文件传输(TCP扩展)
  • 案例:客户端把本地txt文件发给服务端。
  • 核心逻辑(补充)
    1. 服务端:循环接收客户端发送的二进制数据,写入本地文件;
    2. 客户端:打开本地文件,按1024字节分段读取,逐段发送给服务端;
    3. 依赖TCP的“可靠性”,保证文件传输完整。

3.2 UDP的应用场景

场景:多人聊天室(UDP特点适配)
  • 案例:3个客户端同时给服务端发消息,服务端广播给所有客户端。
  • 核心逻辑(补充)
    1. 服务端:创建UDP Socket,绑定端口,循环接收所有客户端的消息和地址;
    2. 客户端:创建UDP Socket,无需连接,直接向服务端端口发消息;
    3. 服务端收到消息后,遍历所有客户端地址,把消息转发出去;
    4. 依赖UDP的“无连接、速度快”,适配多人实时聊天(允许偶尔丢包)。

3.3 表达式计算器的应用场景

场景1:简易计算器软件
  • 案例:用户输入(100-20)*5+80/2,程序输出结果。
  • 详细运算过程
    1. 匹配最内层括号(100-20)→计算得80→原式变为80*5+80/2
    2. 算乘除:80*5=40080/2=40→原式变为400+40
    3. 算加减:400+40=440→输出结果440。
场景2:财务/数据分析工具
  • 案例:批量计算财报中的复杂公式(如营收=(单价*销量-成本)*税率+补贴)。
  • 核心逻辑
    1. 把公式中的变量(单价、销量等)替换为实际数值;
    2. 调用计算器函数,自动解析括号和运算优先级,输出结果;
    3. 替代人工计算,避免出错,提升效率。


28、Socket编程_黏包问题_socketserver_反射机制

全文总结

知识点模块 核心内容 关键特点/解决方法
TCP/UDP协议 TCP:面向连接、可靠、传输慢;UDP:无连接、不可靠、传输快 TCP需三次握手/四次挥手,数据无边界易黏包;UDP数据有边界不黏包,适合实时场景
Socket编程 网络通信的基础“套接字”,分为客户端/服务端架构 UDP用sendto/recvfrom收发;TCP用connect/accept/send/recv,需手动处理连接
黏包问题 TCP因缓冲区+无数据边界,导致多段短数据黏合 解决思路:先传数据长度(固定长度字符串/struct模块打包为4字节),再传实际数据
socketserver 基于Socket封装,实现TCP服务端多线程并发 自定义类继承BaseRequestHandler,重写handle方法,通过ThreadingTCPServer实现并发
反射机制 通过字符串动态操作类/模块的属性、方法 核心函数:hasattr(检测)、getattr(获取)、setattr(设置)、delattr(删除)

补充分段总结

1. 协议层(TCP/UDP)
  • TCP:像“打电话”,必须建立连接,数据传输可靠但慢,适合文件、邮件等大/稳定数据传输场景;
  • UDP:像“发短信”,无需建立连接,数据传输快但可能丢包,适合语音、视频、聊天等小/实时数据传输场景。
2. 基础编程(Socket)
  • Socket是网络通信的核心工具,UDP编程更简单(无需维护连接),TCP需处理连接的建立/断开;
  • 核心区别:UDP收发数据需指定对方地址,TCP通过连接对象收发数据。
3. 黏包解决
  • 仅TCP存在黏包,UDP因数据有边界无此问题;
  • 核心方案:“先传长度,再传数据”,struct模块可将长度打包为固定4字节,是最优解。
4. 并发与反射
  • socketserver解决TCP服务端“单客户端”限制,多线程可同时处理多个客户端;
  • 反射实现“字符串驱动编程”,无需硬编码调用逻辑,提升代码扩展性。

完整详细内容

1. TCP/UDP协议基础

1.1 核心概念
  • TCP:面向连接的可靠协议,传输前必须“握手”建立连接,传输后“挥手”断开,数据丢了会重传,但速度慢;
  • UDP:无连接的不可靠协议,直接发数据,不管对方是否收到,速度快,支持多客户端同时通信。
1.2 UDP Socket编程(最简入门)
步骤1:UDP服务端(循环收发)
# 1. 导入模块
import socket
# 2. 创建UDP Socket对象(指定SOCK_DGRAM)
sk = socket.socket(type=socket.SOCK_DGRAM)
# 3. 绑定IP和端口(服务端必须绑定)
sk.bind(("127.0.0.1", 9000))
# 4. 循环收发数据
while True:
    # 接收:返回(数据字节,客户端地址),1024=最大接收字节
    msg, cli_addr = sk.recvfrom(1024)
    # 解码打印
    print(f"客户端[{cli_addr}]:{msg.decode('utf-8')}")
    # 输入回复
    reply = input("服务端回复:")
    # 发送:编码+指定客户端地址
    sk.sendto(reply.encode("utf-8"), cli_addr)
# 5. 关闭(循环不会执行到,手动停止)
sk.close()
步骤2:UDP客户端(循环收发)
# 1. 导入模块
import socket
# 2. 创建UDP Socket对象
sk = socket.socket(type=socket.SOCK_DGRAM)
# 3. 循环收发数据
while True:
    # 输入消息
    msg = input("客户端发送:")
    # 发送到服务端(指定IP+端口)
    sk.sendto(msg.encode("utf-8"), ("127.0.0.1", 9000))
    # 接收回复
    reply, addr = sk.recvfrom(1024)
    print(f"服务端:{reply.decode('utf-8')}")
# 4. 关闭
sk.close()
执行过程
  1. 先运行服务端:绑定9000端口,进入循环等待接收;
  2. 再运行客户端:输入“你好”→发送到服务端;
  3. 服务端收到消息,打印客户端[(127.0.0.1, 54321)]:你好→输入“你好呀”→回复客户端;
  4. 客户端收到回复,打印服务端:你好呀
  5. 循环往复,实现“你发我收”的UDP聊天。
1.3 TCP Socket编程(基础版)
步骤1:TCP服务端(单客户端)
# 1. 导入模块
import socket
# 2. 创建TCP Socket(默认SOCK_STREAM)
sk = socket.socket()
# 3. 允许端口复用(避免测试时端口占用报错)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 4. 绑定IP+端口
sk.bind(("127.0.0.1", 9000))
# 5. 监听端口(参数=最大等待连接数)
sk.listen()
# 6. 接受连接(三次握手,返回连接对象+客户端地址)
conn, addr = sk.accept()
print(f"客户端[{addr}]已连接")
# 7. 收发数据
msg = conn.recv(1024).decode("utf-8")
print(f"客户端:{msg}")
conn.send("TCP服务端回复".encode("utf-8"))
# 8. 关闭连接(四次挥手)
conn.close()
sk.close()
步骤2:TCP客户端
# 1. 导入模块
import socket
# 2. 创建TCP Socket
sk = socket.socket()
# 3. 连接服务端
sk.connect(("127.0.0.1", 9000))
# 4. 发送消息
sk.send("TCP客户端消息".encode("utf-8"))
# 5. 接收回复
reply = sk.recv(1024).decode("utf-8")
print(f"服务端:{reply}")
# 6. 关闭
sk.close()
执行过程
  1. 运行服务端:执行到sk.accept()阻塞,等待客户端连接;
  2. 运行客户端:sk.connect()触发三次握手,服务端退出阻塞;
  3. 客户端发送消息→服务端接收并打印→服务端回复→客户端接收并打印;
  4. 双方关闭连接,触发四次挥手。

2. 黏包问题(TCP核心痛点)

2.1 黏包成因

TCP的缓冲区像“快递箱”,连续发2个小包裹(短数据)且间隔短,快递员会把2个包裹塞一个箱子里(黏包);接收端不知道拆包,就会把2个包裹当1个。

2.2 黏包演示
服务端(连续发2条短数据)
# 1. 导入Python内置的socket模块,用于网络通信
import socket

# 2. 创建TCP套接字对象
# socket() 默认参数:family=AF_INET(IPv4协议),type=SOCK_STREAM(TCP协议)
# sk 就是服务端的套接字,负责监听和建立连接
sk = socket.socket()

# 3. 设置端口复用(关键:解决测试时端口被占用报错)
# socket.SOL_SOCKET:代表当前套接字层
# socket.SO_REUSEADDR:允许复用本地IP和端口
# 1:开启该功能,关闭程序后立即释放端口,无需等待系统回收
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 4. 绑定IP地址和端口号
# 元组格式:(IP, 端口)
# 127.0.0.1 = 本机回环地址(仅本机自己访问)
# 9000 = 服务端监听的端口(自定义,范围1024~65535)
sk.bind(("127.0.0.1", 9000))

# 5. 开启监听,等待客户端连接
# 作用:将套接字变为被动监听模式,准备接收客户端的连接请求
sk.listen()

# 6. 接受客户端的连接请求(阻塞式)
# 程序会停在这里,直到有客户端连接上来
# conn:新创建的**连接专用套接字**,专门和当前客户端收发数据
# addr:客户端的地址信息 (客户端IP, 客户端端口)
conn, addr = sk.accept()

# ------------------- 黏包核心代码 -------------------
# TCP协议特性:流式传输,数据无边界,连续发送短数据会被操作系统缓冲区合并
# 7. 第一次发送数据:字符串转二进制字节流,发送给客户端
conn.send("hello,".encode("utf-8"))
# 8. 第二次发送数据:连续发送短数据,两条数据会被黏合在一起
conn.send("world".encode("utf-8"))
# ----------------------------------------------------

# 9. 关闭与当前客户端的连接(四次挥手,断开通信)
conn.close()

# 10. 关闭服务端套接字,释放端口资源
sk.close()
客户端(接收黏包数据)
# 1. 导入Python内置socket模块,用于实现TCP网络通信
import socket

# 2. 创建TCP客户端套接字对象
# 默认参数:IPv4协议(AF_INET) + TCP流式协议(SOCK_STREAM)
# sk 是客户端专属套接字,负责和服务端建立连接、收发数据
sk = socket.socket()

# 3. 客户端主动连接服务端
# 格式:connect( (服务端IP, 服务端端口) )
# 127.0.0.1:本机回环地址,仅本机测试使用
# 9000:服务端绑定的监听端口,必须和服务端一致
sk.connect(("127.0.0.1", 9000))

# ------------------- 黏包核心接收 -------------------
# 4. 接收服务端发送的数据
# recv(1024):一次性最多接收1024字节的二进制数据
# 服务端连续发送了"hello,"和"world"两条短数据,TCP流式传输会将两条数据黏合
# 客户端一次recv就会收到合并后的完整数据:b"hello,world"
# decode("utf-8"):将二进制字节流解码为字符串
res = sk.recv(1024).decode("utf-8")

# 5. 打印接收结果,验证黏包现象
# 输出:hello,world(两条数据黏合成一条,这就是TCP黏包)
print(res)
# ----------------------------------------------------

# 6. 关闭客户端套接字,释放网络资源,断开与服务端的连接
sk.close()
2.3 解决方法:struct模块(最优解)
步骤1:struct基础(打包/解包长度)

解决TCP黏包的核心工具,专门用来把数字打包成固定4字节,让客户端精准知道要接收多少数据,彻底解决黏包问题

# 1. 导入struct模块:Python内置模块,用于打包/解包固定长度的二进制数据
# 核心作用:把任意整数 → 固定4字节字节流,解决TCP黏包的关键
import struct

# 2. struct.pack(格式符, 要打包的数字) → 打包数据
# "i" = 格式符,代表 int 整型,打包后固定生成 4个字节 的二进制数据
# 120 = 我们要打包的数字(实际场景中是:数据的长度)
packed_len = struct.pack("i", 120)

# 3. 打印打包后的结果:二进制字节流(固定4字节)
# 输出示例:b'x\x00\x00\x00',肉眼看不懂,但网络传输专用
print(packed_len)

# 4. 验证长度:无论数字多大,"i"格式打包后永远是 4 字节
# 这是解决黏包的关键!客户端固定只收4字节,就能拿到数据长度
print(len(packed_len))

# 5. struct.unpack(格式符, 打包的二进制数据) → 解包数据
# 作用:把4字节的二进制 → 还原成原来的整数
# 注意:unpack 返回值是 元组(120,),所以必须加 [0] 取出第一个元素
unpacked_len = struct.unpack("i", packed_len)[0]

# 6. 打印解包后的结果:成功还原为数字 120
print(unpacked_len)

🔥 结合黏包场景总结

  1. 为什么用struct?
    TCP黏包的根源是数据无边界,我们需要固定长度的包头来标记数据大小;
    pack("i", 长度) 能把任意数字变成固定4字节,客户端只需要先收4字节,就能精准知道后续要收多少数据。

  2. 格式符i的意义
    代表4字节整型,是网络编程解决黏包最常用的格式符,通用且稳定。


步骤2:服务端(先传长度,再传数据)

解决黏包的标准方案先发送固定4字节的数据长度包头 → 再发送真实数据,客户端根据长度精准接收,彻底避免黏包

# 1. 导入socket模块(网络通信) + struct模块(打包数据长度,解决黏包)
import socket
import struct

# 2. 创建TCP套接字(默认IPv4 + TCP流式协议)
sk = socket.socket()

# 3. 端口复用:解决测试时端口被占用报错,关闭程序后立即释放端口
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 4. 绑定本机IP和端口,让客户端可以找到服务端
sk.bind(("127.0.0.1", 9000))

# 5. 开启监听,等待客户端连接
sk.listen()

# 6. 阻塞等待客户端连接,建立连接后返回:
# conn:专属连接对象(负责和当前客户端收发数据)
# addr:客户端的IP+端口
conn, addr = sk.accept()

# ------------------- 解决黏包核心逻辑 -------------------
# 7. 定义要发送给客户端的真实数据
msg = "Python黏包解决测试,这是一段较长的数据"

# 8. 把字符串编码为二进制(网络只能传输字节流)
msg_bytes = msg.encode("utf-8")

# 9. 计算二进制数据的长度(核心:告诉客户端要收多少数据)
msg_len = len(msg_bytes)

# 10. 【关键】用struct把数据长度打包成 固定4字节 二进制
# 格式符i = 4字节整型,无论长度多大,包头永远是4字节
packed_len = struct.pack("i", msg_len)

# 11. 第一步:先发送【4字节的长度包头】
conn.send(packed_len)

# 12. 第二步:再发送【真实的业务数据】
conn.send(msg_bytes)

# 13. 额外发送一条数据(测试:两条数据完全分离,不会黏包)
conn.send("额外数据".encode("utf-8"))
# --------------------------------------------------------

# 14. 关闭与客户端的连接(四次挥手)
conn.close()

# 15. 关闭服务端套接字,释放资源
sk.close()

🔥 核心原理

  1. 为什么这样能解决黏包?
    TCP是流式数据无边界,客户端不知道第一条数据在哪结束;
    我们先发固定4字节的长度,客户端先收4字节→解包得到数据长度→严格按照长度收数据,剩余数据就是下一条,完美分隔。

  2. 执行顺序(服务端)
    建立连接 → 计算数据长度 → 打包4字节包头并发送 → 发送真实数据 → 发送额外数据 → 断开连接


步骤3:客户端(先收长度,再收数据)

这是黏包解决方案的客户端实现,严格按照「先收4字节长度 → 按长度收数据」的逻辑,完美区分多条数据,彻底解决黏包问题

# 1. 导入模块
# socket:实现TCP网络通信
# struct:解包服务端发送的4字节长度包头,还原数据长度
import socket
import struct

# 2. 创建TCP客户端套接字(IPv4 + 流式TCP协议)
sk = socket.socket()

# 3. 主动连接服务端(IP和端口必须与服务端完全一致)
sk.connect(("127.0.0.1", 9000))

# ------------------- 解决黏包核心接收逻辑 -------------------
# 4. 第一步:固定接收 4 字节数据
# 服务端用struct.pack("i") 发送了固定4字节的长度包头,所以这里只收4字节
packed_len = sk.recv(4)

# 5. 解包4字节包头,还原真实的数据长度
# struct.unpack("i", 数据) 返回元组,必须用 [0] 取出长度数值
msg_len = struct.unpack("i", packed_len)[0]

# 6. 第二步:严格按照解包得到的长度,接收真实数据
# 精准接收指定字节数,不会多收也不会少收,完美分隔数据
data1 = sk.recv(msg_len).decode("utf-8")

# 7. 第三步:接收服务端额外发送的第二条数据(无黏包,独立接收)
# 第一条数据已经精准收完,剩余的数据就是第二条独立数据
data2 = sk.recv(1024).decode("utf-8")
# ------------------------------------------------------------

# 8. 打印结果:两条数据完全分离,验证黏包已解决
print("主数据:", data1)
print("额外数据:", data2)

# 9. 关闭客户端套接字,断开连接,释放网络资源
sk.close()

🔥 核心逻辑(客户端+服务端联动)

  1. 通信流程
    服务端:发4字节长度 → 发主数据 → 发额外数据
    客户端:收4字节 → 解包得长度 → 收主数据 → 收额外数据
  2. 为什么能解决黏包?
    客户端不再盲目接收,而是通过固定4字节包头知道第一条数据的精确大小,严格按大小接收,剩余数据自动成为独立的第二条,彻底分隔开。
  3. 执行结果
    主数据和额外数据会分开打印,不会黏合成一条,黏包问题完美解决!
执行过程
  1. 服务端:计算数据长度→打包成4字节→发送长度→发送实际数据→发送额外数据;
  2. 客户端:接收4字节→解包得到长度→按长度接收主数据→接收额外数据;
  3. 输出结果:主数据和额外数据分开,无黏包。

3. socketserver(TCP并发)

3.1 核心逻辑

TCP基础版服务端只能处理1个客户端,socketserver的ThreadingTCPServer可创建多线程,同时处理多个客户端。

步骤1:服务端(并发版)
# 导入Python内置的socketserver模块,用于快速实现并发TCP服务端
import socketserver

# 自定义处理类,必须继承socketserver的BaseRequestHandler基类
# 基类封装了连接逻辑,我们只需要重写handle方法实现通信
class MyTCPServer(socketserver.BaseRequestHandler):
    # 重写handle方法:所有和客户端的通信逻辑都写在这里
    # 每有一个客户端连接,就会自动创建一个线程执行这个方法
    def handle(self):
        # self.request 等价于原生socket的conn连接对象
        # 专门用于和当前客户端收发数据
        conn = self.request
        
        # self.client_address 存储客户端的(IP, 端口)信息
        print(f"[{self.client_address}] 已连接")
        
        # 循环:持续和客户端收发消息
        while True:
            # 接收客户端发送的数据,最大接收1024字节,并解码为字符串
            msg = conn.recv(1024).decode("utf-8")
            
            # 如果接收为空,说明客户端主动断开连接,退出循环
            if not msg:
                break
            
            # 打印客户端发送的消息
            print(f"[{self.client_address}]:{msg}")
            
            # 服务端回复:将消息转为大写,编码后发送给客户端
            conn.send(msg.upper().encode("utf-8"))
        
        # 客户端断开后打印提示
        print(f"[{self.client_address}] 已断开")
        # 关闭当前客户端的连接
        conn.close()

# 创建多线程TCP服务器
# 参数1:服务端绑定的IP和端口
# 参数2:指定自定义的处理类MyTCPServer
# ThreadingTCPServer:多线程模式,支持同时处理多个客户端
server = socketserver.ThreadingTCPServer(("127.0.0.1", 9000), MyTCPServer)

# 启动服务端,永久监听客户端连接(无限循环运行)
server.serve_forever()

核心注释总结

  1. 核心对象
    self.request = 客户端连接通道(收发数据)
    self.client_address = 客户端地址
  2. 核心功能
    ThreadingTCPServer = 多线程并发,解决单客户端限制
  3. 执行逻辑
    服务端启动 → 监听连接 → 新客户端接入 → 创建线程 → 循环通信 → 断开连接

步骤2:客户端(多开测试)

线程TCP服务端配套客户端代码

# 导入socket模块,实现TCP客户端网络通信
import socket

# 创建TCP客户端套接字对象 (IPv4 + TCP流式协议)
sk = socket.socket()

# 主动连接服务端,IP和端口必须与服务端完全一致
sk.connect(("127.0.0.1", 9000))

# 无限循环:实现客户端与服务端持续收发消息
while True:
    # 控制台输入要发送给服务端的消息
    msg = input("客户端发送:")
    
    # 将字符串编码为二进制字节流,发送给服务端
    sk.send(msg.encode("utf-8"))
    
    # 接收服务端的回复数据,最大接收1024字节,并解码为字符串
    reply = sk.recv(1024).decode("utf-8")
    
    # 打印服务端的回复内容
    print(f"服务端回复:{reply}")

# 关闭客户端套接字(无限循环,这行代码永远不会执行)
sk.close()

核心说明

  1. 配合使用:这个客户端专门对接上面的 socketserver 多线程服务端,可以多开几个客户端,同时和服务端通信
  2. 循环逻辑:while True 让客户端可以一直发消息、收回复,直到手动关闭程序
  3. 编码解码:网络传输只能传二进制,所以发送要encode,接收要decode

执行过程
  1. 启动服务端:循环监听9000端口;
  2. 启动客户端1:连接→服务端创建线程处理,客户端发“hello”→服务端回复“HELLO”;
  3. 启动客户端2:连接→服务端创建新线程处理,客户端发“world”→服务端回复“WORLD”;
  4. 两个客户端可同时发消息,服务端分别回复,实现并发。

4. 反射机制

4.1 核心逻辑

反射是“通过字符串找/调功能”,比如输入字符串“cry”,就能调用对象的cry()方法,无需硬编码obj.cry()

步骤1:类的反射(演示)
# 定义类和对象
class Children:
    hair = "黑色"
    def cry(self):
        print("小孩哭了")
    def smile(self):
        print("小孩笑了")
obj = Children()

# 1. hasattr:检测是否有该属性/方法(字符串传参)
print(hasattr(obj, "hair"))  # True
print(hasattr(obj, "cry"))   # True

# 2. getattr:获取属性/方法(找不到返回默认值)
hair = getattr(obj, "hair")
print(hair)  # 黑色
cry_func = getattr(obj, "cry")
cry_func()   # 小孩哭了

# 3. setattr:设置属性/方法
setattr(obj, "height", 100)
print(obj.height)  # 100

# 4. delattr:删除属性/方法
delattr(obj, "height")
# print(obj.height)  # 报错(已删除)
步骤2:模块的反射(演示)
import sys
# 获取当前模块对象
current_module = sys.modules["__main__"]

# 定义模块内函数
def func1():
    print("执行功能1")
def func2():
    print("执行功能2")

# 循环输入字符串,调用对应函数
while True:
    func_name = input("请输入要调用的函数(func1/func2):")
    if hasattr(current_module, func_name):
        func = getattr(current_module, func_name)
        func()
    else:
        print("函数不存在")
执行过程
  1. 运行代码→输入“func1”→反射调用func1()→输出“执行功能1”;
  2. 输入“func2”→反射调用func2()→输出“执行功能2”;
  3. 输入“func3”→提示“函数不存在”。

应用场景及案例(带详细执行过程)

1. UDP应用场景:实时群聊

1.1 场景说明

UDP无需维护连接,速度快,适合简单的多人群聊(服务端转发消息)。

1.2 案例代码(简化版)
服务端(转发消息)
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(("127.0.0.1", 9000))
# 存储已连接的客户端地址
clients = set()
print("UDP群聊服务端启动...")
while True:
    # 接收消息
    msg, cli_addr = sk.recvfrom(1024)
    # 新客户端加入
    if cli_addr not in clients:
        clients.add(cli_addr)
        print(f"[{cli_addr}] 加入群聊")
    # 转发消息给所有客户端
    for client in clients:
        if client != cli_addr:  # 不发给自己
            sk.sendto(f"[{cli_addr}]:{msg.decode('utf-8')}".encode("utf-8"), client)
# 1. 导入socket模块,用于实现UDP网络通信
import socket

# 2. 创建UDP套接字对象
# type=socket.SOCK_DGRAM:固定参数,代表使用UDP数据报协议(区别于TCP的SOCK_STREAM)
sk = socket.socket(type=socket.SOCK_DGRAM)

# 3. 绑定服务端的IP地址和端口号(UDP服务端必须绑定,客户端才能找到)
# 127.0.0.1:本机回环地址,仅本机测试
# 9000:服务端监听端口
sk.bind(("127.0.0.1", 9000))

# 4. 定义一个集合(set),存储所有加入群聊的客户端地址 (IP, 端口)
# 集合自动去重,保证同一个客户端不会被重复存储
clients = set()

# 5. 打印服务端启动提示
print("UDP群聊服务端启动...")

# 6. 无限循环:持续接收客户端消息并转发
while True:
    # 7. UDP接收消息(核心方法:recvfrom)
    # 接收最多1024字节数据,返回两个值:
    # msg:客户端发送的二进制数据
    # cli_addr:发送消息的客户端地址 (IP, 端口)
    msg, cli_addr = sk.recvfrom(1024)

    # 8. 判断:如果该客户端是第一次发消息(地址不在集合中)
    if cli_addr not in clients:
        # 将新客户端地址加入集合
        clients.add(cli_addr)
        # 打印新成员加入群聊的提示
        print(f"[{cli_addr}] 加入群聊")

    # 9. 消息转发逻辑:遍历所有已加入的客户端
    for client in clients:
        # 条件:不把消息发给发送者自己(群聊逻辑)
        if client != cli_addr:
            # 10. UDP发送消息(核心方法:sendto)
            # 格式:拼接 客户端地址 + 消息内容,编码为二进制
            # 参数:(发送的字节数据, 目标客户端地址)
            sk.sendto(f"[{cli_addr}]:{msg.decode('utf-8')}".encode("utf-8"), client)

UDP群聊服务端代码
这是无连接UDP协议实现的群聊服务端,核心是接收消息 → 广播转发给所有客户端,无需建立连接,支持多人同时聊天

🔥 核心知识点(必看)

  1. UDP关键方法
    • 接收:recvfrom(1024) → 返回 (消息, 客户端地址)
    • 发送:sendto(数据, 目标地址) → 必须指定接收方地址
  2. 群聊原理
    集合存储所有客户端地址,收到消息后遍历集合,转发给除发送者外的所有人
  3. UDP特点
    无连接、不需要connect/accept,代码比TCP更简单,适合轻量聊天场景

客户端
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
print("UDP群聊客户端启动,输入消息发送...")
while True:
    msg = input("我:")
    sk.sendto(msg.encode("utf-8"), ("127.0.0.1", 9000))

UDP群聊客户端代码(逐行极简注释)

# 导入socket模块,用于实现UDP网络通信
import socket

# 创建UDP套接字对象
# type=socket.SOCK_DGRAM 固定参数,表示使用UDP协议
sk = socket.socket(type=socket.SOCK_DGRAM)

# 打印客户端启动提示
print("UDP群聊客户端启动,输入消息发送...")

# 无限循环:持续发送消息
while True:
    # 控制台输入要发送的群聊消息
    msg = input("我:")
    # UDP发送消息:必须指定 服务端IP+端口
    # 先把字符串编码为二进制,再通过sendto发送给服务端
    sk.sendto(msg.encode("utf-8"), ("127.0.0.1", 9000))

# 关闭套接字(无限循环,这行代码不会执行)
# sk.close()

核心要点

  1. UDP客户端无需connect,直接用sendto指定服务端地址就能发消息
  2. 配合上面的UDP群聊服务端,多开几个客户端即可实现多人聊天
  3. 服务端会自动转发消息,实现群聊效果

1.3 执行过程
  1. 启动服务端:绑定9000端口,初始化空集合存储客户端;
  2. 启动客户端1:输入“大家好”→发送到服务端;
  3. 服务端:将客户端1地址加入集合→转发消息给所有客户端(暂无其他);
  4. 启动客户端2:输入“你好呀”→发送到服务端;
  5. 服务端:将客户端2地址加入集合→转发消息给客户端1;
  6. 客户端1收到消息:[(127.0.0.1, 58001)]:你好呀
  7. 实现多客户端实时群聊,消息自动转发。


2. TCP应用场景:文件传输(解决黏包)

2.1 场景说明

TCP可靠,适合文件传输;用struct解决黏包,确保文件完整接收。

2.2 案例代码
服务端(发送文件)
import socket
import struct
# 读取文件内容
with open("test.txt", "r", encoding="utf-8") as f:
    file_content = f.read()
# TCP服务端初始化
sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(("127.0.0.1", 9000))
sk.listen()
conn, addr = sk.accept()
# 打包文件长度并发送
content_bytes = file_content.encode("utf-8")
content_len = len(content_bytes)
conn.send(struct.pack("i", content_len))
# 发送文件内容
conn.send(content_bytes)
print("文件发送完成")
conn.close()
sk.close()

TCP文件传输服务端(解决黏包)逐行注释
核心:先发送文件长度(4字节) → 再发送文件内容,彻底避免黏包,保证文件完整传输

# 1. 导入模块
# socket:实现TCP网络通信
# struct:打包文件长度为固定4字节,解决TCP黏包问题
import socket
import struct

# 2. 读取本地文件内容
# with open:自动关闭文件,安全读写
# "test.txt":要发送的文件名
# "r":只读模式,encoding="utf-8":指定编码防止中文乱码
# f.read():一次性读取文件全部内容到变量中
with open("test.txt", "r", encoding="utf-8") as f:
    file_content = f.read()

# 3. 创建TCP套接字(默认IPv4 + 流式TCP协议)
sk = socket.socket()

# 4. 端口复用:解决测试时端口占用报错,程序关闭后立即释放端口
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 5. 绑定本机IP和端口,等待客户端连接
sk.bind(("127.0.0.1", 9000))

# 6. 开启监听模式
sk.listen()

# 7. 阻塞等待客户端连接
# conn:客户端连接对象,专门收发数据
# addr:客户端IP+端口
conn, addr = sk.accept()

# ------------------- 解决黏包 + 文件传输核心 -------------------
# 8. 将文件字符串编码为二进制(网络只能传输字节流)
content_bytes = file_content.encode("utf-8")

# 9. 计算文件二进制数据的总长度(核心:告诉客户端要接收多少数据)
content_len = len(content_bytes)

# 10. 【关键】用struct把长度打包为 固定4字节 二进制
# 格式符i = 4字节整型,客户端固定收4字节就能解析长度
# 第一步:发送4字节的长度包头
conn.send(struct.pack("i", content_len))

# 11. 第二步:发送文件的真实二进制数据
conn.send(content_bytes)
# ----------------------------------------------------------------

# 12. 打印发送完成提示
print("文件发送完成")

# 13. 关闭与客户端的连接(四次挥手)
conn.close()

# 14. 关闭服务端套接字,释放网络资源
sk.close()

核心逻辑总结

  1. 文件读取:读取本地test.txt的全部文本内容
  2. 黏包解决:严格遵循 先发固定4字节长度 → 再发文件数据 的规则
  3. 数据格式:字符串必须encode编码为二进制才能网络传输
  4. 配套使用:需要对接之前的TCP文件传输客户端,才能完整接收文件
客户端(接收文件)
import socket
import struct
# TCP客户端初始化
sk = socket.socket()
sk.connect(("127.0.0.1", 9000))
# 接收并解包文件长度
packed_len = sk.recv(4)
content_len = struct.unpack("i", packed_len)[0]
# 接收文件内容并保存
content_bytes = sk.recv(content_len)
with open("recv_test.txt", "w", encoding="utf-8") as f:
    f.write(content_bytes.decode("utf-8"))
print("文件接收完成,保存为recv_test.txt")
sk.close()

TCP文件传输客户端(解决黏包)逐行注释
配套服务端使用:先收4字节长度 → 按长度收文件 → 保存本地,完美解决黏包、文件损坏问题

# 1. 导入模块
# socket:实现TCP网络连接与数据传输
# struct:解包服务端发送的4字节长度包头,还原文件大小
import socket
import struct

# 2. 创建TCP客户端套接字(IPv4协议 + TCP流式传输协议)
sk = socket.socket()

# 3. 主动连接服务端
# IP和端口必须与服务端的bind、listen配置完全一致
sk.connect(("127.0.0.1", 9000))

# ------------------- 解决黏包核心:精准接收文件 -------------------
# 4. 固定接收 4 字节数据
# 服务端用struct打包了文件长度,固定4字节,不会黏包
packed_len = sk.recv(4)

# 5. 解包4字节数据,还原出【文件的真实长度】
# unpack返回元组,[0]取出长度数值
content_len = struct.unpack("i", packed_len)[0]

# 6. 严格按照解包后的长度,接收完整文件二进制数据
# 精准接收,不多不少,彻底避免黏包和文件丢失
content_bytes = sk.recv(content_len)
# ----------------------------------------------------------------

# 7. 将接收的二进制文件数据写入本地文件
# with open:自动管理文件开关,安全写入
# recv_test.txt:保存的文件名,w=写入模式,utf-8编码
with open("recv_test.txt", "w", encoding="utf-8") as f:
    # 二进制解码为字符串,写入文件
    f.write(content_bytes.decode("utf-8"))

# 8. 打印文件接收完成提示
print("文件接收完成,保存为recv_test.txt")

# 9. 关闭客户端套接字,断开与服务端的连接,释放资源
sk.close()

核心执行流程(和服务端一一对应)

  1. 客户端连接服务端
  2. 收4字节长度 → 解包得到文件大小
  3. 按长度收完整文件数据
  4. 写入本地recv_test.txt
  5. 断开连接

关键亮点

  • 彻底解决TCP黏包,文件传输100%完整
  • 固定4字节包头,通用稳定,是网络文件传输的标准方案
2.3 执行过程
  1. 准备test.txt:写入“TCP文件传输测试”;
  2. 启动服务端:读取文件→绑定端口→等待连接;
  3. 启动客户端:连接→接收4字节长度→解包得到文件长度→按长度接收内容→保存为recv_test.txt
  4. 打开recv_test.txt,内容与原文件一致,无黏包,传输完整。

3. 反射应用场景:动态功能菜单

3.1 场景说明

反射可通过用户输入的字符串调用对应功能,无需硬编码if/elif,适合可扩展的系统菜单。

3.2 案例代码
# 定义功能函数
def add_user():
    print("✅ 添加用户成功")
def del_user():
    print("✅ 删除用户成功")
def query_user():
    print("✅ 查询用户成功")
def exit_sys():
    print("🔚 退出系统")
    return "exit"

# 反射实现动态调用
import sys
current_module = sys.modules["__main__"]
# 功能映射:编号→函数名
func_map = {
    "1": "add_user",
    "2": "del_user",
    "3": "query_user",
    "4": "exit_sys"
}
# 菜单循环
while True:
    print("\n=== 用户管理系统 ===")
    print("1. 添加用户 | 2. 删除用户 | 3. 查询用户 | 4. 退出")
    choice = input("请输入功能编号:")
    if choice not in func_map:
        print("❌ 输入错误")
        continue
    # 反射调用函数
    func = getattr(current_module, func_map[choice])
    res = func()
    if res == "exit":
        break

反射实现动态菜单 - 完整代码逐行注释
核心:用字符串动态调用函数,告别大量if/elif,代码扩展性极强

# ------------------- 1. 定义业务功能函数 -------------------
# 定义添加用户的功能函数
def add_user():
    print("✅ 添加用户成功")

# 定义删除用户的功能函数
def del_user():
    print("✅ 删除用户成功")

# 定义查询用户的功能函数
def query_user():
    print("✅ 查询用户成功")

# 定义退出系统的功能函数
def exit_sys():
    print("🔚 退出系统")
    # 返回exit标记,用于外层循环判断退出程序
    return "exit"

# ------------------- 2. 反射核心配置 -------------------
# 导入sys模块,用于获取当前运行的模块对象
import sys

# 获取当前程序的主模块对象(__main__)
# 作用:让反射能找到当前文件中定义的所有函数
current_module = sys.modules["__main__"]

# 功能映射表:键=用户输入的编号,值=对应的函数名(字符串格式)
# 核心:把用户输入的数字 和 函数名 关联起来,用于反射调用
func_map = {
    "1": "add_user",
    "2": "del_user",
    "3": "query_user",
    "4": "exit_sys"
}

# ------------------- 3. 主菜单循环 -------------------
# 无限循环,持续展示菜单
while True:
    # 打印菜单界面
    print("\n=== 用户管理系统 ===")
    print("1. 添加用户 | 2. 删除用户 | 3. 查询用户 | 4. 退出")
    
    # 获取用户输入的功能编号
    choice = input("请输入功能编号:")

    # 判断:用户输入的编号不在映射表中,提示错误
    if choice not in func_map:
        print("❌ 输入错误")
        # 跳过本次循环,重新展示菜单
        continue

    # ------------------- 4. 反射核心调用 -------------------
    # 1. 通过映射表拿到 函数名字符串
    # 2. getattr(模块对象, 函数字符串) → 动态获取函数本身
    func = getattr(current_module, func_map[choice])
    
    # 调用获取到的函数,并接收返回值
    res = func()

    # 判断:如果函数返回"exit",代表要退出系统,终止循环
    if res == "exit":
        break

🔥 核心知识点

  1. 反射的作用
    不用写 if choice ==1: add_user(),通过字符串直接调用函数,新增功能只需改func_map,无需修改循环逻辑
  2. 核心函数
    getattr(模块, 函数字符串) → 根据字符串找到并返回函数
  3. 执行流程
    展示菜单 → 用户输入编号 → 反射匹配函数 → 执行功能 → 输入4退出
3.3 执行过程
  1. 运行代码→打印菜单;
  2. 输入“1”→映射到add_user→反射调用→输出“✅ 添加用户成功”;
  3. 输入“3”→映射到query_user→反射调用→输出“✅ 查询用户成功”;
  4. 输入“4”→映射到exit_sys→反射调用→输出“🔚 退出系统”→循环结束;
  5. 新增功能时,只需添加函数+更新func_map,无需修改循环逻辑,扩展性极强。


29、加密模块_TCPUDP_Socket

全文总结

1. 加密模块(hashlib/hmac)

模块/算法 核心特征 核心用法 适用场景
hashlib.md5 32位16进制摘要,加密快,安全性中等 创建对象→update(字节流)→hexdigest();支持加盐(固定/动态) 密码加密、文件内容校验
hashlib.sha1/sha512 sha1(40位)/sha512(128位),加密慢、安全性更高 用法同md5,仅算法对象不同 对安全性要求更高的加密场景
hmac 带密钥的哈希加密,破解难度更高 hmac.new(密钥, 字节流)→hexdigest() 网络连接合法性校验
文件MD5校验 小文件全量读取,大文件分块读取 大文件循环分块update,避免内存溢出 文件完整性验证(如传输后校验)

2. TCP/UDP协议

协议 连接特性 优缺点 核心问题 典型应用
TCP 面向连接(三次握手/四次挥手)、可靠传输 优点:稳定不丢包、无数据大小限制;缺点:慢、效率低 黏包(数据无边界,收发频繁时多条数据黏合) 登录验证、文件传输、Web访问
UDP 无连接、不可靠传输 优点:速度快、无黏包、资源消耗少;缺点:易丢包、数据大小受限 无黏包,但传输不稳定 实时视频、IP电话、群聊

3. Socket网络通信

角色 核心方法 核心流程 关键注意点
服务端 bind/listen/accept/recv/send 绑定端口→监听→接受连接→收发数据→关闭连接 setsockopt解决端口重复绑定问题
客户端 connect/send/recv 连接服务端→收发数据→关闭连接 数据需序列化(如json)后转字节流传输

完整详细输出

第一步:加密模块(hashlib/hmac)

1.1 hashlib基础用法(MD5)

核心逻辑:加密对象仅处理字节流,需先将字符串encode;相同内容加密结果固定,不同内容结果不同。

import hashlib
import random  # 动态加盐用

# 基础加密流程(无盐)
# 步骤1:创建MD5加密对象
hs = hashlib.md5()
# 步骤2:传入待加密的字节流(字符串需encode成utf-8)
hs.update("abc123".encode("utf-8"))
# 步骤3:获取32位16进制加密结果
res = hs.hexdigest()
print("基础MD5加密结果:", res, "长度:", len(res))  
# 运算结果:e99a18c428cb38d5f260853678922e03 长度:32

# 固定加盐(提升密码复杂度,密钥固定)
hs_salt = hashlib.md5("自定义密钥_XBoy_".encode("utf-8"))  # 加盐:加密对象创建时传入密钥
hs_salt.update("abc123".encode("utf-8"))
res_salt = hs_salt.hexdigest()
print("固定加盐MD5结果:", res_salt)  
# 运算结果:116a497d72be80227c7b6c1cb4521594

# 动态加盐(密钥随机,更安全)
random_salt = str(random.randrange(10000, 100000))  # 生成5位随机数作为盐
hs_rand = hashlib.md5(random_salt.encode("utf-8"))
hs_rand.update("abc123".encode("utf-8"))
res_rand = hs_rand.hexdigest()
print("动态加盐MD5结果:", res_rand)  
# 运算结果(示例):d2b62001e744b5fabdd381191569b999

# SHA系列(用法同MD5,仅算法对象不同)
# SHA1(40位)
hs_sha1 = hashlib.sha1()
hs_sha1.update("abc123".encode("utf-8"))
res_sha1 = hs_sha1.hexdigest()
print("SHA1加密结果:", res_sha1, "长度:", len(res_sha1))  
# 运算结果:469e80d32c0559f89a44b8c88926220108ff4840 长度:40

# SHA512(128位)
hs_sha512 = hashlib.sha512()
hs_sha512.update("abc123".encode("utf-8"))
res_sha512 = hs_sha512.hexdigest()
print("SHA512加密结果:", res_sha512, "长度:", len(res_sha512))  
# 运算结果(超长):长度固定128位
1.2 hmac加密(带密钥,更安全)

核心逻辑:将“密钥+待加密内容”一起加密,比hashlib加盐更难破解,需传入字节流。

import hmac
import os  # 生成随机密钥用

# 基础hmac加密
key = b"固定密钥_abc"  # 密钥必须是字节流
msg = b"abc123"       # 待加密内容(字节流)
hm = hmac.new(key, msg)
res_hmac = hm.hexdigest()
print("基础hmac加密结果:", res_hmac, "长度:", len(res_hmac))  
# 运算结果:ddc4981deef83156ea53d68b6e2f4416 长度:32

# 随机密钥hmac(更安全)
random_key = os.urandom(32)  # 生成32位随机二进制密钥
hm_rand = hmac.new(random_key, msg)
res_hmac_rand = hm_rand.hexdigest()
print("随机密钥hmac结果:", res_hmac_rand)  
# 运算结果(示例):e87f1a07beda362dbfedb5858d246492
1.3 文件MD5校验(小文件+大文件)

核心逻辑:小文件直接读全量,大文件分块读取(避免内存溢出),最终MD5一致则文件内容一致。

import hashlib
import os

# 场景1:小文件校验(直接读取全量内容)
def check_md5_small(file_path):
    hs = hashlib.md5()
    with open(file_path, mode="rb") as fp:  # rb模式读字节流,避免编码问题
        hs.update(fp.read())  # 一次性读取全量内容
    return hs.hexdigest()

# 测试:假设ceshi1.txt和ceshi2.txt内容相同
res1 = check_md5_small("ceshi1.txt")
res2 = check_md5_small("ceshi2.txt")
print("小文件1 MD5:", res1)  # 运算结果:554be14fdf647e7be24c5c78bf52400d
print("小文件2 MD5:", res2)  # 运算结果:554be14fdf647e7be24c5c78bf52400d(内容相同则一致)

# 场景2:大文件校验(分块读取,避免内存溢出)
def check_md5_big(file_path):
    hs = hashlib.md5()
    file_size = os.path.getsize(file_path)  # 获取文件总大小
    with open(file_path, mode="rb") as fp:
        while file_size:  # 循环读取直到文件读完
            content = fp.read(1024)  # 每次读1024字节(可自定义大小)
            hs.update(content)       # 分块更新加密
            file_size -= len(content)  # 剩余未读大小递减
    return hs.hexdigest()

res_big = check_md5_big("ceshi3.txt")  # 假设是20G大文件
print("大文件MD5:", res_big)
# 运算逻辑:分块读取每1024字节,逐个update,最终生成唯一MD5

第二步:TCP/UDP协议核心

2.1 核心概念
  • TCP:像“打电话”,必须先接通(三次握手),说话要对方确认,挂电话要确认(四次挥手),稳定但慢。
    • 三次握手:客户端→服务端(请求连接)→服务端→客户端(确认连接)→客户端→服务端(最终确认)
    • 四次挥手:客户端→服务端(请求断开)→服务端→客户端(确认收到)→服务端→客户端(准备断开)→客户端→服务端(最终确认)
    • 黏包问题:TCP无数据边界,收发频繁时多条数据黏成一条(比如连续发“a”和“b”,接收端可能拿到“ab”)。
  • UDP:像“发短信”,不用接通,直接发,对方收没收到不管,快但可能丢包,无黏包。
2.2 Socket基础(TCP通信)

Socket是“套接字”,是网络通信的工具,TCP通信分服务端和客户端,步骤如下:

2.2.1 TCP登录案例(服务端)

核心逻辑:绑定端口→监听→接受连接→接收客户端数据→校验账号密码→返回结果。

import socket
import hashlib
import json

# 步骤1:定义密码加密函数(结合用户名加盐)
def get_md5_code(usr, pwd):
    hm = hashlib.md5(usr.encode())  # 以用户名为盐
    hm.update(pwd.encode())
    return hm.hexdigest()  # 返回加密后的密码

# 步骤2:创建Socket对象并配置
sk = socket.socket()  # 默认AF_INET(ipv4)+SOCK_STREAM(TCP)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 允许端口重复绑定(测试用)
sk.bind(("127.0.0.1", 9001))  # 绑定地址+端口
sk.listen()  # 开始监听

# 步骤3:接受客户端连接(三次握手)
conn, addr = sk.accept()  # 阻塞等待客户端连接,返回连接对象+客户端地址

# 步骤4:处理客户端数据
msg = conn.recv(1024).decode()  # 接收客户端发送的字节流,转字符串
dic = json.loads(msg)  # 反序列化成字典(客户端传的{用户名,密码,操作})

# 步骤5:校验账号密码(模拟数据库:userinfo.txt,格式:用户名:加密密码)
sign = False  # 默认登录失败
with open("userinfo.txt", mode="r", encoding="utf-8") as fp:
    for line in fp:
        usr_db, pwd_db = line.strip().split(":")  # 读取数据库中的账号密码
        # 对比:用户名一致 + 客户端密码加密后和数据库一致
        if usr_db == dic['username'] and pwd_db == get_md5_code(dic['username'], dic['password']):
            res = {"code": 1}  # 1=登录成功
            conn.send(json.dumps(res).encode())  # 序列化+转字节流发送
            sign = True

# 步骤6:登录失败返回结果
if not sign:
    res = {"code": 0}  # 0=登录失败
    conn.send(json.dumps(res).encode())

# 步骤7:关闭连接(四次挥手)
conn.close()
sk.close()
2.2.2 TCP登录案例(客户端)

核心逻辑:连接服务端→输入账号密码→序列化数据→发送→接收结果→判断登录状态。

import socket
import json

# 步骤1:创建Socket对象
sk = socket.socket()
sk.connect(("127.0.0.1", 9001))  # 连接服务端地址+端口

# 步骤2:输入账号密码并封装数据
usr = input("请输入您的用户名:")
pwd = input("请输入您的密码:")
dic = {"username": usr, "password": pwd, "operate": "login"}  # 封装请求字典

# 步骤3:序列化+转字节流发送
res = json.dumps(dic)  # 字典转字符串
bytes_msg = res.encode("utf-8")  # 字符串转字节流
sk.send(bytes_msg)  # 发送给服务端

# 步骤4:接收服务端结果并判断
res_msg = sk.recv(1024).decode()  # 字节流转字符串
dic_code = json.loads(res_msg)  # 字符串转字典

if dic_code['code']:
    print("恭喜你,登陆成功!~")  # code=1时执行
else:
    print("抱歉,登陆失败~")     # code=0时执行

# 步骤5:关闭连接
sk.close()
2.3 Socket合法性校验(TCP)

核心逻辑:服务端生成随机密钥→发给客户端→双方用相同密钥加密随机值→对比加密结果,一致则合法。

2.3.1 合法性校验(服务端)
import socket
import hmac
import os

# 步骤1:定义合法性校验函数
def auth(conn, secret_key):
    # 生成32位随机二进制字节流(挑战码)
    msg = os.urandom(32)
    conn.send(msg)  # 发给客户端
    
    # 服务端加密挑战码
    hm = hmac.new(secret_key, msg)
    res_serve = hm.hexdigest()
    
    # 接收客户端加密结果并对比
    res_client = conn.recv(1024).decode("utf-8")
    if res_serve == res_client:
        print("是一个合法连接")
        return True
    else:
        print("不合法的连接")
        return False

# 步骤2:Socket基础配置
sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(("127.0.0.1", 9000))
sk.listen()

# 步骤3:接受连接并校验
conn, addr = sk.accept()
secret_key = b"love is my best gift for you"  # 双方约定的密钥
if auth(conn, secret_key):
    print(conn.recv(1024).decode("utf-8"))  # 合法则接收客户端消息

# 步骤4:关闭连接
conn.close()
sk.close()
2.3.2 合法性校验(客户端)
import socket
import hmac

# 步骤1:定义校验函数
def auth(sk, secret_key):
    # 接收服务端的随机挑战码
    msg = sk.recv(32)
    # 客户端用相同密钥加密挑战码
    hm = hmac.new(secret_key, msg)
    res = hm.hexdigest()
    sk.send(res.encode())  # 发给服务端

# 步骤2:Socket连接+校验
sk = socket.socket()
secret_key = b"love is my best gift for you"  # 和服务端一致的密钥
sk.connect(("127.0.0.1", 9000))
auth(sk, secret_key)  # 执行校验

# 步骤3:校验通过后发送业务数据
sk.send(b'download')  # 发送“下载”请求

# 步骤4:关闭连接
sk.close()

应用场景及案例(带注释+运算过程)

场景1:文件完整性校验(MD5)

适用场景
  • 下载文件后校验(防止文件被篡改);
  • 备份文件后校验(确认备份和原文件一致);
  • 大文件传输后校验(避免传输过程中数据丢失/篡改)。
案例代码(大文件校验)
import hashlib
import os

def check_file_md5(file_path, chunk_size=1024*1024):
    """
    校验文件MD5(分块读取,支持超大文件)
    :param file_path: 文件路径
    :param chunk_size: 每次读取的块大小(默认1MB,可调整)
    :return: 文件MD5摘要
    """
    # 步骤1:创建MD5对象
    md5_obj = hashlib.md5()
    
    # 步骤2:获取文件大小(可选,用于进度提示)
    file_size = os.path.getsize(file_path)
    read_size = 0  # 已读取大小
    
    # 步骤3:分块读取文件并更新MD5
    with open(file_path, "rb") as f:
        while True:
            chunk = f.read(chunk_size)  # 读取1MB
            if not chunk:  # 读取完毕
                break
            md5_obj.update(chunk)  # 分块加密
            read_size += len(chunk)
            # 可选:打印进度
            print(f"已读取:{read_size}/{file_size} 字节")
    
    # 步骤4:返回最终MD5
    return md5_obj.hexdigest()

# 案例执行
if __name__ == "__main__":
    # 原文件MD5
    original_md5 = check_file_md5("原文件.txt")
    print("原文件MD5:", original_md5)  # 运算结果(示例):554be14fdf647e7be24c5c78bf52400d
    
    # 下载/备份后的文件MD5
    copy_md5 = check_file_md5("备份文件.txt")
    print("备份文件MD5:", copy_md5)
    
    # 校验结果
    if original_md5 == copy_md5:
        print("文件完整,未被篡改")
    else:
        print("文件被篡改/传输错误")

场景2:用户登录密码加密(hashlib加盐)

适用场景
  • 网站/APP用户登录(密码不明文存储,防止数据库泄露后密码被盗);
  • 后台管理系统账号验证。
案例代码(注册+登录)
import hashlib

def encrypt_pwd(username, password):
    """
    密码加密(以用户名为盐,提升安全性)
    :param username: 用户名(作为盐)
    :param password: 明文密码
    :return: 加密后的32位MD5字符串
    """
    # 步骤1:创建MD5对象,加入用户名作为盐
    md5_obj = hashlib.md5(username.encode("utf-8"))
    # 步骤2:更新明文密码
    md5_obj.update(password.encode("utf-8"))
    # 步骤3:返回加密结果
    return md5_obj.hexdigest()

def register(username, password):
    """
    注册:将加密后的密码写入文件(模拟数据库)
    """
    encrypt_p = encrypt_pwd(username, password)
    with open("user_db.txt", "a", encoding="utf-8") as f:
        f.write(f"{username}:{encrypt_p}\n")
    print("注册成功!")

def login(username, password):
    """
    登录:校验用户名+加密后的密码
    """
    encrypt_p = encrypt_pwd(username, password)
    with open("user_db.txt", "r", encoding="utf-8") as f:
        for line in f:
            usr, pwd = line.strip().split(":")
            if usr == username and pwd == encrypt_p:
                print("登录成功!")
                return True
    print("用户名/密码错误!")
    return False

# 案例执行
if __name__ == "__main__":
    # 注册(新人先执行注册)
    register("zhangsan", "123456")
    # 运算过程:
    # username=zhangsan → encode后为字节流,作为盐
    # password=123456 → encode后update到MD5对象
    # 最终加密结果:d8578edf8458ce06fbc5bb76a58c5ca4
    
    # 登录(正确密码)
    login("zhangsan", "123456")  # 输出:登录成功!
    # 登录(错误密码)
    login("zhangsan", "654321")  # 输出:用户名/密码错误!

场景3:网络连接合法性校验(hmac+TCP)

适用场景
  • 企业内部系统通信(防止非法客户端连接服务端);
  • 敏感数据传输前的身份验证(如文件下载、接口调用)。
案例代码(简化版)
# 服务端(server.py)
import socket
import hmac
import os

# 双方约定的密钥(核心,需保密)
SECRET_KEY = b"company_secret_123456"

def auth(conn):
    # 步骤1:生成随机挑战码
    challenge = os.urandom(16)  # 16位随机字节流
    conn.send(challenge)
    
    # 步骤2:服务端加密挑战码
    server_hmac = hmac.new(SECRET_KEY, challenge).hexdigest()
    
    # 步骤3:接收客户端加密结果并对比
    client_hmac = conn.recv(1024).decode()
    return server_hmac == client_hmac

# Socket服务端逻辑
sk = socket.socket()
sk.bind(("127.0.0.1", 8000))
sk.listen()
conn, addr = sk.accept()

if auth(conn):
    conn.send(b"验证通过,可下载数据")
    print(f"合法客户端 {addr} 连接成功")
else:
    conn.send(b"验证失败,拒绝连接")
    print(f"非法客户端 {addr} 被拒绝")

conn.close()
sk.close()

# 客户端(client.py)
import socket
import hmac

SECRET_KEY = b"company_secret_123456"  # 必须和服务端一致

def auth(sk):
    # 步骤1:接收服务端挑战码
    challenge = sk.recv(16)
    
    # 步骤2:客户端加密挑战码
    client_hmac = hmac.new(SECRET_KEY, challenge).hexdigest()
    
    # 步骤3:发送加密结果给服务端
    sk.send(client_hmac.encode())

# Socket客户端逻辑
sk = socket.socket()
sk.connect(("127.0.0.1", 8000))

auth(sk)
# 接收服务端结果
res = sk.recv(1024).decode()
print("服务端返回:", res)  # 验证通过则输出“验证通过,可下载数据”

sk.close()

场景4:TCP vs UDP 选型参考

业务场景 推荐协议 原因 注意点
电商订单提交 TCP 需确保订单数据100%送达,不丢包 需处理黏包(但订单数据量小,黏包影响低)
直播弹幕 UDP 弹幕允许少量丢失,要求实时性 控制单条弹幕大小(不超过UDP数据包限制)
大文件下载 TCP 确保文件完整,支持断点续传 黏包无需处理(最终合并成完整文件)
实时语音通话 UDP 语音要求低延迟,少量丢包不影响理解 可加冗余数据,降低丢包影响


30、进程

全文总结

模块 核心知识点 关键操作/特性
进程基础 进程是OS资源分配最小单位,有唯一PID;同一程序多次执行=多个独立进程 1. 用Process创建进程,start()启动
2. 进程间数据默认隔离
3. 自定义类需继承Process并重写run
进程控制 主进程默认等待所有子进程;守护进程随主进程代码结束终止 1. join():主进程等待子进程
2. daemon=True:设置守护进程(需在start()前)
进程同步 解决多进程修改共享数据的竞态问题;控制进程阻塞逻辑 1. Lock:互斥锁(acquire上锁/release解锁)
2. Event:通过set/clear控制wait阻塞
进程通信(IPC) 实现进程间数据传输,保证进程安全 1. Queue:队列(put存/get取)
2. 管道(Pipe):双向通信(较少用)
核心特性 并发(单CPU多任务)/并行(多CPU多任务);进程三状态(就绪/执行/阻塞) 1. 同步(单主线)/异步(多主线)
2. 阻塞(等待资源)/非阻塞(无等待)

完整详细讲解

1. 进程基础:创建与执行

1.1 核心定义

进程是操作系统资源分配的最小单位(分配CPU、内存等),每个进程有唯一进程号(PID);进程间数据默认彼此隔离,需通过IPC(队列/管道)通信。

1.2 基础创建:指定目标函数
# 导入核心模块:os(获取进程号)、Process(创建进程)
import os
from multiprocessing import Process

# 定义子进程要执行的函数
def child_task():
    """子进程执行逻辑"""
    # os.getpid():获取当前进程PID;os.getppid():获取父进程PID
    print(f"子进程 - PID: {os.getpid()}, 父进程PID: {os.getppid()}")

if __name__ == "__main__":
    # 主进程入口(Windows系统必须加,避免子进程重复执行主逻辑)
    print(f"主进程 - PID: {os.getpid()}, 父进程PID: {os.getppid()}")
    
    # 步骤1:创建进程对象,指定目标函数
    # target:子进程要执行的函数(仅传函数名,不加括号)
    p = Process(target=child_task)
    
    # 步骤2:启动子进程(转为就绪态,等待CPU调度)
    p.start()

# 执行过程:
# 1. 主进程先打印自身PID(如1234)和父进程PID(如终端/解释器PID)
# 2. 创建子进程对象p,启动后CPU调度子进程执行child_task
# 3. 子进程打印自身PID(如5678)和父进程PID(主进程PID:1234)
# 4. 主进程默认等待子进程执行完毕后终止
1.3 带参数的子进程
import os
from multiprocessing import Process

def child_task(n):
    """带参数的子进程函数:循环打印"""
    for i in range(1, n+1):
        print(f"子进程(PID:{os.getpid()}) - 循环{i}次")

if __name__ == "__main__":
    # 定义传入子进程的参数
    loop_num = 5
    
    # 创建进程对象:args传参(必须是元组,单个参数需加逗号)
    p = Process(target=child_task, args=(loop_num,))
    
    # 启动子进程
    p.start()
    
    # 主进程同步执行循环
    for i in range(1, loop_num+1):
        print(f"主进程(PID:{os.getpid()}) - 打印{'*'*i}")

# 执行过程(并发特性):
# 1. 主进程创建子进程并传参loop_num=5,启动后两者并发执行
# 2. CPU调度决定执行顺序,输出可能交替(如主进程先打印*,子进程再打印循环1)
# 3. 最终主进程打印5行星号,子进程打印5行循环数
1.4 自定义类创建进程
import os
from multiprocessing import Process

# 步骤1:继承Process父类(必须)
class MyProcess(Process):
    # 步骤2:重写构造方法(可选,用于传参)
    def __init__(self, arg):
        # 必须先调用父类构造方法,否则进程初始化失败
        super().__init__()
        self.arg = arg  # 保存传入的参数
    
    # 步骤3:重写run方法(核心,子进程自动执行run内逻辑)
    def run(self):
        print(f"自定义子进程 - PID: {os.getpid()}, 父进程PID: {os.getppid()}")
        print(f"子进程接收参数: {self.arg}")

if __name__ == "__main__":
    process_list = []
    # 创建10个子进程
    for i in range(10):
        # 步骤4:创建自定义进程对象,传入参数
        p = MyProcess(f"参数{i}")
        # 步骤5:启动子进程(start()自动调用run方法)
        p.start()
        process_list.append(p)
    
    # 步骤6:等待所有子进程执行完毕
    for p in process_list:
        p.join()
    
    print(f"主进程(PID:{os.getpid()}) - 所有子进程执行完毕")

# 执行过程:
# 1. 主进程循环创建10个自定义进程,每个传入不同参数
# 2. 启动后子进程自动执行run方法,打印PID和参数
# 3. join()让主进程阻塞,直到所有子进程执行完毕
# 4. 最终主进程打印结束信息

2. 进程核心特性

2.1 进程间数据隔离
import os
import time
from multiprocessing import Process

# 全局变量(模拟共享数据)
count = 100

def child_modify():
    """子进程尝试修改全局变量"""
    global count  # 声明使用全局变量
    count += 1
    print(f"子进程(PID:{os.getpid()}) - count: {count}")  # 输出101

if __name__ == "__main__":
    p = Process(target=child_modify)
    p.start()
    time.sleep(1)  # 等待子进程执行完毕
    print(f"主进程(PID:{os.getpid()}) - count: {count}")  # 输出100

# 执行过程:
# 1. 全局count初始=100,子进程启动后修改的是自身内存空间的count
# 2. 主进程的count不受影响,验证进程间数据完全隔离
2.2 join方法:主进程等待子进程
import os
from multiprocessing import Process

def send_email(index):
    """模拟发送邮件"""
    print(f"发送第{index}封邮件 - 子进程PID:{os.getpid()}")

if __name__ == "__main__":
    process_list = []
    # 创建10个子进程
    for i in range(10):
        p = Process(target=send_email, args=(i,))
        p.start()
        process_list.append(p)
    
    # 等待所有子进程执行完毕(同步控制)
    for p in process_list:
        p.join()
    
    print("主进程 - 所有邮件发送完毕")

# 执行过程:
# 1. 主进程创建10个子进程并启动,保存到列表
# 2. 循环调用join,主进程阻塞,直到每个子进程执行完毕
# 3. 所有邮件发送后,主进程才打印结束信息
2.3 守护进程
import os
import time
from multiprocessing import Process

def daemon_task():
    """守护进程:模拟监控逻辑"""
    count = 1
    while True:
        print(f"守护进程(PID:{os.getpid()}) - 监控中... 第{count}次")
        time.sleep(0.5)
        count += 1

def normal_task():
    """普通子进程:模拟核心业务"""
    print(f"普通子进程(PID:{os.getpid()}) - 开始执行")
    time.sleep(2)  # 模拟耗时操作
    print(f"普通子进程(PID:{os.getpid()}) - 执行完毕")

if __name__ == "__main__":
    # 创建守护进程
    p1 = Process(target=daemon_task)
    p1.daemon = True  # 设置为守护进程(必须在start前)
    p1.start()
    
    # 创建普通子进程
    p2 = Process(target=normal_task)
    p2.start()
    
    print(f"主进程(PID:{os.getpid()}) - 代码执行结束")

# 执行过程:
# 1. 主进程创建守护进程p1(监控)和普通进程p2(业务)
# 2. 启动后,p1循环打印监控信息,p2执行2秒业务逻辑
# 3. 主进程打印"代码执行结束"(主进程代码完毕)
# 4. 主进程等待普通进程p2执行完毕(2秒后),然后终止
# 5. 守护进程p1随主进程终止,不再循环监控

3. 进程同步与通信

3.1 锁(Lock):保证数据安全
import os
import time
from multiprocessing import Process, Lock

# 共享数据:计数器
num = 0

def add_num(lock):
    """修改共享数据,加锁保证原子性"""
    global num
    # 步骤1:上锁(同一时间仅一个进程能获取锁)
    lock.acquire()
    try:
        # 模拟耗时操作(放大数据错乱问题)
        temp = num
        time.sleep(0.1)
        num = temp + 1
        print(f"进程(PID:{os.getpid()}) - 修改后num: {num}")
    finally:
        # 步骤2:解锁(必须解锁,否则其他进程永久阻塞)
        lock.release()

if __name__ == "__main__":
    # 创建锁对象
    lock = Lock()
    process_list = []
    
    # 创建10个子进程
    for i in range(10):
        p = Process(target=add_num, args=(lock,))
        p.start()
        process_list.append(p)
    
    # 等待所有子进程执行完毕
    for p in process_list:
        p.join()
    
    print(f"最终num值: {num}")  # 加锁=10,不加锁≈1

# 执行过程:
# 1. 主进程创建锁,初始化num=0
# 2. 子进程竞争锁,获取锁的进程修改num(temp=num→sleep→num+1)
# 3. 未获取锁的进程阻塞,直到前一个进程解锁
# 4. 最终num=10,保证数据正确(不加锁时多个进程同时读temp=0,最终num=1)
3.2 事件(Event):控制进程阻塞
import os
import time
from multiprocessing import Process, Event

def wait_event(e):
    """等待事件触发的子进程"""
    print(f"进程(PID:{os.getpid()}) - 等待事件触发...")
    # wait():is_set()=False时阻塞,=True时立即执行
    e.wait()
    print(f"进程(PID:{os.getpid()}) - 事件触发,继续执行")

if __name__ == "__main__":
    # 创建事件对象(初始is_set()=False)
    e = Event()
    
    # 创建子进程
    p = Process(target=wait_event, args=(e,))
    p.start()
    
    # 主进程模拟准备工作(3秒)
    time.sleep(3)
    print("主进程 - 触发事件")
    e.set()  # 将is_set()改为True,解除子进程阻塞
    
    p.join()

# 执行过程:
# 1. 子进程执行wait(),因is_set()=False阻塞
# 2. 主进程3秒后调用set(),子进程解除阻塞,打印"事件触发"
3.3 队列(Queue):进程间通信
import os
from multiprocessing import Process, Queue

def put_data(q):
    """子进程存数据"""
    for i in range(5):
        data = f"数据{i}"
        q.put(data)  # 存入队列
        print(f"进程(PID:{os.getpid()}) - 存入: {data}")

def get_data(q):
    """子进程取数据"""
    while True:
        try:
            data = q.get_nowait()  # 非阻塞取数据(取不到抛异常)
            print(f"进程(PID:{os.getpid()}) - 取出: {data}")
        except:
            break  # 队列空时退出

if __name__ == "__main__":
    # 创建队列对象
    q = Queue()
    
    # 存数据进程
    p1 = Process(target=put_data, args=(q,))
    # 取数据进程
    p2 = Process(target=get_data, args=(q,))
    
    p1.start()
    p2.start()
    
    p1.join()
    p2.join()

# 执行过程:
# 1. p1向队列存入5条数据(数据0~4)
# 2. p2循环取数据,直到队列空
# 3. 进程间通过队列传输数据,避免数据隔离问题

应用场景及案例

场景1:多进程并发处理批量任务

适用场景:批量发送邮件、批量处理文件、接口批量测试(提高处理效率)。

import os
import time
from multiprocessing import Process

def process_task(task_id):
    """模拟单个任务处理"""
    print(f"开始处理任务{task_id} - 子进程PID:{os.getpid()}")
    time.sleep(1)  # 模拟任务耗时(如处理文件/调用接口)
    print(f"完成处理任务{task_id} - 子进程PID:{os.getpid()}")

if __name__ == "__main__":
    start_time = time.time()  # 记录开始时间
    task_num = 10  # 批量任务数
    process_list = []
    
    # 创建多进程处理任务
    for i in range(task_num):
        p = Process(target=process_task, args=(i,))
        p.start()
        process_list.append(p)
    
    # 等待所有任务完成
    for p in process_list:
        p.join()
    
    # 计算总耗时
    total_time = time.time() - start_time
    print(f"所有任务完成,总耗时: {total_time:.2f}秒")  # 并发≈1秒,串行≈10秒

# 代码注释:
# 1. process_task:单个任务逻辑,耗时1秒
# 2. 多进程并发处理10个任务,总耗时≈1秒(串行需10秒)
# 3. 核心价值:利用多核CPU,大幅缩短批量任务耗时

场景2:守护进程实现服务监控

适用场景:服务器心跳监控、日志实时检测(主进程退出则监控终止)。

import os
import time
from multiprocessing import Process

def server_monitor():
    """守护进程:发送服务器心跳包"""
    while True:
        # 模拟向监控中心发送状态
        print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 服务器运行正常 - PID:{os.getpid()}")
        time.sleep(1)

def business_service():
    """主业务:模拟服务器核心功能"""
    print(f"核心业务启动 - PID:{os.getpid()}")
    time.sleep(5)  # 模拟业务运行5秒
    print(f"核心业务结束 - PID:{os.getpid()}")

if __name__ == "__main__":
    # 创建守护进程(监控)
    monitor = Process(target=server_monitor)
    monitor.daemon = True
    monitor.start()
    
    # 创建核心业务进程
    service = Process(target=business_service)
    service.start()
    
    service.join()  # 等待核心业务完成
    print("主进程:服务器关闭,监控终止")

# 代码注释:
# 1. server_monitor:守护进程,每秒发送心跳包
# 2. business_service:核心业务,运行5秒后结束
# 3. 守护进程随主进程终止,避免僵尸进程占用资源
# 执行过程:
# 1. 监控进程每秒打印状态,业务进程运行5秒
# 2. 业务结束后,主进程终止,监控进程也随之终止

场景3:进程锁保证订单号唯一

适用场景:多进程创建订单、多进程更新数据库计数器(防止数据错乱)。

import os
import time
from multiprocessing import Process, Lock

order_count = 0  # 订单计数器(共享数据)

def create_order(lock, shop_name):
    """创建订单,加锁保证订单号唯一"""
    global order_count
    lock.acquire()  # 上锁
    try:
        time.sleep(0.2)  # 模拟订单创建耗时
        order_count += 1
        order_id = f"{shop_name}-{order_count}"
        print(f"进程(PID:{os.getpid()}) - 生成订单: {order_id}")
    finally:
        lock.release()  # 解锁

if __name__ == "__main__":
    lock = Lock()
    process_list = []
    
    # 5个进程同时创建订单
    for i in range(5):
        p = Process(target=create_order, args=(lock, "咖啡屋"))
        p.start()
        process_list.append(p)
    
    for p in process_list:
        p.join()
    
    print(f"最终订单总数: {order_count}")  # 输出5

# 代码注释:
# 1. order_count:全局计数器,生成唯一订单号
# 2. 加锁后,每个进程依次修改计数器,订单号为咖啡屋-1~5
# 3. 核心价值:避免多进程同时修改计数器,保证订单号唯一

场景4:生产者消费者模型(Queue)

适用场景:爬虫(爬取数据+处理数据)、消息队列(生产+消费)、日志分析(写日志+分析日志)。

import os
import time
import random
from multiprocessing import Process, Queue

def producer(q, prod_id):
    """生产者:生成数据(模拟爬取网页)"""
    for i in range(3):
        time.sleep(random.random())  # 随机耗时
        data = f"商品-{prod_id}-{i}"
        q.put(data)
        print(f"生产者(PID:{os.getpid()}) - 生产: {data}")

def consumer(q, cons_id):
    """消费者:处理数据(模拟订单处理)"""
    while True:
        try:
            data = q.get_nowait()  # 非阻塞取数据
            time.sleep(random.random())
            print(f"消费者(PID:{os.getpid()}) - 消费: {data}")
        except:
            break  # 队列空则退出

if __name__ == "__main__":
    q = Queue()  # 队列作为缓冲区
    process_list = []
    
    # 2个生产者
    for i in range(2):
        p = Process(target=producer, args=(q, i))
        p.start()
        process_list.append(p)
    
    # 等待生产者生产完毕
    for p in process_list:
        p.join()
    process_list.clear()
    
    # 3个消费者
    for i in range(3):
        c = Process(target=consumer, args=(q, i))
        c.start()
        process_list.append(c)
    
    for c in process_list:
        c.join()
    
    print("所有商品生产并消费完毕")

# 代码注释:
# 1. 生产者:2个进程各生产3个商品(共6个),存入队列
# 2. 消费者:3个进程并发消费,队列空则退出
# 3. 核心价值:解耦生产和消费,应对生产/消费速度不匹配问题


31、互斥锁_信号量_事件_队列

全文总结(表格版)

知识点模块 核心作用 核心方法 关键特点
Lock 互斥锁 多进程修改共享数据时,保证同一时间仅1个进程操作,防止数据错乱 acquire() 上锁
release() 解锁
独占锁,必须成对使用;只上锁不解锁会死锁
Semaphore 信号量 控制同时访问资源的进程数量(多把锁) acquire() 上锁
release() 解锁
允许多个进程并发操作,如 KTV 包房限制
Event 事件 通过状态标记控制进程阻塞/放行 wait() 阻塞
set() 放行
clear() 重置
模拟红绿灯、任务触发、流程同步
Queue 队列 实现进程间通信(解决数据隔离),先进先出 put() 存数据
get() 取数据
进程安全;空取/满存会阻塞
JoinableQueue 增强版队列,可监控数据是否消费完成 task_done() 标记消费
join() 阻塞等待
配合守护进程,确保所有数据处理完
生产者消费者模型 解耦「生产任务」与「消费任务」,用队列缓冲速度差 生产者put
消费者get
爬虫、日志、消息队列标准模型

完整详细讲解(逐行注释+执行过程)

1. Lock 互斥锁(解决共享数据错乱)

# 导入模块:Process创建进程,Lock互斥锁
from multiprocessing import Process, Lock
import time, json

# 读写文件函数(共享资源:票数)
def wr_info(sign, dic=None):
    # sign=r 读文件;sign=w 写文件
    if sign == "r":
        with open("ticket.txt", mode="r", encoding="utf-8") as fp:
            return json.load(fp)
    elif sign == "w":
        with open("ticket.txt", mode="w", encoding="utf-8") as fp:
            json.dump(dic, fp)

# 抢票逻辑(核心:修改共享数据)
def get_ticket(person):
    # 读取剩余票数
    dic = wr_info("r")
    time.sleep(0.1)  # 模拟网络延迟(放大数据错乱问题)
    if dic["count"] > 0:
        print(f"{person} 抢票成功")
        dic["count"] -= 1  # 票数减1
        wr_info("w", dic)  # 写回文件
    else:
        print(f"{person} 抢票失败")

# 抢票入口(加锁保证安全)
def ticket(person, lock):
    # 先查询票数(异步,不加锁)
    dic = wr_info("r")
    print(f"{person} 查询剩余票数:{dic['count']}")
    
    # ===================== 上锁 =====================
    lock.acquire()  # 上锁:同一时间仅1个进程执行抢票
    get_ticket(person)
    lock.release()  # 解锁:释放锁,其他进程可抢

if __name__ == "__main__":
    lock = Lock()  # 创建锁对象
    # 创建10个进程抢票
    for i in range(10):
        p = Process(target=ticket, args=(f"person{i}", lock))
        p.start()

执行过程

  1. 10个进程同时查询票数(异步,不加锁)
  2. 抢票时上锁,逐个进程修改票数
  3. 不加锁会出现「多人同时抢到同一张票」,加锁后串行修改,数据安全

2. Semaphore 信号量(控制并发数量)

# 导入模块:Process创建进程,Semaphore信号量
from multiprocessing import Process, Semaphore
import time, random

# KTV唱歌逻辑(限制同时唱歌人数)
def ktv(person, sem):
    # ===================== 上锁 =====================
    sem.acquire()  # 上锁:占用1个名额
    print(f"{person} 进入KTV包房唱歌")
    time.sleep(random.randrange(3, 5))  # 模拟唱歌时间
    print(f"{person} 离开KTV包房")
    sem.release()  # 解锁:释放名额

if __name__ == "__main__":
    # 创建信号量:最多允许4个进程同时执行
    sem = Semaphore(4)
    # 创建10个进程(模拟10个人排队唱歌)
    for i in range(10):
        p = Process(target=ktv, args=(f"person{i}", sem))
        p.start()

执行过程

  1. 信号量初始=4,同时只允许4个人进入
  2. 有人离开(解锁),排队的人才能进入
  3. 控制并发数量,避免资源过载

3. Event 事件(控制阻塞/放行,模拟红绿灯)

# 导入模块:Process创建进程,Event事件
from multiprocessing import Process, Event
import time, random

# 交通灯逻辑(控制事件状态)
def traffic_light(e):
    print("红灯亮")
    while True:
        if e.is_set():
            time.sleep(1)  # 绿灯亮1秒
            print("红灯亮")
            e.clear()  # 状态改为False → 阻塞
        else:
            time.sleep(1)  # 红灯亮1秒
            print("绿灯亮")
            e.set()  # 状态改为True → 放行

# 汽车通行逻辑(等待事件放行)
def car(e, i):
    if not e.is_set():
        print(f"car{i} 在等待红灯")
        e.wait()  # 状态False → 阻塞;True → 放行
    print(f"car{i} 通行了")

if __name__ == "__main__":
    e = Event()  # 创建事件对象(默认False,阻塞)
    lst = []
    
    # 启动交通灯(守护进程,主进程结束自动终止)
    p1 = Process(target=traffic_light, args=(e,))
    p1.daemon = True
    p1.start()
    
    # 创建20辆汽车(随机时间出发)
    for i in range(20):
        time.sleep(random.randrange(0, 2))
        p2 = Process(target=car, args=(e, i))
        p2.start()
        lst.append(p2)
    
    # 等待所有汽车通行
    for i in lst:
        i.join()
    print("程序全部结束")

执行过程

  1. 事件初始False → 红灯,汽车wait()阻塞
  2. 交通灯切换set() → 绿灯,汽车放行
  3. 交通灯切换clear() → 红灯,汽车再次阻塞

4. Queue 队列(进程间通信)

# 导入模块:Process创建进程,Queue队列
from multiprocessing import Process, Queue

# 子进程:取数据+存数据
def func(q):
    # 取数据(主进程存的)
    res = q.get()
    print(f"子进程取到数据:{res}")
    # 存数据(给主进程)
    q.put("曾文你想去7期me?")

if __name__ == "__main__":
    q = Queue()  # 创建队列对象
    p = Process(target=func, args=(q,))
    p.start()
    
    # 主进程存数据
    q.put("李毅真帅~")
    p.join()  # 等待子进程执行完
    
    # 主进程取数据(子进程存的)
    print(f"主进程取到数据:{q.get()}")

执行过程

  1. 主进程put数据 → 队列有数据
  2. 子进程get数据 → 打印
  3. 子进程put新数据 → 主进程get打印
  4. 队列实现进程间数据传递,解决数据隔离

5. 生产者消费者模型(解耦生产/消费)

# 导入模块
from multiprocessing import Process, Queue
import time, random

# 消费者:取数据并处理
def consumer(q, name):
    while True:
        food = q.get()
        if food is None:  # 收到None终止
            break
        time.sleep(random.uniform(0.1, 1))
        print(f"{name} 吃了一个 {food}")

# 生产者:生产数据并存入队列
def producer(q, name, food):
    for i in range(5):
        time.sleep(random.uniform(0.1, 1))
        print(f"{name} 生产了 {food}{i}")
        q.put(food + str(i))

if __name__ == "__main__":
    q = Queue()  # 缓冲队列
    
    # 创建2个消费者
    c1 = Process(target=consumer, args=(q, "张三"))
    c2 = Process(target=consumer, args=(q, "曾文"))
    c1.start()
    c2.start()
    
    # 创建2个生产者
    p1 = Process(target=producer, args=(q, "李四", "苹果"))
    p2 = Process(target=producer, args=(q, "李毅", "榴莲"))
    p1.start()
    p2.start()
    
    # 等待生产者生产完
    p1.join()
    p2.join()
    
    # 给消费者发送终止信号(2个消费者发2个None)
    q.put(None)
    q.put(None)

执行过程

  1. 生产者循环生产数据 → 存入队列
  2. 消费者循环取数据 → 处理数据
  3. 生产完后发None → 消费者终止
  4. 解耦生产与消费,缓冲速度差异

6. JoinableQueue(增强队列,监控消费完成)

# 导入模块
from multiprocessing import Process, JoinableQueue
import time, random

# 消费者
def consumer(q, name):
    while True:
        food = q.get()
        time.sleep(random.uniform(0.1, 1))
        print(f"{name} 吃了一个 {food}")
        q.task_done()  # 标记:当前数据消费完成

# 生产者
def producer(q, name, food):
    for i in range(5):
        time.sleep(random.uniform(0.1, 1))
        print(f"{name} 生产了 {food}{i}")
        q.put(food + str(i))

if __name__ == "__main__":
    jq = JoinableQueue()  # 创建增强队列
    
    # 消费者(守护进程)
    c1 = Process(target=consumer, args=(jq, "张三"))
    c2 = Process(target=consumer, args=(jq, "曾文"))
    c1.daemon = True
    c2.daemon = True
    c1.start()
    c2.start()
    
    # 生产者
    p1 = Process(target=producer, args=(jq, "李四", "苹果"))
    p2 = Process(target=producer, args=(jq, "李毅", "榴莲"))
    p1.start()
    p2.start()
    
    # 等待生产者生产完
    p1.join()
    p2.join()
    
    jq.join()  # 阻塞:等待所有数据task_done完成
    print("程序彻底结束")

执行过程

  1. 生产者put数据 → 队列计数+1
  2. 消费者get+task_done → 队列计数-1
  3. jq.join() 阻塞 → 计数=0放行
  4. 守护进程随主进程终止,确保所有数据消费完

应用场景+案例(带详细注释)

场景1:抢票系统(Lock 互斥锁)

适用:多进程修改共享文件/数据库,防止超卖、数据错乱

# 完整抢票案例(复制可用)
from multiprocessing import Process, Lock
import time, json

# 初始化ticket.txt:{"count": 5}
def wr_info(sign, dic=None):
    if sign == "r":
        with open("ticket.txt", "r", encoding="utf-8") as f:
            return json.load(f)
    elif sign == "w":
        with open("ticket.txt", "w", encoding="utf-8") as f:
            json.dump(dic, f)

def get_ticket(person):
    dic = wr_info("r")
    time.sleep(0.1)
    if dic["count"] > 0:
        print(f"{person} 抢票成功")
        dic["count"] -= 1
        wr_info("w", dic)
    else:
        print(f"{person} 抢票失败")

def ticket(person, lock):
    dic = wr_info("r")
    print(f"{person} 查票:{dic['count']}")
    lock.acquire()
    get_ticket(person)
    lock.release()

if __name__ == "__main__":
    lock = Lock()
    for i in range(10):
        p = Process(target=ticket, args=(f"用户{i}", lock))
        p.start()

场景2:机房服务器限制(Semaphore 信号量)

适用:控制同时访问的进程数,如数据库连接、接口请求限制

from multiprocessing import Process, Semaphore
import time

def visit_server(user, sem):
    sem.acquire()
    print(f"{user} 连接服务器成功")
    time.sleep(2)
    print(f"{user} 断开服务器连接")
    sem.release()

if __name__ == "__main__":
    sem = Semaphore(3)  # 最多3个并发连接
    for i in range(10):
        p = Process(target=visit_server, args=(f"用户{i}", sem))
        p.start()

场景3:红绿灯通行系统(Event 事件)

适用:流程控制、任务触发、状态同步

# 简化版红绿灯(复制可用)
from multiprocessing import Process, Event
import time

def light(e):
    while True:
        print("🔴 红灯")
        time.sleep(3)
        e.set()
        print("🟢 绿灯")
        time.sleep(3)
        e.clear()

def car(e, idx):
    if not e.is_set():
        print(f"🚗 汽车{idx} 等待红灯")
        e.wait()
    print(f"🚗 汽车{idx} 通过路口")

if __name__ == "__main__":
    e = Event()
    Process(target=light, args=(e,), daemon=True).start()
    for i in range(5):
        time.sleep(1)
        Process(target=car, args=(e, i)).start()

场景4:爬虫数据处理(生产者消费者模型)

适用:爬虫爬取数据→队列→解析数据,解耦提速

from multiprocessing import Process, Queue
import time

# 生产者:爬取url
def spider(q, url):
    print(f"爬取:{url}")
    time.sleep(1)
    q.put(f"数据_{url}")

# 消费者:解析数据
def parse(q):
    while True:
        data = q.get()
        if data is None:
            break
        time.sleep(1)
        print(f"解析:{data}")

if __name__ == "__main__":
    q = Queue()
    url_list = ["www.baidu.com", "www.taobao.com", "www.jd.com"]
    
    # 生产者进程
    for url in url_list:
        Process(target=spider, args=(q, url)).start()
    
    # 消费者进程
    p = Process(target=parse, args=(q,))
    p.start()
    
    time.sleep(5)
    q.put(None)


32、进程_线程_进程池

一、全文总结

知识点分类 核心概念 关键操作/特点 注意事项
进程基础 资源分配最小单位,独立内存空间;并发(单CPU)/并行(多CPU);三状态(就绪/执行/阻塞) 进程号唯一标识;同步(单主线)/异步(多主线);阻塞(等待输入)/非阻塞(无等待) 多进程数据隔离,需IPC通信;过多进程会降低CPU效率
线程基础 调度最小单位,共享进程资源;GIL锁导致Python线程无法并行(仅并发) 线程创建/启动;线程ID/状态查询;共享全局变量(需加锁) 计算密集型用多进程,IO密集型用多线程
同步互斥机制 保证多任务数据安全/有序执行 Lock(互斥锁,单锁)、Semaphore(信号量,多锁)、RLock(递归锁,解死锁)、Event(事件阻塞) 锁需成对解锁,否则死锁;递归锁仅应急使用
进程/线程通信 实现跨进程/线程数据交互 Manager(进程共享字典/列表)、Pipe(管道)、Queue(队列) 通信需加锁保证数据安全;Queue的get/put为阻塞操作
进程池 限制进程数量,提升CPU并行效率 apply(同步阻塞)、apply_async(异步非阻塞)、map(并行映射) close后不可加新任务;join等待所有子进程完成
守护进程/线程 随主进程/线程结束而终止 守护进程:主进程代码结束即终止;守护线程:所有非守护线程结束即终止 守护进程内不可开子进程;设置守护需在start前
核心应用逻辑 生产者消费者模型(解耦生产/消费速度) 队列缓存数据,生产者存、消费者取 平衡生产/消费速度,避免队列满/空导致阻塞

二、完整详细内容

步骤1:先搞懂「进程」- 操作系统分配资源的最小单位

1.1 进程核心概念
  • 进程 = 正在运行的程序,每个进程有唯一「进程号」,同一程序运行2次=2个独立进程(数据互相隔离)。
  • 并发 vs 并行:
    • 并发:1个CPU快速切换执行多个进程(比如1个人做4件事,轮流做);
    • 并行:多个CPU同时执行多个进程(比如4个人各做1件事,同时做)。
  • 进程三状态:
    • 就绪:除了CPU,所有资源都齐了,等CPU调度;
    • 执行:CPU正在跑这个进程;
    • 阻塞:等某个事件(比如输入、IO),CPU暂时切走。
1.2 进程同步/异步 + 阻塞/非阻塞(新人类比)
类型 类比说明 效率
同步阻塞 你等奶茶做好(阻塞),拿到后才去买面包(同步) 最低
异步阻塞 你下单奶茶(异步),但要等奶茶做好才能走(阻塞)(比如socket多连接但等recv) 中等
同步非阻塞 你买面包不用等(非阻塞),买完再去买奶茶(同步)(普通顺序代码) 中高
异步非阻塞 你下单奶茶(异步),同时去买面包(非阻塞)(CPU利用率最高,注意过热) 最高
1.3 进程核心操作:创建/通信/池(代码+逐行注释)
案例1:进程间共享数据(Manager) 新人必懂的「数据安全」
# 导入进程、Manager(进程间共享数据)、Lock(互斥锁)模块
from multiprocessing import Process, Manager, Lock
import time

# 定义子进程执行的函数:修改共享字典的count值
def work(dic, lock):
    """
    :param dic: Manager创建的共享字典(进程间可共享)
    :param lock: 互斥锁(保证同一时间只有1个进程修改数据)
    """
    # with语法自动上锁+解锁(替代acquire/release,更简洁)
    with lock:
        # 共享字典的count值减1
        dic['count'] -= 1
        # 打印当前进程的修改(方便新人看执行过程)
        print(f"当前进程修改后count: {dic['count']}")

if __name__ == "__main__":
    # 1. 创建互斥锁(解决多进程修改数据的竞争问题)
    lock = Lock()
    # 2. 存储所有子进程对象的列表(用于后续等待子进程完成)
    lst = []
    # 3. 创建Manager对象(用于生成进程间共享的字典/列表)
    m = Manager()
    # 4. 生成共享字典,初始count=500
    dic = m.dict({"count": 500})
    
    # 5. 创建500个子进程,每个进程执行work函数
    for i in range(500):
        # 创建子进程,目标函数work,参数是共享字典+锁
        p = Process(target=work, args=(dic, lock))
        # 启动子进程(异步执行)
        p.start()
        # 将子进程对象加入列表,方便后续join
        lst.append(p)
    
    # 6. 等待所有子进程执行完毕(不加join会导致主进程先执行,打印结果异常)
    for i in lst:
        i.join()
    
    # 7. 打印最终的共享字典(新人验证:500次减1后count=0)
    print(f"最终结果: {dic}")
运算过程:
  1. 主进程创建共享字典dic={"count":500},并创建500个子进程;
  2. 每个子进程启动后,通过with lock抢占锁,抢到锁的进程修改count(减1),没抢到的等待;
  3. 由于锁的存在,同一时间只有1个进程修改count,避免「多进程同时修改导致数据错乱」;
  4. 500个子进程全部执行完后,count从500减到0,最终打印{'count': 0}
案例2:进程池(「并行效率」)
# 导入进程池、进程、系统、随机数、时间模块
from multiprocessing import Pool, Process
import os, random, time

# 定义子任务函数:打印任务编号+进程ID,返回平方值
def task(num):
    """
    :param num: 任务编号
    :return: num的平方(用于验证返回值)
    """
    # 随机休眠0.1-1秒(模拟真实任务耗时)
    time.sleep(random.uniform(0.1, 1))
    # 打印当前任务编号+进程ID(新人看:进程池会复用进程)
    print(f"任务{num} | 进程ID: {os.getpid()}")
    # 返回任务编号的平方(验证进程池返回值)
    return num ** 2

if __name__ == "__main__":
    # 1. 记录程序开始时间(新人看耗时)
    start_time = time.time()
    
    # 2. 创建进程池(默认大小=CPU逻辑核心数,比如6核则同时跑6个任务)
    p = Pool()
    # 存储进程池任务返回值的列表
    res_list = []
    
    # 3. 向进程池提交20个任务(异步非阻塞)
    for i in range(20):
        # apply_async:异步提交任务,不阻塞主进程
        res = p.apply_async(task, args=(i,))
        # 将返回值对象加入列表(后续获取结果)
        res_list.append(res)
    
    # 4. 获取所有任务的返回值(get()会阻塞,直到对应任务完成)
    result = [i.get() for i in res_list]
    
    # 5. 记录程序结束时间,计算耗时
    end_time = time.time()
    cost_time = end_time - start_time
    
    # 6. 打印结果(新人验证:20个任务的平方值,耗时远低于手动创建20个进程)
    print(f"所有任务返回值: {result}")
    print(f"进程池执行耗时: {cost_time:.2f}秒")
    print("程序结束")
运算过程:
  1. 进程池创建后,默认按CPU核心数(比如6核)启动固定数量的进程;
  2. 20个任务提交后,进程池会把任务分配给这6个进程,一个进程完成任务后,再接收新任务(进程复用);
  3. 异步提交任务(apply_async),主进程不等待,直到调用get()才阻塞获取结果;
  4. 对比「手动创建20个进程」:进程池避免了「进程创建/销毁的开销」,耗时更短(比如6核CPU执行20个任务,耗时≈1秒,手动创建则需5+秒)。

步骤2:再搞懂「线程」- 操作系统调度的最小单位

2.1 线程核心概念(新人易懂版)
  • 线程 = 轻量级进程,共享所属进程的内存资源(比如全局变量);
  • 1个进程至少有1个线程(主线程),多线程共享数据但需加锁保证安全;
  • GIL锁(Python特有):同一时间,1个进程下的多线程只能被1个CPU执行(无法并行,仅并发);
    • 解决:计算密集型用多进程,IO密集型(爬虫/网页)用多线程。
2.2 线程核心操作:创建/锁/信号量(代码+逐行注释)
案例1:线程锁(解决数据安全问题)
# 导入线程、互斥锁模块
from threading import Thread, Lock
# 定义全局变量n(多线程共享)
n = 0

# 定义线程函数1:对n加1(100万次)
def func1(lock):
    global n  # 声明修改全局变量
    for i in range(1000000):
        # 上锁:同一时间只有1个线程能执行后续代码
        lock.acquire()
        n += 1  # 对n加1
        # 解锁:释放锁,让其他线程可以抢占
        lock.release()

# 定义线程函数2:对n减1(100万次)
def func2(lock):
    global n  # 声明修改全局变量
    for i in range(1000000):
        # with语法自动上锁/解锁(更简洁,新人推荐)
        with lock:
            n -= 1  # 对n减1

if __name__ == "__main__":
    # 1. 创建互斥锁(解决多线程修改n的竞争问题)
    lock = Lock()
    # 存储线程对象的列表
    lst = []
    
    # 2. 创建10组线程(每组1个func1+1个func2)
    for i in range(10):
        t1 = Thread(target=func1, args=(lock,))  # 加1线程
        t2 = Thread(target=func2, args=(lock,))  # 减1线程
        t1.start()  # 启动线程
        t2.start()
        lst.append(t1)  # 加入列表
        lst.append(t2)
    
    # 3. 等待所有线程执行完毕
    for i in lst:
        i.join()
    
    # 4. 打印最终n的值(新人验证:加1000万次,减1000万次,n=0)
    print("主线程执行结束...")
    print(f"最终n的值: {n}")
运算过程:
  1. 全局变量n初始=0,创建10个加线程(每个加100万次)+10个减线程(每个减100万次);
  2. 不加锁时:多线程同时修改n,会导致「指令交错」(比如n=0时,加线程读n=0,减线程也读n=0,加完=1,减完= -1,最终n≠0);
  3. 加锁后:同一时间只有1个线程能修改n,加1和减1操作原子化,最终n=0。
案例2:信号量(线程限流,「多锁并发」)
# 导入线程、信号量模块
from threading import Thread, Semaphore
import time, random

# 定义线程函数:模拟限流任务
def func(i, sem):
    """
    :param i: 任务编号
    :param sem: 信号量对象(限制同时执行的线程数)
    """
    # with语法自动获取/释放信号量
    with sem:
        # 休眠3秒(模拟任务耗时,比如接口请求)
        time.sleep(3)
        # 打印任务编号(新人看:同时只有5个线程执行)
        print(f"执行任务{i}")

if __name__ == "__main__":
    # 1. 创建信号量,允许同时5个线程获取锁(限流5个)
    sem = Semaphore(5)
    
    # 2. 创建20个线程
    for i in range(20):
        t = Thread(target=func, args=(i, sem))
        t.start()
    
    # 3. 主线程打印(新人看:主线程先结束,子线程按限流执行)
    print("主线程结束")
运算过程:
  1. 信号量初始值=5,相当于有5把锁;
  2. 20个线程启动后,前5个线程抢到锁,执行任务(休眠3秒);
  3. 1个线程执行完释放锁,第6个线程抢到锁,以此类推;
  4. 最终20个线程分4批执行(每批5个),总耗时≈12秒(3秒×4批),实现「限流」。
2.3 递归锁(解决死锁问题)
# 导入线程、递归锁模块
from threading import Thread, RLock
import time

# 定义递归锁(替代2个互斥锁,解决死锁)
noodle_lock = kuaizi_lock = RLock()

# 定义吃面条函数1:先拿面再拿筷子
def eat1(name):
    # 递归锁上锁(第1次)
    noodle_lock.acquire()
    print(f"{name}拿到面条(上锁)")
    # 递归锁再次上锁(第2次,递归锁允许同一线程多次上锁)
    kuaizi_lock.acquire()
    print(f"{name}拿到筷子(上锁)")

    print(f"{name}开始吃面条")
    time.sleep(0.9)  # 模拟吃面耗时

    # 递归锁解锁(第2次)
    kuaizi_lock.release()
    print(f"{name}放下筷子(解锁)")
    # 递归锁解锁(第1次)
    noodle_lock.release()
    print(f"{name}放下面条(解锁)")

# 定义吃面条函数2:先拿筷子再拿面
def eat2(name):
    kuaizi_lock.acquire()
    print(f"{name}拿到筷子(上锁)")
    noodle_lock.acquire()
    print(f"{name}拿到面条(上锁)")

    print(f"{name}开始吃面条")
    time.sleep(0.9)

    noodle_lock.release()
    print(f"{name}放下面条(解锁)")
    kuaizi_lock.release()
    print(f"{name}放下筷子(解锁)")

if __name__ == "__main__":
    # 创建4个线程(2个先拿面,2个先拿筷子)
    namelist1 = ["曾文","李毅"]  # 先拿面
    namelist2 = ["学斌","李杰"]  # 先拿筷子
    for name in namelist1:
        Thread(target=eat1, args=(name,)).start()
    for name in namelist2:
        Thread(target=eat2, args=(name,)).start()
运算过程:
  1. 死锁原因:2个互斥锁嵌套,线程A拿面锁等筷子锁,线程B拿筷子锁等面锁,互相等待;
  2. 递归锁解决:同一线程可多次上锁,且解锁次数=上锁次数;
  3. 执行流程:比如「曾文」先拿面锁(递归锁计数=1),再拿筷子锁(计数=2),吃完后依次解锁(计数1→0),其他线程可抢占,无死锁。

步骤3:进阶知识点(守护进程/线程、Event事件)

3.1 守护线程(「后台任务」)
# 导入线程、时间模块
from threading import Thread
import time

# 定义守护线程函数:循环打印
def func1():
    while True:
        time.sleep(0.5)  # 每0.5秒打印1次
        print("我是守护线程,持续运行...")

# 定义普通线程函数:执行3秒
def func2():
    print("普通线程开始执行")
    time.sleep(3)  # 模拟任务耗时
    print("普通线程执行结束")

if __name__ == "__main__":
    # 1. 创建守护线程
    t1 = Thread(target=func1)
    # 设置为守护线程(必须在start前):随所有非守护线程结束而终止
    t1.setDaemon(True)
    t1.start()

    # 2. 创建普通线程
    t2 = Thread(target=func2)
    t2.start()

    # 3. 主线程休眠5秒
    time.sleep(5)
    print("主线程结束...")
运算过程:
  1. 守护线程t1启动后,循环打印;普通线程t2执行3秒后结束;
  2. 主线程休眠5秒后结束,此时所有非守护线程(t2+主线程)都结束,守护线程t1自动终止;
  3. 新人验证:守护线程不会一直运行,主程序结束即停止。

三、应用场景及案例

场景1:进程池批量处理IO密集型任务(比如批量下载文件)

场景说明
  • 适用:需要批量执行大量耗时短的IO任务(爬虫、文件下载),利用CPU并行提升效率;
  • 核心:进程池复用进程,避免频繁创建/销毁进程的开销。
案例代码(逐行注释)
from multiprocessing import Pool
import requests  # 需安装:pip install requests
import time

# 定义下载函数:下载指定URL的内容(模拟IO任务)
def download_file(url):
    """
    :param url: 要下载的URL
    :return: URL+下载状态
    """
    try:
        # 发送GET请求(超时5秒)
        response = requests.get(url, timeout=5)
        # 模拟保存文件(写入本地,文件名=URL最后部分)
        filename = url.split("/")[-1] + ".html"
        with open(filename, "w", encoding="utf-8") as f:
            f.write(response.text)
        return f"{url} → 下载成功(文件:{filename})"
    except Exception as e:
        return f"{url} → 下载失败:{str(e)}"

if __name__ == "__main__":
    # 1. 待下载的URL列表(新人可替换为自己的URL)
    url_list = [
        "https://www.baidu.com",
        "https://www.taobao.com",
        "https://www.jd.com",
        "https://www.163.com",
        "https://www.sina.com.cn",
        "https://www.tmall.com",
        "https://www.meituan.com",
        "https://www.douban.com"
    ]
    
    # 2. 记录开始时间
    start_time = time.time()
    
    # 3. 创建进程池(CPU核心数=8,根据自己电脑调整)
    pool = Pool(4)
    
    # 4. 批量提交任务(map自动分配任务,返回结果列表)
    results = pool.map(download_file, url_list)
    
    # 5. 关闭进程池(不再接受新任务)
    pool.close()
    # 等待所有任务完成
    pool.join()
    
    # 6. 计算耗时,打印结果
    end_time = time.time()
    cost_time = end_time - start_time
    
    print(f"===== 下载结果 =====")
    for res in results:
        print(res)
    print(f"总耗时:{cost_time:.2f}秒")
运算过程
  1. 进程池创建4个进程,将8个URL分配给这4个进程;
  2. 每个进程下载2个URL(并行执行),相比单进程串行下载,耗时减少约50%;
  3. 下载完成后,返回每个URL的下载状态,主进程打印结果。

场景2:线程锁保证多线程数据安全(比如电商库存扣减)

场景说明
  • 适用:多线程同时修改共享数据(库存、订单数),需保证数据一致性;
  • 核心:互斥锁让「扣减库存」操作原子化,避免超卖。
案例代码(逐行注释)
from threading import Thread, Lock
import time

# 模拟电商库存
inventory = {
    "phone": 100  # 手机库存100台
}
# 创建互斥锁
lock = Lock()

# 定义扣减库存函数
def reduce_inventory(user, product, num):
    """
    :param user: 用户名
    :param product: 商品名
    :param num: 扣减数量
    """
    global inventory
    # 上锁:保证同一时间只有1个线程修改库存
    with lock:
        # 检查库存是否充足
        if inventory[product] >= num:
            # 模拟订单处理耗时
            time.sleep(0.1)
            # 扣减库存
            inventory[product] -= num
            print(f"用户{user}:成功购买{num}台{product},剩余库存:{inventory[product]}")
        else:
            print(f"用户{user}:库存不足!{product}剩余{inventory[product]}台,无法购买{num}台")

if __name__ == "__main__":
    # 模拟200个用户同时抢购(每个用户买1台手机)
    thread_list = []
    for i in range(200):
        t = Thread(target=reduce_inventory, args=(f"user{i}", "phone", 1))
        thread_list.append(t)
        t.start()
    
    # 等待所有线程完成
    for t in thread_list:
        t.join()
    
    # 打印最终库存(新人验证:100台库存,最终剩余0,无超卖)
    print(f"最终库存:{inventory}")
运算过程
  1. 初始库存100台,200个线程模拟抢购(每个买1台);
  2. 加锁后,同一时间只有1个线程检查并扣减库存,前100个用户抢购成功,后100个提示库存不足;
  3. 不加锁时:多个线程同时检查库存(比如库存=1时,2个线程都认为库存充足,扣减后库存=-1,超卖);加锁后无超卖,数据安全。

场景3:信号量实现接口限流(比如接口请求频率限制)

场景说明
  • 适用:限制同时访问接口的线程数(比如接口最多允许10个并发请求);
  • 核心:信号量设置并发数,超过则等待,避免接口过载。
案例代码(逐行注释)
from threading import Thread, Semaphore
import time
import random

# 创建信号量,限制最多5个并发请求
sem = Semaphore(5)

# 定义接口请求函数
def api_request(user_id):
    """
    :param user_id: 用户ID
    """
    # 获取信号量(限流)
    with sem:
        # 模拟接口处理耗时(0.5-2秒)
        process_time = random.uniform(0.5, 2)
        time.sleep(process_time)
        print(f"用户{user_id}:接口请求成功,耗时{process_time:.2f}秒 | 当前并发数:{5 - sem._value}")

if __name__ == "__main__":
    # 模拟30个用户同时请求接口
    for i in range(30):
        t = Thread(target=api_request, args=(i,))
        t.start()
    
    print("所有请求已提交,主线程结束")
运算过程
  1. 信号量初始值=5,相当于接口最多5个并发;
  2. 30个线程启动后,前5个线程进入接口处理,剩余25个等待;
  3. 1个线程处理完释放信号量,下1个线程立即进入,始终保持5个并发;
  4. 新人验证:接口不会因并发过高崩溃,实现限流保护。

四、新人学习小贴士

  1. 先理解「进程≠线程」:进程是资源分配单位(独立内存),线程是调度单位(共享内存);
  2. 锁的核心目的:保证数据安全,而非提升效率,牺牲速度换安全;
  3. 练手顺序:先写单进程/线程 → 加锁保证安全 → 用进程池/信号量优化效率 → 结合业务场景落地;
  4. 调试技巧:打印进程/线程ID、执行时间、变量值,直观看到执行过程。


33、多线程_协程

全文总结

模块分类 核心概念 关键API/函数 核心作用
线程数据隔离 threading.local对象实现线程间数据独立 local()、current_thread()、Thread() 多线程中每个线程拥有独立的变量副本,避免数据混乱
线程Event事件 线程间通信的“信号开关”,控制线程阻塞/放行 Event()、wait()、set()、clear()、is_set() 实现线程间条件等待(如数据库连接检测、任务触发)
线程Condition条件 精细化控制线程阻塞/放行,需配合锁使用 Condition()、acquire()、release()、wait()、notify()、notifyAll() 按需释放指定数量的阻塞线程(如任务分发、批量唤醒线程)
线程Timer定时器 延迟执行指定任务 Timer(延迟时间, 目标函数)、start() 延迟触发任务(实际生产常用crontab替代)
线程队列 线程安全的任务/数据存储结构,支持不同存取规则 Queue(先进先出)、LifoQueue(先进后出)、PriorityQueue(优先级排序)、put/get 多线程间数据传递、任务调度(如生产者消费者模型)
新版线程池/进程池 简化池管理,支持异步并发、返回值获取 ThreadPoolExecutor/ProcessPoolExecutor、submit()、map()、shutdown()、result() 限制并发数,高效管理多线程/多进程任务,避免频繁创建销毁资源
回调函数 任务执行完成后自动触发的函数 add_done_callback(回调函数) 任务结束后异步处理结果(如爬取数据后解析、任务执行后日志记录)
协程 单线程内的“微线程”,通过切换规避IO阻塞,提升并发效率 gevent.spawn()、join()、joinall()、monkey.patch_all()、greenlet.switch() 高IO密集型任务(爬虫、接口请求)的高效并发,比线程更轻量

二、完整详细讲解(逐模块+代码注释+运行过程)

2.1 线程之间数据隔离

核心逻辑

每个线程通过threading.local对象拥有独立的变量副本,修改不会互相影响,解决多线程数据共享混乱问题。

# ### 线程之间数据隔离
# 导入线程相关模块:Thread(创建线程)、local(线程数据隔离对象)、current_thread(获取当前线程)
from threading import Thread, local, current_thread

# 1. 创建local对象(核心:每个线程访问该对象时,都会拿到自己的专属副本)
loc = local()
print(loc)  # 输出:<_thread._local object at 0x7fxxxx>(内存地址)

# 2. 主线程给loc的val属性赋值
loc.val = "main_thread 下载进度98%"

# 定义子线程要调用的函数:打印当前loc.val和线程名
def func2():
    # current_thread().name 获取当前执行该函数的线程名称
    # 每个线程的loc.val是独立的,子线程不会拿到主线程的val
    print("%s 该数据归属于 %s" % (loc.val, current_thread().name))

# 定义子线程执行的核心函数:给当前线程的loc.val赋值,再调用func2
def func1(speed):
    # 给当前线程的loc.val赋值(仅当前线程可见)
    loc.val = speed
    # 调用func2,打印当前线程的val和名称
    func2()

# 3. 创建第一个子线程
# target:线程要执行的函数;args:传给函数的参数(元组形式)
t1 = Thread(target=func1, args=("下载进度13%",))
# 给线程命名(方便识别)
t1.setName("child_thread")
# 启动线程(此时线程开始执行func1)
t1.start()

# 4. 创建第二个子线程(简化写法:创建时直接命名)
t2 = Thread(target=func1, args=("下载进度22%",), name="child_thread2222")
t2.start()

# 5. 主线程等待子线程执行完毕(join:阻塞主线程,直到子线程执行完)
t1.join()
t2.join()

# 6. 主线程打印自己的loc.val(不受子线程修改影响)
print(loc.val)

# 【运行过程&结果】
# 1. 先打印loc对象:<_thread._local object at 0x7f8a1b3b7d30>
# 2. 子线程child_thread执行func1:给loc.val赋值"下载进度13%",调用func2打印 → 下载进度13% 该数据归属于 child_thread
# 3. 子线程child_thread2222执行func1:给loc.val赋值"下载进度22%",调用func2打印 → 下载进度22% 该数据归属于 child_thread2222
# 4. 主线程打印loc.val → main_thread 下载进度98%

2.2 线程的Event事件

核心逻辑

通过“信号开关”(True/False)控制线程阻塞:wait()阻塞直到开关为True,set()打开开关,clear()关闭开关,is_set()判断开关状态。

# ### 线程的Event事件
from threading import Event, Thread
import time, random  # time用于延迟,random用于随机等待时间

"""
Event核心API说明:
- wait(timeout=None):阻塞线程,直到Event内部状态为True;timeout为最大阻塞时间(秒)
- set():将Event内部状态设为True,释放所有等待的线程
- clear():将Event内部状态设为False(默认初始状态)
- is_set():返回Event内部状态(True/False)
"""

# 模拟:检测数据库连接合法性(子线程1)
def check(e):
    print("开始检测数据连接的合法性 ... ")
    # 随机休眠1-7秒(模拟检测耗时)
    time.sleep(random.randrange(1, 8))
    # 检测完成,打开Event开关(状态设为True)
    e.set()

# 模拟:尝试连接数据库(子线程2),最多尝试3次,每次等待1秒
def connect(e):
    # 标记是否连接成功
    sign = False
    # 循环3次尝试连接
    for i in range(3):
        # 阻塞最多1秒:如果1秒内Event开关打开,就放行;否则超时放行
        e.wait(1)
        # 判断开关是否打开
        if e.is_set():
            sign = True  # 标记连接成功
            print("数据库连接成功啦~")
            break  # 跳出循环,不再尝试
        else:
            # 第i+1次尝试失败(i从0开始)
            print("尝试连接数据库%s次失败了" % (i+1))
    
    # 如果3次都失败,主动抛出超时异常
    if sign == False:
        raise TimeoutError("数据库连接超时(3次尝试失败)")

# 1. 创建Event对象(初始状态False)
e = Event()

# 2. 创建并启动检测线程
Thread(target=check, args=(e,)).start()

# 3. 创建并启动连接线程
Thread(target=connect, args=(e,)).start()

# 【运行过程&结果(两种情况)】
# 情况1:check线程在3秒内完成检测(比如休眠2秒)
# - 第1次循环:e.wait(1) → 1秒后e仍为False → 打印"尝试连接数据库1次失败了"
# - 第2次循环:e.wait(1) → 此时check线程执行e.set() → e.is_set()为True → 打印"数据库连接成功啦~",跳出循环
# 情况2:check线程超过3秒完成检测(比如休眠5秒)
# - 第1次循环:失败 → 打印"尝试连接数据库1次失败了"
# - 第2次循环:失败 → 打印"尝试连接数据库2次失败了"
# - 第3次循环:失败 → 打印"尝试连接数据库3次失败了"
# - 抛出TimeoutError: 数据库连接超时(3次尝试失败)

2.3 线程的Condition条件

核心逻辑

基于锁的精细化线程阻塞/放行控制:wait()需先上锁,阻塞线程;notify(n)释放n个阻塞线程(需上锁),notifyAll()释放所有阻塞线程。

# ### 线程的Condition条件(精细化控制线程阻塞/放行)
from threading import Condition, Thread 
import time

"""
Condition核心规则:
- wait()/notify()/notifyAll() 必须在acquire()(上锁)和release()(解锁)之间调用
- wait():释放锁并阻塞线程,直到被notify()唤醒,唤醒后重新获取锁
- notify(n):唤醒n个阻塞的线程(默认n=1);n超过阻塞数则按实际数唤醒
- notifyAll():唤醒所有阻塞的线程
"""

# 定义子线程执行的函数:等待被唤醒
def func(con, index):
    print("%s在等待" % (index))
    # 1. 上锁(必须)
    con.acquire()
    # 2. 阻塞线程,等待被notify()唤醒
    con.wait()
    # 3. 被唤醒后执行:打印释放信息
    print("%s 释放了" % (index))
    # 4. 解锁(必须)
    con.release()

# 1. 创建Condition对象
con = Condition()

# 2. 创建10个子线程,每个线程执行func,等待被唤醒
for i in range(10):
    t = Thread(target=func, args=(con, i))
    t.start()

# 延迟1秒:确保所有线程都进入wait()阻塞状态
time.sleep(1)

# 3. 手动输入释放数量,循环释放所有阻塞线程
count = 10  # 总阻塞线程数
while count > 0:	
    # 输入要释放的线程数量
    num = int(input(">>>想要释放阻塞的数量:"))
    # 上锁(必须)
    con.acquire()
    # 释放num个阻塞线程
    con.notify(num)
    # 解锁(必须)
    con.release()
    # 剩余未释放线程数
    count -= num

# 【运行过程&结果】
# 1. 先打印10行:0在等待、1在等待...9在等待
# 2. 控制台提示输入释放数量,比如输入3 → 打印0 释放了、1 释放了、2 释放了;count变为7
# 3. 再输入5 → 打印3 释放了、4 释放了、5 释放了、6 释放了、7 释放了;count变为2
# 4. 输入2 → 打印8 释放了、9 释放了;count变为0,循环结束

2.4 线程Timer定时器

核心逻辑

延迟指定时间后执行单个任务,本质是特殊的线程。

# ### 线程的Timer定时器(延迟执行任务)
"""
Timer核心语法:
Timer(延迟时间(秒), 目标函数, args=(函数参数))
- start():启动定时器(线程)
- cancel():取消定时器(未执行前)
"""
from threading import Timer

# 定义要延迟执行的函数
def func():
    print("我正在上传电影... ")

# 1. 创建定时器:延迟2秒执行func
t = Timer(2, func)
print(t)  # 输出:<Timer(Thread-1, initial)>(定时器线程对象)

# 2. 启动定时器
t.start()

# 主线程先执行完毕
print("主线程执行完毕 ... ")

# 【运行过程&结果】
# 1. 先打印:<Timer(Thread-1, initial)>
# 2. 打印:主线程执行完毕 ... 
# 3. 延迟2秒后,打印:我正在上传电影... 
# 【注意】实际生产中,Linux的crontab(计划任务)更常用,替代Timer

2.5 线程队列

核心逻辑

线程安全的队列结构,支持3种存取规则:先进先出、先进后出、优先级排序,避免多线程数据竞争。

# ### 线程队列(线程安全的任务/数据存储)
"""
队列核心API:
- put(item):向队列放数据,队列满则阻塞
- get():从队列取数据,队列空则阻塞
- put_nowait(item):放数据,队列满则直接报错(不阻塞)
- get_nowait():取数据,队列空则直接报错(不阻塞)
"""

# ---------------------- (1) Queue:先进先出(FIFO) ----------------------
from queue import Queue
# 创建普通队列
q = Queue()
# 放入数据
q.put(1)
q.put(2)
# 取出数据(先进先出)
print(q.get())  # 输出:1
print(q.get_nowait())  # 输出:2(非阻塞取)

# 限制队列长度为2
q = Queue(2)
q.put(3)
q.put(4)
# q.put(5)        # 队列满,阻塞
# q.put_nowait(5) # 队列满,报错:queue.Full

# ---------------------- (2) LifoQueue:先进后出(LIFO,栈) ----------------------
from queue import LifoQueue
lq = LifoQueue()
lq.put(5)
lq.put(6)
print(lq.get())  # 输出:6(后进先出)

# ---------------------- (3) PriorityQueue:按优先级排序 ----------------------
from queue import PriorityQueue
pq = PriorityQueue()

# 规则1:元组形式 → 按第一个元素(数字)从小到大排序
pq.put( (12,"wangwen") )
pq.put( (5,"zhangsan") )
pq.put( (18,"lisi") )
pq.put( (3,"zhaoliu") )
print(pq.get())  # 输出:(3, 'zhaoliu')
print(pq.get())  # 输出:(5, 'zhangsan')
print(pq.get())  # 输出:(12, 'wangwen')
print(pq.get())  # 输出:(18, 'lisi')

# 规则2:元组形式 → 第一个元素是字符串,按ASCII编码从小到大排序
pq = PriorityQueue()
pq.put( ("wangwen",12) )
pq.put( ("zhangsan",5) )
pq.put( ("lisi",18) )
pq.put( ("zhaoliu",3) )
print(pq.get())  # 输出:('lisi', 18)(l的ASCII比w/z小)
print(pq.get())  # 输出:('wangwen', 12)
print(pq.get())  # 输出:('zhangsan', 5)
print(pq.get())  # 输出:('zhaoliu', 3)

# 规则3:单一元素 → 必须是同种类型(数字/字符串)
# 数字:从小到大
pq = PriorityQueue()
pq.put(3)
pq.put(5)
pq.put(0)
print(pq.get())  # 0
print(pq.get())  # 3
print(pq.get())  # 5

# 字符串:按ASCII编码排序
pq = PriorityQueue()
pq.put("oneal")
pq.put("james")
pq.put("davis")
pq.put("kobe")
print(pq.get())  # davis(d的ASCII最小)
print(pq.get())  # james
print(pq.get())  # kobe
print(pq.get())  # oneal

# 【运行过程&结果】
# 1. Queue部分:1 → 2
# 2. LifoQueue部分:6
# 3. PriorityQueue数字元组:(3, 'zhaoliu') → (5, 'zhangsan') → (12, 'wangwen') → (18, 'lisi')
# 4. PriorityQueue字符串元组:('lisi', 18) → ('wangwen', 12) → ('zhangsan', 5) → ('zhaoliu', 3)
# 5. 单一数字:0 → 3 → 5
# 6. 单一字符串:davis → james → kobe → oneal

2.6 新版线程池/进程池

核心逻辑

简化池管理,自动控制并发数,避免频繁创建/销毁线程/进程的开销;支持异步提交任务、获取返回值。

# ### 新版进程池/线程池(concurrent.futures)
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import os, time
from threading import current_thread as cthread  # 获取当前线程信息

# ---------------------- (1) 线程池基础使用 ----------------------
def func(i):
    # 打印线程编号、线程ID
    print("thread", i, cthread().ident)
    # 延迟0.1秒(模拟任务耗时,避免线程执行太快导致并发不明显)
    time.sleep(0.1)
    print("thread %s end" % (i))
    # 返回结果:i个星号
    return "*" * i

# 1. 创建线程池,最大并发数5
tp = ThreadPoolExecutor(5)

# 2. 异步提交20个任务(map方式:自动遍历range(20),每个元素传给func)
it = tp.map(func, range(20))

# 3. 关闭线程池(shutdown = close + join:拒绝新任务,等待所有任务完成)
tp.shutdown()

# 4. 遍历map返回的迭代器,获取每个任务的返回值
for res in it:
    print("任务返回值:", res)

# 验证返回值是迭代器
from collections import Iterator
print("it是否为迭代器:", isinstance(it, Iterator))

print("主线程执行结束...")

# ---------------------- (2) 进程池基础使用(需加if __name__ == "__main__") ----------------------
"""
def func(i):
    print("Process", i, os.getpid())  # 打印进程ID
    time.sleep(0.3)
    print("Process ... end")
    return 5488

if __name__ == "__main__":
    # 创建进程池
    p = ProcessPoolExecutor()
    # 异步提交任务
    obj = p.submit(func, 666)
    # 获取返回值
    res = obj.result()
    print("进程任务返回值:", res)
    # 关闭进程池
    p.shutdown()
    print("主进程执行完毕 ... ")
"""

# 【运行过程&结果(线程池)】
# 1. 线程池最多同时运行5个线程,依次处理20个任务:
#    - 先打印thread 0 12345 → thread 1 12346 → ... → thread 4 12349
#    - 0.1秒后,打印thread 0 end → thread 1 end → ... → thread 4 end
#    - 接着启动thread 5-9,重复上述过程,直到20个任务完成
# 2. 遍历迭代器,打印每个任务的返回值:"" → "*" → "**" → ... → "*******************"(19个星)
# 3. 打印:it是否为迭代器:True
# 4. 打印:主线程执行结束...

2.7 回调函数

核心逻辑

任务执行完成后自动触发的函数,线程池的回调由子线程执行,进程池的回调由主进程执行。

# ### 回调函数(任务完成后自动执行)
"""
回调函数核心:
- submit()返回的对象调用add_done_callback(回调函数)
- 回调函数会接收submit返回的对象作为参数,通过result()获取任务返回值
"""

# ---------------------- (1) 线程池的回调函数(子线程执行) ----------------------
from concurrent.futures import ThreadPoolExecutor
from threading import current_thread as cthread
import time

# 定义线程任务函数
def func(i):
    print("thread", i, cthread().ident)  # 打印线程ID
    time.sleep(0.1)
    print("thread end %s" % (i))
    return "*" * i  # 返回i个星号

# 定义回调函数(任务完成后执行)
def call_back(args):
    print("call back线程ID:", cthread().ident)
    print("任务返回值:", args.result())  # args是submit返回的对象

# 1. 创建线程池(最大并发5)
tp = ThreadPoolExecutor(5)

# 2. 提交10个任务,每个任务绑定回调函数
for i in range(1, 11):
    tp.submit(func, i).add_done_callback(call_back)

# 3. 关闭线程池
tp.shutdown()

print("主线程ID:", cthread().ident)
print("主线程执行结束")

# ---------------------- (2) 进程池的回调函数(主进程执行) ----------------------
"""
from concurrent.futures import ProcessPoolExecutor
import os , time

def func(i):
    print("Process", i, os.getpid())  # 子进程ID
    time.sleep(0.1)
    print("Process end %s" % (i))
    return "*" * i

def call_back(args):
    print("call back进程ID:", os.getpid())  # 主进程ID
    print("任务返回值:", args.result())

if __name__ == "__main__":
    ppe = ProcessPoolExecutor(5)
    for i in range(1, 11):
        ppe.submit(func, i).add_done_callback(call_back)
    ppe.shutdown()
    print("主进程ID:", os.getpid())
    print("父进程ID:", os.getppid())
"""

# 【运行过程&结果(线程池)】
# 1. 子线程执行func(1) → 打印thread 1 12345 → 0.1秒后打印thread end 1
# 2. 自动触发call_back → 打印call back线程ID:12345(和func的线程ID相同)→ 打印任务返回值:*
# 3. 重复上述过程,直到10个任务完成
# 4. 主线程打印:主线程ID:12340 → 主线程执行结束

2.8 协程

核心逻辑

单线程内的“微线程”,通过切换规避IO阻塞(如sleep、网络请求),比线程更轻量;gevent配合monkey.patch_all()可自动识别IO阻塞并切换。

# ### 协程(单线程高并发)
"""
协程核心:
- greenlet:手动切换协程(需手动调用switch())
- gevent:自动切换协程(遇到IO阻塞自动切),需monkey.patch_all()识别系统IO阻塞
- spawn(函数, 参数):创建协程对象
- join():阻塞直到协程执行完毕;joinall([协程1, 协程2]):等待多个协程
- value:获取协程返回值
"""

# 第一步:解决gevent无法识别系统IO的问题(如time.sleep)
from gevent import monkey
monkey.patch_all()  # 识别所有导入模块的IO阻塞
import gevent
import time

# 定义协程任务1:吃
def eat():
    print("eat one1")
    time.sleep(1)  # IO阻塞,gevent自动切换到其他协程
    print("eat two2")
    return "吃..done"

# 定义协程任务2:玩
def play():
    print("play one3")
    time.sleep(1)  # IO阻塞,gevent自动切换回eat协程
    print("play two4")
    return "玩..done"

# 1. 创建协程对象
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)

# 2. 等待所有协程执行完毕(替代单独调用g1.join()和g2.join())
gevent.joinall([g1, g2])

# 3. 获取协程返回值
print("g1返回值:", g1.value)
print("g2返回值:", g2.value)

print("主线程执行结束...")

# 【运行过程&结果】
# 1. 执行g1.spawn(eat) → 打印eat one1 → 遇到time.sleep(1),gevent切换到g2
# 2. 执行g2.spawn(play) → 打印play one3 → 遇到time.sleep(1),gevent切换回g1
# 3. 1秒后,g1的sleep结束 → 打印eat two2
# 4. g2的sleep结束 → 打印play two4
# 5. 打印g1返回值:吃..done → g2返回值:玩..done
# 6. 打印主线程执行结束...
# 【耗时】总耗时≈1秒(而非2秒,因为IO阻塞时切换协程,并发执行)

三、应用场景及案例(详细注释+运行过程)

3.1 线程数据隔离:多线程下载任务(每个线程独立记录进度)

场景说明

多线程下载多个文件,每个线程独立记录自己的下载进度,避免进度数据互相覆盖。

# ### 应用场景1:多线程下载任务(线程数据隔离)
from threading import Thread, local, current_thread
import time

# 1. 创建local对象(每个线程独立的变量空间)
loc = local()

# 模拟下载函数:每个线程下载一个文件,记录进度
def download_file(file_name, progress_step):
    # 给当前线程的loc对象赋值:文件名+初始进度0%
    loc.file_name = file_name
    loc.progress = "0%"
    # 模拟5次进度更新(每次延迟0.5秒)
    for i in range(1, 6):
        # 更新当前线程的进度
        loc.progress = f"{progress_step * i}%"
        # 打印当前线程的下载进度(仅当前线程可见)
        print(f"线程[{current_thread().name}] - 文件[{loc.file_name}] - 下载进度:{loc.progress}")
        time.sleep(0.5)

# 2. 创建3个子线程,分别下载3个文件
t1 = Thread(target=download_file, args=("movie.mp4", 20), name="下载线程1")
t2 = Thread(target=download_file, args=("music.mp3", 15), name="下载线程2")
t3 = Thread(target=download_file, args=("document.pdf", 10), name="下载线程3")

# 3. 启动线程
t1.start()
t2.start()
t3.start()

# 4. 等待所有线程完成
t1.join()
t2.join()
t3.join()

# 5. 主线程打印自己的loc(无数据,因为子线程的loc独立)
print(f"主线程loc:{loc.__dict__}")

# 【运行过程&结果】
# 线程[下载线程1] - 文件[movie.mp4] - 下载进度:20%
# 线程[下载线程2] - 文件[music.mp3] - 下载进度:15%
# 线程[下载线程3] - 文件[document.pdf] - 下载进度:10%
# (0.5秒后)
# 线程[下载线程1] - 文件[movie.mp4] - 下载进度:40%
# 线程[下载线程2] - 文件[music.mp3] - 下载进度:30%
# 线程[下载线程3] - 文件[document.pdf] - 下载进度:20%
# (重复直到100%/75%/50%)
# 主线程loc:{}

3.2 线程Event:多线程任务触发(等待资源准备完成)

场景说明

主线程准备数据(如加载配置文件),多个工作线程等待数据准备完成后才开始执行任务。

# ### 应用场景2:多线程任务触发(等待资源准备)
from threading import Event, Thread
import time

# 1. 创建Event对象(初始状态False)
resource_ready = Event()

# 模拟:准备资源(主线程/子线程)
def prepare_resource():
    print("开始加载配置文件...")
    time.sleep(3)  # 模拟加载耗时3秒
    print("配置文件加载完成!")
    resource_ready.set()  # 标记资源准备完成

# 模拟:工作线程(处理业务逻辑,需等待资源准备)
def worker(worker_id):
    print(f"工作线程{worker_id}:等待资源准备...")
    resource_ready.wait()  # 阻塞直到资源准备完成
    print(f"工作线程{worker_id}:开始处理业务逻辑!")
    # 模拟业务处理
    time.sleep(1)
    print(f"工作线程{worker_id}:业务处理完成!")

# 2. 创建资源准备线程
prepare_thread = Thread(target=prepare_resource)
prepare_thread.start()

# 3. 创建5个工作线程
for i in range(5):
    t = Thread(target=worker, args=(i+1,))
    t.start()

# 【运行过程&结果】
# 开始加载配置文件...
# 工作线程1:等待资源准备...
# 工作线程2:等待资源准备...
# 工作线程3:等待资源准备...
# 工作线程4:等待资源准备...
# 工作线程5:等待资源准备...
# (3秒后)
# 配置文件加载完成!
# 工作线程1:开始处理业务逻辑!
# 工作线程2:开始处理业务逻辑!
# 工作线程3:开始处理业务逻辑!
# 工作线程4:开始处理业务逻辑!
# 工作线程5:开始处理业务逻辑!
# (1秒后)
# 工作线程1:业务处理完成!
# 工作线程2:业务处理完成!
# ...(依次完成)

3.3 线程队列:生产者消费者模型(多线程生产任务,多线程消费任务)

场景说明

生产者线程生成任务(如爬取任务),消费者线程处理任务(如解析数据),通过队列解耦,线程安全。

# ### 应用场景3:生产者消费者模型(线程队列)
from queue import Queue
from threading import Thread
import time
import random

# 1. 创建队列(最大长度10)
task_queue = Queue(10)

# 模拟:生产者(生成爬取任务)
def producer(producer_id):
    for i in range(5):  # 每个生产者生成5个任务
        # 模拟任务数据:爬取URL
        task = f"https://example.com/page_{producer_id}_{i}"
        # 放入队列(队列满则阻塞)
        task_queue.put(task)
        print(f"生产者{producer_id}:生成任务 → {task}")
        time.sleep(random.uniform(0.1, 0.5))  # 随机延迟,模拟生产速度

# 模拟:消费者(处理爬取任务)
def consumer(consumer_id):
    while True:
        # 从队列取任务(队列空则阻塞)
        task = task_queue.get()
        print(f"消费者{consumer_id}:处理任务 → {task}")
        # 模拟处理耗时
        time.sleep(random.uniform(0.2, 0.6))
        # 标记任务完成(队列的task_done(),配合join()使用)
        task_queue.task_done()
        # 终止条件:队列空且所有生产者完成(此处简化:处理10个任务后退出)
        if task_queue.qsize() == 0 and all(not t.is_alive() for t in producer_threads):
            break

# 2. 创建2个生产者线程
producer_threads = []
for i in range(2):
    t = Thread(target=producer, args=(i+1,))
    producer_threads.append(t)
    t.start()

# 3. 创建3个消费者线程
consumer_threads = []
for i in range(3):
    t = Thread(target=consumer, args=(i+1,))
    consumer_threads.append(t)
    t.start()

# 4. 等待队列所有任务完成
task_queue.join()

# 5. 等待所有线程完成
for t in producer_threads + consumer_threads:
    t.join()

print("所有任务处理完成!")

# 【运行过程&结果】
# 生产者1:生成任务 → https://example.com/page_1_0
# 生产者2:生成任务 → https://example.com/page_2_0
# 消费者1:处理任务 → https://example.com/page_1_0
# 消费者2:处理任务 → https://example.com/page_2_0
# (交替生成和处理,直到所有10个任务完成)
# 所有任务处理完成!

3.4 线程池:高并发接口请求(限制并发数,避免端口耗尽)

场景说明

批量请求多个接口,用线程池限制并发数,避免创建过多线程导致系统资源耗尽。

# ### 应用场景4:线程池处理高并发接口请求
from concurrent.futures import ThreadPoolExecutor
import requests
import time

# 1. 定义接口请求函数
def request_api(url):
    """请求单个接口,返回状态码和响应长度"""
    try:
        start = time.time()
        response = requests.get(url, timeout=5)
        cost = round(time.time() - start, 2)  # 耗时(秒)
        return {
            "url": url,
            "status_code": response.status_code,
            "response_length": len(response.text),
            "cost_time": cost
        }
    except Exception as e:
        return {
            "url": url,
            "error": str(e),
            "cost_time": round(time.time() - start, 2)
        }

# 2. 定义回调函数(处理接口返回结果)
def handle_result(result):
    """回调函数:打印接口请求结果"""
    res = result.result()
    if "error" in res:
        print(f"URL:{res['url']} → 失败 → 错误:{res['error']} → 耗时:{res['cost_time']}s")
    else:
        print(f"URL:{res['url']} → 成功 → 状态码:{res['status_code']} → 响应长度:{res['response_length']} → 耗时:{res['cost_time']}s")

# 3. 待请求的URL列表
url_list = [
    "https://www.baidu.com",
    "https://www.taobao.com",
    "https://www.jingdong.com",
    "https://www.4399.com",
    "https://www.7k7k.com",
] * 4  # 复制4次,共20个URL

# 4. 创建线程池(最大并发5)
with ThreadPoolExecutor(max_workers=5) as executor:
    # 5. 提交任务并绑定回调函数
    for url in url_list:
        executor.submit(request_api, url).add_done_callback(handle_result)

print("所有接口请求任务已提交,等待完成...")

# 【运行过程&结果】
# URL:https://www.baidu.com → 成功 → 状态码:200 → 响应长度:2443 → 耗时:0.05s
# URL:https://www.taobao.com → 成功 → 状态码:200 → 响应长度:102456 → 耗时:0.12s
# (并发处理5个请求,依次完成20个URL的请求,打印结果)
# 所有接口请求任务已提交,等待完成...

3.5 协程:高效爬虫(单线程并发爬取多个网页)

场景说明

爬取多个网页,用协程替代线程,减少线程切换开销,提升爬取效率(IO密集型任务)。

# ### 应用场景5:协程实现高效爬虫
from gevent import monkey
monkey.patch_all()  # 识别IO阻塞
import gevent
import requests
import time

# 1. 定义爬取函数
def crawl_url(url):
    """爬取单个URL,返回URL和响应长度"""
    try:
        start = time.time()
        response = requests.get(url, timeout=5)
        cost = round(time.time() - start, 2)
        return {
            "url": url,
            "status_code": response.status_code,
            "length": len(response.text),
            "cost": cost
        }
    except Exception as e:
        return {
            "url": url,
            "error": str(e),
            "cost": cost
        }

# 2. 定义结果处理函数
def print_result(result):
    """打印爬取结果"""
    if "error" in result:
        print(f"【失败】{result['url']} → 错误:{result['error']} → 耗时:{result['cost']}s")
    else:
        print(f"【成功】{result['url']} → 状态码:{result['status_code']} → 长度:{result['length']} → 耗时:{result['cost']}s")

# 3. 待爬取的URL列表
url_list = [
    "https://www.baidu.com",
    "https://www.4399.com",
    "https://www.jingdong.com",
    "https://www.taobao.com",
] * 10  # 复制10次,共40个URL

# 4. 记录开始时间
start_time = time.time()

# 5. 创建协程任务列表
tasks = []
for url in url_list:
    # 创建协程:爬取URL后执行print_result
    task = gevent.spawn(crawl_url, url)
    tasks.append(task)

# 6. 等待所有协程完成
gevent.joinall(tasks)

# 7. 打印每个协程的结果
for task in tasks:
    print_result(task.value)

# 8. 打印总耗时
total_cost = round(time.time() - start_time, 2)
print(f"\n总爬取耗时:{total_cost}s")

# 【运行过程&结果】
# 【成功】https://www.baidu.com → 状态码:200 → 长度:2443 → 耗时:0.04s
# 【成功】https://www.4399.com → 状态码:200 → 长度:123456 → 耗时:0.08s
# (协程自动切换,并发爬取40个URL,总耗时≈2-3秒,远低于单线程顺序爬取的20+秒)
# 总爬取耗时:2.56s


34、FTP 注册登录下载系统

全文总结

模块 核心功能 关键技术 执行流程
注册功能 账号注册、用户名查重、密码MD5加密存储 socketserver多线程、json序列化、MD5加盐加密 客户端输入账号密码→服务端查重→加密写入文件→返回结果
登录功能 账号密码校验、登录状态判断 for...else、反射、MD5加密比对 客户端输入账号密码→服务端加密比对→返回成功/失败
下载功能 文件存在性校验、大文件传输、解决黏包 struct打包长度、分块传输、路径处理 客户端请求下载→服务端校验文件→发送长度+文件→客户端接收保存
核心工具 数据传输、并发处理、反射调用 json、socketserver.ThreadingTCPServer、getattr反射 字典序列化传输→多线程处理多客户端→反射调用功能函数
文件结构 数据存储、路径管理 os路径拼接、db/userinfo.txt存储账号密码 自动创建路径、账号密码加密存储、文件分目录管理

完整详细输出(代码+逐行注释+执行过程)

1. 注册功能(客户端+服务端)

1.1 注册客户端(1.client_register.py)
# 导入socket(网络通信)、json(序列化字典)
import socket
import json

# 1. 创建TCP客户端套接字
sk = socket.socket()
# 2. 连接注册服务端(IP+端口:127.0.0.1:9000)
sk.connect(("127.0.0.1", 9000))

# 封装登录/注册函数
def auth(opt):
    """
    opt: 操作类型(register注册/login登录)
    功能:获取用户输入,序列化发送,接收服务端响应
    """
    # 获取用户名和密码(去空格)
    usr = input("username: ").strip()
    pwd = input("password: ").strip()
    # 封装请求字典
    dic = {"user": usr, "passwd": pwd, "operate": opt}
    # 字典→json字符串→字节流,发送给服务端
    str_dic = json.dumps(dic)
    sk.send(str_dic.encode())
    
    # 接收服务端响应,解码转字典
    file_info = sk.recv(1024).decode()
    file_dic = json.loads(file_info)
    return file_dic

# 执行注册操作
res = auth("register")
# 打印注册结果
print(res)
# 关闭连接
sk.close()

# 执行过程:
# 1. 输入用户名→输入密码→发送{"user":"xxx","passwd":"xxx","operate":"register"}
# 2. 服务端查重、加密存储→返回{"result":True/False,"info":"xxx"}
# 3. 客户端打印结果,关闭连接
1.2 注册服务端(1.server_register.py)
# 导入socketserver(多线程服务)、json、hashlib(MD5加密)、os(路径)
import socketserver
import json
import hashlib
import os

# 获取当前文件所在目录,保证路径移植性
base_path = os.path.dirname(__file__)
# 拼接账号存储路径:当前目录/db/userinfo.txt
userinfo = os.path.join(base_path, "db", "userinfo.txt")

# 认证类:封装加密、注册逻辑
class Auth():
    # 静态方法:MD5加密(用户名作为盐,提升安全性)
    @staticmethod
    def md5(usr, pwd):
        md5_obj = hashlib.md5(usr.encode())
        md5_obj.update(pwd.encode())
        return md5_obj.hexdigest()
    
    # 类方法:注册逻辑
    @classmethod
    def register(cls, opt_dic):
        # 1. 用户名查重:遍历userinfo.txt
        with open(userinfo, mode="r", encoding="utf-8") as fp:
            for line in fp:
                username = line.split(":")[0]
                # 用户名已存在,返回失败
                if username == opt_dic["user"]:
                    return {"result": False, "info": "用户名被注册了"}
        
        # 2. 写入新用户(用户名:加密密码)
        with open(userinfo, mode="a+", encoding="utf-8") as fp:
            fp.write("%s:%s\n" % (opt_dic['user'], cls.md5(opt_dic['user'], opt_dic["passwd"])))
        
        # 3. 返回注册成功
        return {"result": True, "info": "注册成功"}

# FTP服务端类:处理客户端请求
class FTPServer(socketserver.BaseRequestHandler):
    # 处理客户端通信(重写父类方法)
    def handle(self):
        # 接收客户端数据(字典)
        opt_dic = self.myrecv()
        # 反射:判断Auth类是否有对应操作方法
        if hasattr(Auth, opt_dic["operate"]):
            # 调用注册方法,获取结果
            res = getattr(Auth, opt_dic['operate'])(opt_dic)
            # 发送结果给客户端
            self.mysend(res)
    
    # 自定义接收:字节流→字符串→字典
    def myrecv(self):
        info = self.request.recv(1024)
        opt_str = info.decode()
        opt_dic = json.loads(opt_str)
        return opt_dic
    
    # 自定义发送:字典→字符串→字节流
    def mysend(self, send_info):
        send_info = json.dumps(send_info)
        self.request.send(send_info.encode())

# 创建多线程TCP服务端,绑定9000端口
myserver = socketserver.ThreadingTCPServer(("127.0.0.1", 9000), FTPServer)
# 永久运行服务端
myserver.serve_forever()

# 执行过程:
# 1. 服务端启动,监听9000端口
# 2. 客户端连接,发送注册请求
# 3. 服务端接收→反射调用register→查重→加密写入→返回结果
# 4. 客户端接收结果,打印

2. 登录功能(客户端+服务端)

2.1 登录客户端(2.client_login.py)
import socket
import json

# 1. 创建客户端,连接9001端口
sk = socket.socket()
sk.connect(("127.0.0.1", 9001))

# 封装认证函数
def auth(opt):
    usr = input("username: ").strip()
    pwd = input("password: ").strip()
    dic = {"user": usr, "passwd": pwd, "operate": opt}
    str_dic = json.dumps(dic)
    sk.send(str_dic.encode())
    # 接收响应
    file_info = sk.recv(1024).decode()
    file_dic = json.loads(file_info)
    return file_dic

# 注册函数
def register():
    res = auth("register")
    return res

# 登录函数
def login():
    res = auth("login")
    return res

# 退出函数
def myexit():
    opt_dic = {"operate": "myexit"}
    sk.send(json.dumps(opt_dic).encode())
    exit("欢迎您来,欢迎您再来~")

# 操作菜单1(未登录)
operate_lst1 = [("登录", login), ("注册", register), ("退出", myexit)]

# 主菜单函数
def main(operate_lst1):
    # 打印菜单
    for i, tup in enumerate(operate_lst1, start=1):
        print(i, tup[0])
    # 选择操作
    num = int(input("请选择要执行的序号:>>>>").strip())
    # 调用对应函数
    res = operate_lst1[num-1][1]()
    return res

# 循环显示菜单
while True:
    res = main(operate_lst1)
    print(res)
sk.close()

# 执行过程:
# 1. 客户端启动,显示菜单:1登录 2注册 3退出
# 2. 选择1→输入账号密码→发送登录请求→服务端校验→返回结果
# 3. 登录成功/失败,打印结果
2.2 登录服务端(2.server_login.py)
import socketserver
import json
import hashlib
import os

base_path = os.path.dirname(__file__)
userinfo = os.path.join(base_path, "db", "userinfo.txt")

class Auth():
    @staticmethod
    def md5(usr, pwd):
        md5_obj = hashlib.md5(usr.encode())
        md5_obj.update(pwd.encode())
        return md5_obj.hexdigest()
    
    # 注册方法
    @classmethod
    def register(cls, opt_dic):
        with open(userinfo, mode="r", encoding="utf-8") as fp:
            for line in fp:
                username = line.split(":")[0]
                if username == opt_dic["user"]:
                    return {"result": False, "info": "用户名被注册了"}
        with open(userinfo, mode="a+", encoding="utf-8") as fp:
            fp.write("%s:%s\n" % (opt_dic['user'], cls.md5(opt_dic['user'], opt_dic["passwd"])))
        return {"result": True, "info": "注册成功"}
    
    # 登录方法
    @classmethod
    def login(cls, opt_dic):
        with open(userinfo, mode="r+", encoding="utf-8") as fp:
            # 遍历账号密码,比对加密后的值
            for line in fp:
                username, password = line.strip().split(":")
                # 用户名+加密密码比对一致,登录成功
                if username == opt_dic["user"] and password == cls.md5(opt_dic['user'], opt_dic['passwd']):
                    return {"result": True, "info": "登陆成功"}
            # 循环正常结束(无break/return),执行else,登录失败
            else:
                return {"result": False, "info": "登陆失败"}
    
    # 退出方法
    @classmethod
    def myexit(cls, opt_dic):
        return {"result": "myexit"}

class FTPServer(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            opt_dic = self.myrecv()
            if hasattr(Auth, opt_dic["operate"]):
                res = getattr(Auth, opt_dic['operate'])(opt_dic)
                # 退出操作,直接返回,关闭连接
                if res["result"] == "myexit":
                    return 
                self.mysend(res)
            else:
                dic = {"result": False, "info": "没有该类操作"}
                self.mysend(dic)
    
    def myrecv(self):
        info = self.request.recv(1024)
        opt_str = info.decode()
        opt_dic = json.loads(opt_str)
        return opt_dic
    
    def mysend(self, send_info):
        send_info = json.dumps(send_info)
        self.request.send(send_info.encode())

# 创建服务端,绑定9001端口,允许端口复用
myserver = socketserver.ThreadingTCPServer(("127.0.0.1", 9001), FTPServer)
socketserver.TCPServer.allow_reuse_address = True
myserver.serve_forever()

# 执行过程:
# 1. 服务端监听9001端口
# 2. 客户端发送登录请求→服务端调用login→加密比对
# 3. 返回登录结果→客户端接收打印

3. 下载功能(客户端+服务端)

3.1 下载客户端(3.client_download.py)
import socket
import json
import struct
import os

sk = socket.socket()
sk.connect(("127.0.0.1", 9001))

# 自定义接收(解决黏包:sign=True接收4字节长度)
def myrecv(info_len=1024, sign=False):
    if sign:
        # 接收4字节长度,解包
        info_len = sk.recv(4)
        info_len = struct.unpack("i", info_len)[0]
    file_info = sk.recv(info_len).decode()
    file_dic = json.loads(file_info)
    return file_dic

# 认证函数
def auth(opt):
    usr = input("username: ").strip()
    pwd = input("password: ").strip()
    dic = {"user": usr, "passwd": pwd, "operate": opt}
    str_dic = json.dumps(dic)
    sk.send(str_dic.encode())
    return myrecv()

def register():
    return auth("register")

def login():
    return auth("login")

def myexit():
    opt_dic = {"operate": "myexit"}
    sk.send(json.dumps(opt_dic).encode())
    exit("欢迎您来,欢迎您再来~")

# 下载函数
def download():
    # 发送下载请求
    operate_dic = {"operate": "download", "filename": "ceshimovie.mp4"}
    sk.send(json.dumps(operate_dic).encode())
    # 1. 接收文件是否存在的结果(带长度)
    res = myrecv(sign=True)
    if res["result"]:
        # 2. 接收文件名+文件大小
        dic = myrecv(sign=True)
        # 创建下载目录
        try:
            os.mkdir("mydownload")
        except:
            pass
        # 3. 分块接收文件,写入本地
        with open("./mydownload/"+dic['filename'], mode="wb") as fp:
            while dic['filesize']:
                content = sk.recv(1024000)
                fp.write(content)
                dic['filesize'] -= len(content)
        print("客户端_文件下载完毕")
    else:
        print("没有该文件")

# 菜单
operate_lst1 = [("登录", login), ("注册", register), ("退出", myexit)]
operate_lst2 = [("下载", download), ("退出", myexit)]

def main(operate_lst1):
    for i, tup in enumerate(operate_lst1, start=1):
        print(i, tup[0])
    num = int(input("请选择要执行的序号:>>>>").strip())
    res = operate_lst1[num-1][1]()
    return res

# 循环菜单:登录成功后进入下载菜单
while True:
    res = main(operate_lst1)
    print(res)
    if res["result"]:
        while True:
            res = main(operate_lst2)
            print(res)
sk.close()

# 执行过程:
# 1. 登录成功→显示下载菜单
# 2. 选择下载→发送文件名→服务端校验文件
# 3. 服务端发送长度+文件名大小+文件内容
# 4. 客户端分块接收,保存到mydownload目录
3.2 下载服务端(3.server_download.py)
import socketserver
import json
import hashlib
import os
import struct

base_path = os.path.dirname(__file__)
userinfo = os.path.join(base_path, "db", "userinfo.txt")

class Auth():
    @staticmethod
    def md5(usr, pwd):
        md5_obj = hashlib.md5(usr.encode())
        md5_obj.update(pwd.encode())
        return md5_obj.hexdigest()
    
    @classmethod
    def register(cls, opt_dic):
        with open(userinfo, mode="r", encoding="utf-8") as fp:
            for line in fp:
                username = line.split(":")[0]
                if username == opt_dic["user"]:
                    return {"result": False, "info": "用户名被注册了"}
        with open(userinfo, mode="a+", encoding="utf-8") as fp:
            fp.write("%s:%s\n" % (opt_dic['user'], cls.md5(opt_dic['user'], opt_dic["passwd"])))
        return {"result": True, "info": "注册成功"}
    
    @classmethod
    def login(cls, opt_dic):
        with open(userinfo, mode="r+", encoding="utf-8") as fp:
            for line in fp:
                username, password = line.strip().split(":")
                if username == opt_dic["user"] and password == cls.md5(opt_dic['user'], opt_dic['passwd']):
                    return {"result": True, "info": "登陆成功"}
            else:
                return {"result": False, "info": "登陆失败"}
    
    @classmethod
    def myexit(cls, opt_dic):
        return {"result": "myexit"}

class FTPServer(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            opt_dic = self.myrecv()
            if hasattr(Auth, opt_dic["operate"]):
                res = getattr(Auth, opt_dic['operate'])(opt_dic)
                if res["result"] == "myexit":
                    return 
                self.mysend(res)
                
                # 登录成功,进入下载/退出菜单
                if res["result"]:
                    while True:
                        opt_dic = self.myrecv()
                        if opt_dic['operate'] == 'myexit':
                            return 
                        # 反射调用下载方法
                        if hasattr(self, opt_dic['operate']):
                            getattr(self, opt_dic['operate'])(opt_dic)
            else:
                dic = {"result": False, "info": "没有该类操作"}
                self.mysend(dic)
    
    def myrecv(self):
        info = self.request.recv(1024)
        opt_str = info.decode()
        opt_dic = json.loads(opt_str)
        return opt_dic
    
    # 自定义发送(sign=True:先发4字节长度,解决黏包)
    def mysend(self, send_info, sign=False):
        send_info = json.dumps(send_info)
        if sign:
            # 打包数据长度,先发长度
            send_len = struct.pack("i", len(send_info))
            self.request.send(send_len)
        # 发送真实数据
        self.request.send(send_info.encode())
    
    # 下载方法
    def download(self, opt_dic):
        # 获取请求的文件名
        filename = opt_dic['filename']
        # 拼接文件绝对路径(video目录下)
        file_abs = os.path.join(base_path, "video", filename)
        if os.path.exists(file_abs):
            # 1. 发送文件存在的结果(带长度)
            dic = {"result": True, "info": "该文件存在"}
            self.mysend(dic, True)
            # 2. 发送文件名+文件大小(带长度)
            filesize = os.path.getsize(file_abs)
            dic = {"filename": filename, "filesize": filesize}
            self.mysend(dic, True)
            
            # 3. 分块发送文件内容
            with open(file_abs, mode="rb") as fp:
                while filesize:
                    content = fp.read(1024000)
                    self.request.send(content)
                    filesize -= len(content)
            print("服务器_文件下载完毕")
        else:
            # 文件不存在,返回失败
            dic = {'result': False, "info": "文件不存在"}
            self.mysend(dic, True)

myserver = socketserver.ThreadingTCPServer(("127.0.0.1", 9001), FTPServer)
socketserver.TCPServer.allow_reuse_address = True
myserver.serve_forever()

# 执行过程:
# 1. 客户端登录成功→请求下载文件
# 2. 服务端校验文件是否存在
# 3. 存在→发送长度+文件信息+分块文件
# 4. 客户端接收保存,下载完成

应用场景及案例(详细注释+执行过程)

应用场景1:小型企业FTP文件传输系统

场景说明

企业内部员工通过账号密码登录,下载公司共享文件(文档、视频、安装包),支持多用户同时登录下载。

完整案例代码(整合版)
# 服务端核心(简化版,支持注册+登录+下载)
import socketserver
import json
import hashlib
import os
import struct

# 路径配置
base_path = os.path.dirname(__file__)
userinfo = os.path.join(base_path, "db", "userinfo.txt")

# 认证加密类
class Auth():
    @staticmethod
    def md5(usr, pwd):
        md5_obj = hashlib.md5(usr.encode())
        md5_obj.update(pwd.encode())
        return md5_obj.hexdigest()
    
    # 注册
    @classmethod
    def register(cls, opt_dic):
        with open(userinfo, mode="r", encoding="utf-8") as fp:
            for line in fp:
                if line.split(":")[0] == opt_dic["user"]:
                    return {"result": False, "info": "用户名已注册"}
        with open(userinfo, mode="a+", encoding="utf-8") as fp:
            fp.write(f"{opt_dic['user']}:{cls.md5(opt_dic['user'],opt_dic['passwd'])}\n")
        return {"result": True, "info": "注册成功"}
    
    # 登录
    @classmethod
    def login(cls, opt_dic):
        with open(userinfo, mode="r") as fp:
            for line in fp:
                usr, pwd = line.strip().split(":")
                if usr == opt_dic["user"] and pwd == cls.md5(opt_dic["user"], opt_dic["passwd"]):
                    return {"result": True, "info": "登录成功"}
        return {"result": False, "info": "账号或密码错误"}
    
    # 退出
    @classmethod
    def myexit(cls, opt_dic):
        return {"result": "myexit"}

# 服务端处理类
class FTPServer(socketserver.BaseRequestHandler):
    def handle(self):
        # 登录注册逻辑
        while True:
            opt_dic = self.myrecv()
            res = getattr(Auth, opt_dic["operate"])(opt_dic)
            if res["result"] == "myexit": return
            self.mysend(res)
            # 登录成功,进入下载
            if res["result"]:
                while True:
                    opt_dic = self.myrecv()
                    if opt_dic["operate"] == "myexit": return
                    self.download(opt_dic)
    
    # 接收数据
    def myrecv(self):
        return json.loads(self.request.recv(1024).decode())
    
    # 发送数据(解决黏包)
    def mysend(self, data, sign=False):
        data = json.dumps(data)
        if sign:
            self.request.send(struct.pack("i", len(data)))
        self.request.send(data.encode())
    
    # 下载功能
    def download(self, opt_dic):
        file_path = os.path.join(base_path, "video", opt_dic["filename"])
        if not os.path.exists(file_path):
            return self.mysend({"result":False,"info":"文件不存在"}, True)
        # 发送文件信息
        self.mysend({"result":True}, True)
        self.mysend({"filename":opt_dic["filename"],"filesize":os.path.getsize(file_path)}, True)
        # 发送文件
        with open(file_path, "rb") as f:
            while True:
                chunk = f.read(1024000)
                if not chunk: break
                self.request.send(chunk)

# 启动服务
if __name__ == "__main__":
    server = socketserver.ThreadingTCPServer(("0.0.0.0", 9001), FTPServer)
    socketserver.TCPServer.allow_reuse_address = True
    print("FTP服务启动,端口9001")
    server.serve_forever()
执行过程
  1. 服务端启动:创建db目录(存账号)、video目录(存共享文件),运行服务端。
  2. 员工注册:客户端输入账号密码,服务端加密存储,返回注册成功。
  3. 员工登录:客户端输入账号密码,服务端校验,返回登录成功。
  4. 文件下载:客户端选择下载,服务端校验文件→分块传输→客户端保存。
  5. 多用户并发:socketserver多线程支持,多名员工同时登录下载不冲突。

### 应用场景2:个人文件共享工具

场景说明

个人搭建简易文件共享服务,家人/朋友注册登录后,下载你共享的电影、照片、文档。

核心优势
  1. 轻量:无需安装大型FTP软件,Python直接运行。
  2. 安全:密码MD5加密存储,不泄露明文密码。
  3. 便捷:支持多用户、大文件传输,解决黏包问题。
  4. 跨平台:Windows/Mac/Linux均可运行。


FTP注册登录下载系统「一键运行版」

一、项目目录结构(直接按这个建文件夹)

ftp_project/          # 项目根目录
├─ db/                # 自动创建,存用户账号密码
│  └─ userinfo.txt     # 账号数据文件
├─ video/             # 手动创建,放要下载的文件
│  └─ ceshimovie.mp4  # 测试下载文件(可放任意文件)
├─ server.py          # 多线程服务端(注册+登录+下载)
└─ client.py          # 客户端(菜单操作)

二、完整代码(直接复制)

1. 服务端 server.py
"""
FTP多线程服务端:注册、登录、文件下载
端口:9001
数据存储:db/userinfo.txt
下载文件:video/xxx
"""
import socketserver
import json
import hashlib
import os
import struct

# ===================== 路径配置(自动适配,不用改) =====================
base_path = os.path.dirname(__file__)
# 用户账号文件
db_path = os.path.join(base_path, "db")
userinfo_file = os.path.join(db_path, "userinfo.txt")
# 下载文件目录
video_path = os.path.join(base_path, "video")

# 自动创建文件夹
os.makedirs(db_path, exist_ok=True)
os.makedirs(video_path, exist_ok=True)

# ===================== 认证加密类 =====================
class Auth:
    # MD5加密(用户名当盐,更安全)
    @staticmethod
    def md5(usr, pwd):
        md5 = hashlib.md5(usr.encode())
        md5.update(pwd.encode())
        return md5.hexdigest()

    # 注册功能
    @classmethod
    def register(cls, data):
        # 用户名查重
        with open(userinfo_file, "a+", encoding="utf-8") as f:
            f.seek(0)
            for line in f:
                if line.strip() and line.split(":")[0] == data["user"]:
                    return {"result": False, "info": "用户名已被注册"}
        # 写入账号密码
        with open(userinfo_file, "a", encoding="utf-8") as f:
            f.write(f"{data['user']}:{cls.md5(data['user'], data['passwd'])}\n")
        return {"result": True, "info": "注册成功"}

    # 登录功能
    @classmethod
    def login(cls, data):
        try:
            with open(userinfo_file, "r", encoding="utf-8") as f:
                for line in f:
                    if not line.strip():
                        continue
                    usr, pwd = line.strip().split(":")
                    if usr == data["user"] and pwd == cls.md5(data["user"], data["passwd"]):
                        return {"result": True, "info": "登录成功"}
            return {"result": False, "info": "账号或密码错误"}
        except:
            return {"result": False, "info": "账号文件异常"}

    # 退出功能
    @classmethod
    def myexit(cls, data):
        return {"result": "exit"}

# ===================== 服务端处理类 =====================
class FTPServer(socketserver.BaseRequestHandler):
    # 接收数据
    def recv_data(self):
        data = self.request.recv(1024).decode()
        return json.loads(data)

    # 发送数据(sign=True:解决黏包)
    def send_data(self, data, sign=False):
        data_str = json.dumps(data, ensure_ascii=False)
        if sign:
            # 先发4字节长度
            len_pack = struct.pack("i", len(data_str))
            self.request.send(len_pack)
        self.request.send(data_str.encode())

    # 文件下载
    def download(self, data):
        filename = data["filename"]
        file_path = os.path.join(video_path, filename)

        # 文件不存在
        if not os.path.exists(file_path):
            self.send_data({"result": False, "info": "文件不存在"}, True)
            return

        # 1. 发送文件存在
        self.send_data({"result": True}, True)
        # 2. 发送文件信息
        file_size = os.path.getsize(file_path)
        self.send_data({"filename": filename, "filesize": file_size}, True)
        # 3. 分块发送文件
        with open(file_path, "rb") as f:
            while file_size > 0:
                chunk = f.read(1024000)
                self.request.send(chunk)
                file_size -= len(chunk)
        print(f"【下载完成】文件:{filename}")

    # 处理请求
    def handle(self):
        print(f"新客户端连接:{self.client_address}")
        # 登录注册阶段
        while True:
            data = self.recv_data()
            res = getattr(Auth, data["operate"])(data)
            # 退出
            if res["result"] == "exit":
                break
            self.send_data(res)
            # 登录成功 → 进入下载功能
            if res["result"]:
                while True:
                    data = self.recv_data()
                    if data["operate"] == "myexit":
                        return
                    self.download(data)
        print(f"客户端断开:{self.client_address}")

# ===================== 启动服务 =====================
if __name__ == "__main__":
    # 允许端口复用
    socketserver.TCPServer.allow_reuse_address = True
    # 多线程TCP服务
    server = socketserver.ThreadingTCPServer(("0.0.0.0", 9001), FTPServer)
    print("="*50)
    print("FTP服务启动成功")
    print("服务端口:9001")
    print("下载目录:video/")
    print("账号目录:db/userinfo.txt")
    print("="*50)
    server.serve_forever()
2. 客户端 client.py
"""
FTP客户端:注册、登录、文件下载
连接:127.0.0.1:9001
下载保存:mydownload/
"""
import socket
import json
import struct
import os

# ===================== 配置 =====================
SERVER_IP = "127.0.0.1"
SERVER_PORT = 9001

# ===================== 客户端初始化 =====================
sk = socket.socket()
sk.connect((SERVER_IP, SERVER_PORT))

# ===================== 接收数据(解决黏包) =====================
def recv_data(sign=False):
    if sign:
        # 接收4字节长度
        len_data = sk.recv(4)
        data_len = struct.unpack("i", len_data)[0]
        data = sk.recv(data_len).decode()
    else:
        data = sk.recv(1024).decode()
    return json.loads(data)

# ===================== 发送数据 =====================
def send_data(data):
    sk.send(json.dumps(data).encode())

# ===================== 功能函数 =====================
# 注册
def register():
    usr = input("用户名:").strip()
    pwd = input("密码:").strip()
    send_data({"user": usr, "passwd": pwd, "operate": "register"})
    return recv_data()

# 登录
def login():
    usr = input("用户名:").strip()
    pwd = input("密码:").strip()
    send_data({"user": usr, "passwd": pwd, "operate": "login"})
    return recv_data()

# 退出
def myexit():
    send_data({"operate": "myexit"})
    print("欢迎下次光临~")
    sk.close()
    exit()

# 下载文件
def download():
    filename = "ceshimovie.mp4"  # 要下载的文件名(和服务端video里一致)
    send_data({"operate": "download", "filename": filename})

    # 1. 接收文件是否存在
    res = recv_data(sign=True)
    if not res["result"]:
        print(res["info"])
        return

    # 2. 接收文件信息
    file_info = recv_data(sign=True)
    print(f"开始下载:{file_info}")

    # 创建下载目录
    os.makedirs("mydownload", exist_ok=True)

    # 3. 接收文件
    save_path = os.path.join("mydownload", file_info["filename"])
    with open(save_path, "wb") as f:
        remaining = file_info["filesize"]
        while remaining > 0:
            chunk = sk.recv(1024000)
            f.write(chunk)
            remaining -= len(chunk)
    print("✅ 文件下载完成!保存到:mydownload/")

# ===================== 菜单 =====================
def show_menu(menu_list):
    print("\n" + "="*30)
    for i, (name, func) in enumerate(menu_list, 1):
        print(f"{i}. {name}")
    print("="*30)
    try:
        choice = int(input("请输入序号:"))
        return menu_list[choice-1][1]()
    except:
        print("输入错误!")
        return {"result": False, "info": "输入错误"}

# 主菜单
menu1 = [("登录", login), ("注册", register), ("退出", myexit)]
# 登录后菜单
menu2 = [("下载文件", download), ("退出", myexit)]

# ===================== 启动客户端 =====================
if __name__ == "__main__":
    print("FTP客户端已启动")
    while True:
        res = show_menu(menu1)
        print(res)
        # 登录成功 → 进入下载菜单
        if res.get("result"):
            while True:
                res = show_menu(menu2)
                print(res)

三、启动步骤

1. 准备工作
  1. 新建文件夹 ftp_project
  2. 按目录结构建好 dbvideo 文件夹
  3. video 里放一个测试文件:ceshimovie.mp4(任意文件都行,名字一致)
2. 启动顺序
  1. 先运行 server.py(看到「FTP服务启动成功」)
  2. 再运行 client.py(弹出菜单)
3. 功能测试流程
  1. 注册:选 2 → 输入用户名、密码
  2. 登录:选 1 → 输入刚注册的账号
  3. 下载:登录成功自动进入下载菜单,选 1 开始下载
  4. 退出:选 2 退出

四、核心功能说明

  1. 多线程并发:支持多人同时登录/下载
  2. 密码加密:MD5加盐存储,不保存明文密码
  3. 黏包解决:struct 打包数据长度,大文件传输不损坏
  4. 自动创建目录:db、video、mydownload 自动创建
  5. 菜单操作:纯命令行菜单,简单易用

五、常见问题

  1. 端口被占用:服务端已加端口复用,重启即可
  2. 下载失败:检查 video 里是否有 ceshimovie.mp4
  3. 连接失败:确保服务端先启动,IP端口正确当前文件内容过长,豆包只阅读了前 61%。










posted @ 2026-05-28 16:43  爱折腾的大臭臭  阅读(8)  评论(0)    收藏  举报