20242203许振宇 2024-2025-2 《Python程序设计》实验三报告
20242203 2024-2025-2 《Python程序设计》实验三报告
课程:《Python程序设计》
班级: 2422
姓名: 许振宇
学号:20242203
实验教师:王志强
实验日期:2024年4月16日
必修/选修: 公选课
1.实验内容
创建服务端和客户端,服务端在特定端口监听多个客户请求。客户端和服务端通过Socket套接字(TCP/UDP)进行通信。
具体要求:
(1)创建服务端和客户端,选择一个通信端口,用Python语言编程实现通信演示程序;
(2)要求包含文件的基本操作,例如打开和读写操作。
(3)要求发送方从文件读取内容,加密后并传输;接收方收到密文并解密,保存在文件中。
(4)程序代码托管到码云。
2. 实验过程及结果
实验代码及注释
注:代码根据课上所学手敲了一部分,但本人能力有限不是很懂也运行不了,最终通过deepseek辅助完成代码编译并运行通过,注释部分为自己借助deepseek理解后补充。
1.服务端代码server.py
# ---------- 服务端代码 server.py ----------
"""
安全文件传输服务器
功能:接收客户端加密文件,验证文件类型,解密存储
使用说明:需预先配置加密密钥,客户端需按协议格式发送数据
"""
import socket
import threading
import os
from cryptography.fernet import Fernet # 导入Fernet对称加密模块
#密钥管理模块------------------------------------------------------------
def load_key():
"""
加载或生成加密密钥
安全策略:
- 密钥文件不存在时自动生成新密钥(首次运行)
- 密钥存储为二进制文件,需保证文件系统安全
- 相同密钥才能保证加解密成功,需妥善保管密钥文件
"""
key_file = "secret.key"
# 密钥不存在时生成新密钥(首次运行)
if not os.path.exists(key_file):
key = Fernet.generate_key() # 生成256位(32字节)安全密钥
with open(key_file, "wb") as f:
f.write(key)
print(f"[*] 已生成新密钥文件 {key_file}")
# 读取现有密钥(后续运行)
with open(key_file, "rb") as f:
return f.read() # 返回bytes类型的密钥
# 初始化加密模块 ------------------------------------------------------------
key = load_key() # 加载密钥(首次运行会生成)
fernet = Fernet(key) # 创建Fernet加密器实例
#客户端处理函数
def handle_client(client_socket):
"""
处理客户端连接的完整生命周期
协议规范:
1. 客户端首先发送4字节文件名长度(大端序)
2. 接着发送实际文件名
3. 发送4字节加密数据长度(大端序)
4. 最后发送加密数据
安全机制:
- 文件名规范化防止路径遍历攻击
- 文件扩展名白名单验证
- 数据长度头校验防止DoS攻击
"""
try:
#阶段1:接收文件名 --------------------------------------------------
# 读取文件名长度头(固定4字节)
name_header = client_socket.recv(4)
if not name_header: # 客户端提前断开
print("[!] 客户端断开连接")
return
# 将4字节转换为整数(大端序,网络标准字节序)
name_len = int.from_bytes(name_header, byteorder='big')
# 分块接收文件名数据(防止大文件导致内存溢出)
filename = b''
while len(filename) < name_len:
# 计算剩余需要接收的字节数
remaining = name_len - len(filename)
# 每次最多接收4096字节(平衡效率和内存使用)
chunk = client_socket.recv(min(4096, remaining))
if not chunk: # 连接意外中断
raise ConnectionError("文件传输中断")
filename += chunk
#文件名安全处理 ----------------------------------------------------
# 解码为字符串并规范化路径(防止../等路径遍历攻击)
save_name = os.path.basename(filename.decode('utf-8')).strip()
# 空文件名检查
if not save_name:
raise ValueError("无效文件名")
# 文件扩展名白名单验证
ALLOWED_EXT = ['.txt', '.jpg', '.xlsx', '.pdf'] # 可根据需求扩展
if not any(save_name.lower().endswith(ext) for ext in ALLOWED_EXT):
raise ValueError(f"禁止的文件类型: {save_name}")
print(f"[*] 客户端请求保存为: {save_name}") #按照用户要求的文件名保存文件
#阶段2:接收加密数据 ------------------------------------------------
# 读取数据长度头(同样使用4字节大端序)
data_header = client_socket.recv(4)
if not data_header:
raise ConnectionError("数据头接收失败")
data_len = int.from_bytes(data_header, byteorder='big')
# 分块接收加密数据(避免一次性接收大文件导致内存耗尽)
encrypted = b''
while len(encrypted) < data_len:
# 动态计算每次接收的块大小
chunk_size = min(4096, data_len - len(encrypted))
chunk = client_socket.recv(chunk_size)
if not chunk:
raise ConnectionError("数据接收中断")
encrypted += chunk
#阶段3:解密存储 ---------------------------------------------------
# 文件存在性检查(实际生产环境应考虑版本管理)
if os.path.exists(save_name):
print(f"[!] 文件 {save_name} 已存在,将被覆盖")
# 使用Fernet解密数据(自动验证完整性)
decrypted = fernet.decrypt(encrypted)
# 二进制写入文件(保持原始数据格式)
with open(save_name, "wb") as f:
f.write(decrypted)
print(f"[✓] 成功保存 {len(decrypted)} 字节到 {save_name}")
except Exception as e:
# 统一异常处理(记录错误类型和具体信息)
print(f"[!] 处理错误: {type(e).__name__}: {e}")
finally:
# 确保关闭客户端套接字(释放资源)
client_socket.close()
#服务器主程序 ---------------------------------------------------
def start_server():
"""
启动文件传输服务器
网络配置:
- 监听所有网络接口(0.0.0.0)
- 使用TCP协议,端口8080
并发处理:
- 每个客户端连接创建独立线程处理
- 支持同时处理多个客户端请求
"""
host = '127.0.0.1' # 生产环境建议使用具体IP
port = 8080
# 创建TCP套接字(AF_INET: IPv4, SOCK_STREAM: TCP)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口重用选项(快速重启服务器)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
# 绑定地址和端口
server.bind((host, port))
# 设置最大挂起连接数(backlog)
server.listen(5)
print(f"[*] 服务端启动在 {host}:{port}")
print(f"[*] 等待客户端连接...")
# 主循环持续接受新连接
while True:
# 接受客户端连接(阻塞等待)
client, addr = server.accept()
print(f"[+] 接受来自 {addr[0]}:{addr[1]} 的连接")
# 创建新线程处理客户端(实现并发)
handler = threading.Thread(target=handle_client, args=(client,))
handler.start()
except KeyboardInterrupt:
# 处理Ctrl+C信号(优雅关闭)
print("\n[!] 服务器关闭中...")
finally:
# 确保关闭服务器套接字
server.close()
if __name__ == "__main__":
# 启动服务器(程序入口)
start_server()
2.客户端代码client.py
# ---------- 客户端代码 client.py ----------
"""
安全文件传输客户端
功能:加密本地文件并按自定义名称发送到服务器
协议规范:
1. 发送4字节文件名长度(大端序网络字节序)
2. 发送实际文件名字节数据
3. 发送4字节加密数据长度(大端序)
4. 发送加密文件内容
"""
import socket
from cryptography.fernet import Fernet # 导入对称加密模块
# region 初始化加密模块
# 加载预共享密钥(需与服务器保持一致)
try:
with open("secret.key", "rb") as key_file:
key = key_file.read() # 读取256位(32字节)密钥
except FileNotFoundError:
print("\033[31m错误:未找到密钥文件 secret.key\033[0m")
exit(1)
fernet = Fernet(key) # 创建Fernet加密器实例
# endregion
def send_file(source_path, save_name, host, port):
"""
文件加密传输主函数
参数:
- source_path: 本地待发送文件路径
- save_name: 服务器保存的文件名
- host: 服务器IP地址
- port: 服务器端口号
安全特性:
- 使用AES-CBC加密模式(Fernet默认)
- 数据完整性验证(HMAC签名)
"""
client = None # 初始化socket对象
try:
# region 文件读取与加密
try:
# 二进制模式读取源文件
with open(source_path, "rb") as f:
file_data = f.read() # 注意:大文件可能消耗较多内存
except FileNotFoundError:
print(f"\033[31m错误:源文件 {source_path} 未找到\033[0m")
return
# 执行加密(自动生成IV,包含在加密数据中)
encrypted_data = fernet.encrypt(file_data)
# endregion
# region 网络连接建立
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
client.connect((host, port)) # 建立TCP连接
# endregion
# region 文件名传输协议
# 将文件名编码为UTF-8字节(限制字符集)
save_name_bytes = save_name.encode('utf-8')
# 发送文件名长度头(4字节大端序)
# 协议设计说明:固定长度头便于服务器预分配内存
client.send(len(save_name_bytes).to_bytes(4, byteorder='big'))
# 发送实际文件名内容(确保网络字节流传输)
client.sendall(save_name_bytes) # sendall保证完整发送
# endregion
# region 加密数据传输协议
# 发送数据长度头(4字节大端序)
# 安全考虑:防止服务器被大文件DoS攻击
client.send(len(encrypted_data).to_bytes(4, byteorder='big'))
# 分块发送加密数据(实际sendall会处理分块)
client.sendall(encrypted_data) # 自动处理TCP分包问题
print(f"\033[32m[✓] {len(encrypted_data)}字节数据已加密发送\033[0m")
# endregion
except Exception as e:
# 统一异常处理(包括网络错误、编码错误等)
print(f"\033[31m错误:{str(e)}\033[0m")
finally:
# 确保关闭网络连接(重要!)
if client:
client.close()
if __name__ == "__main__":
# 服务器配置(生产环境应通过配置文件设置)
server_ip = "127.0.0.1" # 本地环回地址
server_port = 8080 # 需与服务器端口一致
# region 用户交互界面
# 获取源文件名(带输入验证)
while True:
src_file = input("请输入处理文件的文件名:").strip()
if src_file:
break
print("\033[33m文件名不能为空\033[0m")
# 获取目标文件名(带基本验证)
while True:
dst_file = input("请输入解密文件名称:").strip()
if dst_file:
break
print("\033[33m文件名不能为空\033[0m")
# endregion
# 启动文件传输
send_file(src_file, dst_file, server_ip, server_port)
实验运行过程
1.打开PyCharm的Terminal(Alt+F12)
2.执行密钥生成命令:
python -c "from cryptography.fernet import Fernet; key = Fernet.generate_key(); open('secret.key', 'wb').write(key)"
项目目录出现secret.key文件,打开可见如图所示的密钥

