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来远程连接服务器搭建网站,今年也算是明白原理了

参考资料

posted @ 2025-04-17 22:58  双沉默  阅读(50)  评论(0)    收藏  举报