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()
执行过程
- 先运行服务端代码(此时服务端卡在
sk.accept(),等待客户端连接); - 再运行客户端代码,客户端触发
sk.connect(),服务端完成三次握手,代码继续执行; - 客户端发送“你爱我么~”→ 服务端接收并打印;
- 服务端发送“我爱你嗯~”→ 客户端接收并打印;
- 双方关闭连接,程序结束。
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()
执行过程
- 服务端运行后,卡在
sk.accept()等待连接; - 客户端连接后,进入循环:
- 客户端输入消息→发送→等待服务端回复;
- 服务端接收消息→输入回复→发送;
- 当服务端输入“q”并发送,客户端收到
b"q",触发break,关闭连接; - 服务端也触发
break,回到外层循环,等待下一个客户端连接。
二、带括号的数学表达式计算器(一步步拆运算)
2.1 核心思路
计算-30+(40+5*-2)-8这类表达式,核心是“先拆括号、再算乘除、最后算加减”,步骤:
- 找到最内层的括号(比如
(40+5*-2)); - 计算括号内的表达式(先算
5*-2=-10,再算40-10=30); - 把括号替换成计算结果(原式变成
-30+30-8); - 计算最终的加减(
-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,直到表达式无括号;
- 计算最终的加减乘除,得到结果。
三、应用场景 & 案例 & 详细过程
3.1 TCP/Socket的应用场景
场景1:简易聊天工具(对应循环Socket代码)
- 案例:实现双人实时聊天,A(客户端)和B(服务端)互相发消息,输入“q”退出。
- 详细过程:
- 服务端启动:绑定9001端口,监听客户端连接;
- 客户端启动:连接服务端的127.0.0.1:9001;
- 客户端输入“吃饭了吗?”→发送→服务端接收并打印;
- 服务端输入“还没,你呢?”→发送→客户端接收并打印;
- 任意一方输入“q”→发送后,对方收到
b"q",触发退出,连接关闭。
场景2:文件传输(TCP扩展)
- 案例:客户端把本地txt文件发给服务端。
- 核心逻辑(补充):
- 服务端:循环接收客户端发送的二进制数据,写入本地文件;
- 客户端:打开本地文件,按1024字节分段读取,逐段发送给服务端;
- 依赖TCP的“可靠性”,保证文件传输完整。
3.2 UDP的应用场景
场景:多人聊天室(UDP特点适配)
- 案例:3个客户端同时给服务端发消息,服务端广播给所有客户端。
- 核心逻辑(补充):
- 服务端:创建UDP Socket,绑定端口,循环接收所有客户端的消息和地址;
- 客户端:创建UDP Socket,无需连接,直接向服务端端口发消息;
- 服务端收到消息后,遍历所有客户端地址,把消息转发出去;
- 依赖UDP的“无连接、速度快”,适配多人实时聊天(允许偶尔丢包)。
3.3 表达式计算器的应用场景
场景1:简易计算器软件
- 案例:用户输入
(100-20)*5+80/2,程序输出结果。 - 详细运算过程:
- 匹配最内层括号
(100-20)→计算得80→原式变为80*5+80/2; - 算乘除:
80*5=400,80/2=40→原式变为400+40; - 算加减:
400+40=440→输出结果440。
- 匹配最内层括号
场景2:财务/数据分析工具
- 案例:批量计算财报中的复杂公式(如
营收=(单价*销量-成本)*税率+补贴)。 - 核心逻辑:
- 把公式中的变量(单价、销量等)替换为实际数值;
- 调用计算器函数,自动解析括号和运算优先级,输出结果;
- 替代人工计算,避免出错,提升效率。
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()
执行过程
- 先运行服务端:绑定9000端口,进入循环等待接收;
- 再运行客户端:输入“你好”→发送到服务端;
- 服务端收到消息,打印
客户端[(127.0.0.1, 54321)]:你好→输入“你好呀”→回复客户端; - 客户端收到回复,打印
服务端:你好呀; - 循环往复,实现“你发我收”的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()
执行过程
- 运行服务端:执行到
sk.accept()阻塞,等待客户端连接; - 运行客户端:
sk.connect()触发三次握手,服务端退出阻塞; - 客户端发送消息→服务端接收并打印→服务端回复→客户端接收并打印;
- 双方关闭连接,触发四次挥手。
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)
🔥 结合黏包场景总结
-
为什么用struct?
TCP黏包的根源是数据无边界,我们需要固定长度的包头来标记数据大小;
pack("i", 长度)能把任意数字变成固定4字节,客户端只需要先收4字节,就能精准知道后续要收多少数据。 -
格式符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()
🔥 核心原理
-
为什么这样能解决黏包?
TCP是流式数据无边界,客户端不知道第一条数据在哪结束;
我们先发固定4字节的长度,客户端先收4字节→解包得到数据长度→严格按照长度收数据,剩余数据就是下一条,完美分隔。 -
执行顺序(服务端)
建立连接 → 计算数据长度 → 打包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()
🔥 核心逻辑(客户端+服务端联动)
- 通信流程
服务端:发4字节长度 → 发主数据 → 发额外数据
客户端:收4字节 → 解包得长度 → 收主数据 → 收额外数据 - 为什么能解决黏包?
客户端不再盲目接收,而是通过固定4字节包头知道第一条数据的精确大小,严格按大小接收,剩余数据自动成为独立的第二条,彻底分隔开。 - 执行结果
主数据和额外数据会分开打印,不会黏合成一条,黏包问题完美解决!
执行过程
- 服务端:计算数据长度→打包成4字节→发送长度→发送实际数据→发送额外数据;
- 客户端:接收4字节→解包得到长度→按长度接收主数据→接收额外数据;
- 输出结果:主数据和额外数据分开,无黏包。
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()
核心注释总结
- 核心对象
self.request= 客户端连接通道(收发数据)
self.client_address= 客户端地址 - 核心功能
ThreadingTCPServer= 多线程并发,解决单客户端限制 - 执行逻辑
服务端启动 → 监听连接 → 新客户端接入 → 创建线程 → 循环通信 → 断开连接
步骤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()
核心说明
- 配合使用:这个客户端专门对接上面的
socketserver多线程服务端,可以多开几个客户端,同时和服务端通信 - 循环逻辑:
while True让客户端可以一直发消息、收回复,直到手动关闭程序 - 编码解码:网络传输只能传二进制,所以发送要
encode,接收要decode
执行过程
- 启动服务端:循环监听9000端口;
- 启动客户端1:连接→服务端创建线程处理,客户端发“hello”→服务端回复“HELLO”;
- 启动客户端2:连接→服务端创建新线程处理,客户端发“world”→服务端回复“WORLD”;
- 两个客户端可同时发消息,服务端分别回复,实现并发。
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("函数不存在")
执行过程
- 运行代码→输入“func1”→反射调用
func1()→输出“执行功能1”; - 输入“func2”→反射调用
func2()→输出“执行功能2”; - 输入“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协议实现的群聊服务端,核心是接收消息 → 广播转发给所有客户端,无需建立连接,支持多人同时聊天
🔥 核心知识点(必看)
- UDP关键方法
- 接收:
recvfrom(1024)→ 返回 (消息, 客户端地址) - 发送:
sendto(数据, 目标地址)→ 必须指定接收方地址
- 接收:
- 群聊原理
用集合存储所有客户端地址,收到消息后遍历集合,转发给除发送者外的所有人 - 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()
核心要点
UDP客户端无需connect,直接用sendto指定服务端地址就能发消息- 配合上面的
UDP群聊服务端,多开几个客户端即可实现多人聊天 - 服务端会自动转发消息,实现群聊效果
1.3 执行过程
- 启动服务端:绑定9000端口,初始化空集合存储客户端;
- 启动客户端1:输入“大家好”→发送到服务端;
- 服务端:将客户端1地址加入集合→转发消息给所有客户端(暂无其他);
- 启动客户端2:输入“你好呀”→发送到服务端;
- 服务端:将客户端2地址加入集合→转发消息给客户端1;
- 客户端1收到消息:
[(127.0.0.1, 58001)]:你好呀; - 实现多客户端实时群聊,消息自动转发。
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()
核心逻辑总结
- 文件读取:读取本地
test.txt的全部文本内容 - 黏包解决:严格遵循
先发固定4字节长度 → 再发文件数据的规则 - 数据格式:字符串必须
encode编码为二进制才能网络传输 - 配套使用:需要对接之前的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()
核心执行流程(和服务端一一对应)
- 客户端连接服务端
- 收4字节长度 → 解包得到文件大小
- 按长度收完整文件数据
- 写入本地
recv_test.txt - 断开连接
关键亮点
- 彻底解决TCP黏包,文件传输100%完整
- 固定4字节包头,通用稳定,是网络文件传输的标准方案
2.3 执行过程
- 准备
test.txt:写入“TCP文件传输测试”; - 启动服务端:读取文件→绑定端口→等待连接;
- 启动客户端:连接→接收4字节长度→解包得到文件长度→按长度接收内容→保存为
recv_test.txt; - 打开
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
🔥 核心知识点
- 反射的作用
不用写if choice ==1: add_user(),通过字符串直接调用函数,新增功能只需改func_map,无需修改循环逻辑 - 核心函数
getattr(模块, 函数字符串)→ 根据字符串找到并返回函数 - 执行流程
展示菜单 → 用户输入编号 → 反射匹配函数 → 执行功能 → 输入4退出
3.3 执行过程
- 运行代码→打印菜单;
- 输入“1”→映射到
add_user→反射调用→输出“✅ 添加用户成功”; - 输入“3”→映射到
query_user→反射调用→输出“✅ 查询用户成功”; - 输入“4”→映射到
exit_sys→反射调用→输出“🔚 退出系统”→循环结束; - 新增功能时,只需添加函数+更新
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()
执行过程
- 10个进程同时查询票数(异步,不加锁)
- 抢票时上锁,逐个进程修改票数
- 不加锁会出现「多人同时抢到同一张票」,加锁后串行修改,数据安全
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()
执行过程
- 信号量初始=4,同时只允许4个人进入
- 有人离开(解锁),排队的人才能进入
- 控制并发数量,避免资源过载
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("程序全部结束")
执行过程
- 事件初始
False→ 红灯,汽车wait()阻塞 - 交通灯切换
set()→ 绿灯,汽车放行 - 交通灯切换
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()}")
执行过程
- 主进程
put数据 → 队列有数据 - 子进程
get数据 → 打印 - 子进程
put新数据 → 主进程get打印 - 队列实现进程间数据传递,解决数据隔离
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)
执行过程
- 生产者循环生产数据 → 存入队列
- 消费者循环取数据 → 处理数据
- 生产完后发
None→ 消费者终止 - 解耦生产与消费,缓冲速度差异
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("程序彻底结束")
执行过程
- 生产者
put数据 → 队列计数+1 - 消费者
get+task_done→ 队列计数-1 jq.join()阻塞 → 计数=0放行- 守护进程随主进程终止,确保所有数据消费完
应用场景+案例(带详细注释)
场景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}")
运算过程:
- 主进程创建共享字典
dic={"count":500},并创建500个子进程; - 每个子进程启动后,通过
with lock抢占锁,抢到锁的进程修改count(减1),没抢到的等待; - 由于锁的存在,同一时间只有1个进程修改
count,避免「多进程同时修改导致数据错乱」; - 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("程序结束")
运算过程:
- 进程池创建后,默认按CPU核心数(比如6核)启动固定数量的进程;
- 20个任务提交后,进程池会把任务分配给这6个进程,一个进程完成任务后,再接收新任务(进程复用);
- 异步提交任务(apply_async),主进程不等待,直到调用
get()才阻塞获取结果; - 对比「手动创建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}")
运算过程:
- 全局变量n初始=0,创建10个加线程(每个加100万次)+10个减线程(每个减100万次);
- 不加锁时:多线程同时修改n,会导致「指令交错」(比如n=0时,加线程读n=0,减线程也读n=0,加完=1,减完= -1,最终n≠0);
- 加锁后:同一时间只有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("主线程结束")
运算过程:
- 信号量初始值=5,相当于有5把锁;
- 20个线程启动后,前5个线程抢到锁,执行任务(休眠3秒);
- 1个线程执行完释放锁,第6个线程抢到锁,以此类推;
- 最终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()
运算过程:
- 死锁原因:2个互斥锁嵌套,线程A拿面锁等筷子锁,线程B拿筷子锁等面锁,互相等待;
- 递归锁解决:同一线程可多次上锁,且解锁次数=上锁次数;
- 执行流程:比如「曾文」先拿面锁(递归锁计数=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("主线程结束...")
运算过程:
- 守护线程t1启动后,循环打印;普通线程t2执行3秒后结束;
- 主线程休眠5秒后结束,此时所有非守护线程(t2+主线程)都结束,守护线程t1自动终止;
- 新人验证:守护线程不会一直运行,主程序结束即停止。
三、应用场景及案例
场景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}秒")
运算过程
- 进程池创建4个进程,将8个URL分配给这4个进程;
- 每个进程下载2个URL(并行执行),相比单进程串行下载,耗时减少约50%;
- 下载完成后,返回每个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}")
运算过程
- 初始库存100台,200个线程模拟抢购(每个买1台);
- 加锁后,同一时间只有1个线程检查并扣减库存,前100个用户抢购成功,后100个提示库存不足;
- 不加锁时:多个线程同时检查库存(比如库存=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("所有请求已提交,主线程结束")
运算过程
- 信号量初始值=5,相当于接口最多5个并发;
- 30个线程启动后,前5个线程进入接口处理,剩余25个等待;
- 1个线程处理完释放信号量,下1个线程立即进入,始终保持5个并发;
- 新人验证:接口不会因并发过高崩溃,实现限流保护。
四、新人学习小贴士
- 先理解「进程≠线程」:进程是资源分配单位(独立内存),线程是调度单位(共享内存);
- 锁的核心目的:保证数据安全,而非提升效率,牺牲速度换安全;
- 练手顺序:先写单进程/线程 → 加锁保证安全 → 用进程池/信号量优化效率 → 结合业务场景落地;
- 调试技巧:打印进程/线程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()
执行过程
- 服务端启动:创建db目录(存账号)、video目录(存共享文件),运行服务端。
- 员工注册:客户端输入账号密码,服务端加密存储,返回注册成功。
- 员工登录:客户端输入账号密码,服务端校验,返回登录成功。
- 文件下载:客户端选择下载,服务端校验文件→分块传输→客户端保存。
- 多用户并发:socketserver多线程支持,多名员工同时登录下载不冲突。
### 应用场景2:个人文件共享工具
场景说明
个人搭建简易文件共享服务,家人/朋友注册登录后,下载你共享的电影、照片、文档。
核心优势
- 轻量:无需安装大型FTP软件,Python直接运行。
- 安全:密码MD5加密存储,不泄露明文密码。
- 便捷:支持多用户、大文件传输,解决黏包问题。
- 跨平台: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. 准备工作
- 新建文件夹
ftp_project - 按目录结构建好
db、video文件夹 - 在
video里放一个测试文件:ceshimovie.mp4(任意文件都行,名字一致)
2. 启动顺序
- 先运行 server.py(看到「FTP服务启动成功」)
- 再运行 client.py(弹出菜单)
3. 功能测试流程
- 注册:选 2 → 输入用户名、密码
- 登录:选 1 → 输入刚注册的账号
- 下载:登录成功自动进入下载菜单,选 1 开始下载
- 退出:选 2 退出
四、核心功能说明
- 多线程并发:支持多人同时登录/下载
- 密码加密:MD5加盐存储,不保存明文密码
- 黏包解决:struct 打包数据长度,大文件传输不损坏
- 自动创建目录:db、video、mydownload 自动创建
- 菜单操作:纯命令行菜单,简单易用
五、常见问题
- 端口被占用:服务端已加端口复用,重启即可
- 下载失败:检查 video 里是否有
ceshimovie.mp4 - 连接失败:确保服务端先启动,IP端口正确当前文件内容过长,豆包只阅读了前 61%。

浙公网安备 33010602011771号