3.在当前目录创建测试文件test.txt,随意输入内容
4.运行server.py,出现
[*] 服务端启动在 127.0.0.1:8080
[*] 等待客户端连接...
证明服务器启动成功
5.运行client.py,出现
[+] 接受来自 127.0.0.1:58530 的连接
连接成功
6.提示输入待处理文件名和解密文件名
7.输入完成后,目录出现解密文件名,同时
client.py端命令框出现
[✓] 228字节数据已加密发送
server.py端命令框出现
[*] 客户端请求保存为: hello.txt
[✓] 成功保存 108 字节到 hello.txt
操作成功
实验结果
测试文件和解密文件内容一致


服务端处理结果

客户端发送结果

上传gitee
1.用管理员模式打开pycharm,浏览器登录gitee
2.右键程序添加,修正,提交并推送

3. 实验过程中遇到的问题和解决过程
-
问题1:第一次运行时出现错误PermissionError [WinError 10013] 权限不足
![]()
-
问题1解决方案:
通过询问deepseek得知此端口权限不足,可能是Windows防火墙/杀毒软件阻止端口访问,需要以管理员权限运行pycharm
以管理员权限运行后依然报错,于是我采用了第二个办法,换用特权端口:
port = 8080 # 8000-65535之间的端口为特权端口
-
问题2:在我第二次使用server.py时,出现OSError [WinError 10048] 端口占用
![]()
-
问题2解决方案:ai推荐我在命令行输入
netstat -ano | findstr :12345 #监听端口状态
taskkill /PID <12345> /F #终止端口占用
- 问题3:找不到朋友互相测试()
- 问题3解决方案:自己给自己发送
其他(感悟、思考等)
去年的c语言课我就学习了用mobaxterm来远程连接服务器搭建网站,今年也算是明白原理了


浙公网安备 33010602011771号