20242219 实验三《Python程序设计》实验报告

20242219 2025-2026-2 《Python程序设计》实验三报告

课程:《Python程序设计》
班级: 2422
姓名: 陈儒俊
学号:20242219
实验教师:王志强
实验日期:2026年4月27日
必修/选修: 公选课

1. 实验内容

1.1实验内容

创建服务端和客户端,服务端在特定端口监听多个客户请求。客户端和服务端通过Socket套接字(TCP/UDP)进行通信。

1.2实验要求

注意事项:

每人必须做一次客户端和一次服务端,且要和队友(标注学号姓名)互相通信。

要求1:

(1)创建服务端和客户端,选择一个通信端口,用Python语言编程实现通信演示程序;

(2)要求发送方输入内容,加密后并传输;接收方收到密文并解密和显示。要求:发方和收方同时输出明文和明文。

(3)程序代码托管到码云。

(4)添加文件操作,有加分。(可选项)

要求2:使用LLM生成一个带图形界面的程序

(1)分析关键代码的功能和使用方法

(2)分析生成程序的优点

(3)给出运行过程和结果截图

(4)程序代码托管到码云。

注:在华为ECS服务器(OpenOuler系统)和物理机(Windows/Linux系统)上使用VIM、PDB、IDLE、Pycharm等工具编程实现。

2. 实验过程及结果

学号 姓名 ip地址
20242218 段锦坤 192.168.43.18
20242219 陈儒俊 192.168.43.19

2.1 实验环境

开发工具:Pycharm、VS Code、Trae CN
操作系统:Windows 11
编程语言:Python 3.13.3
通信端口:8888(文件传输端口:8889)
核心依赖:socket、threading、tkinter、pycryptodome(AES加密)
测试环境:局域网(IP段:192.168.43.x)

2.2 核心程序代码展示

(1)服务端代码 server.py

import socket
import threading
import base64
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import hashlib

# AES加密密钥(客户端与服务端一致)
PASSWORD = "123456"

# 密钥派生:生成32位AES-256密钥
def derive_key(pwd):
    return hashlib.sha256(pwd.encode()).digest()

# 加密函数
def encrypt(text):
    key = derive_key(PASSWORD)
    cipher = AES.new(key, AES.MODE_CBC)
    encrypted = cipher.encrypt(pad(text.encode(), AES.block_size))
    return base64.b64encode(cipher.iv + encrypted).decode()

# 解密函数
def decrypt(text):
    try:
        key = derive_key(PASSWORD)
        data = base64.b64decode(text)
        iv = data[:16]
        encrypted = data[16:]
        cipher = AES.new(key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(encrypted), AES.block_size).decode()
    except:
        return "解密失败"

# 处理客户端连接(多线程)
def handle_client(client_socket, addr):
    print(f"[新连接] {addr} 已连接")
    while True:
        try:
            # 接收数据
            data = client_socket.recv(1024 * 1024).decode()
            if not data:
                break

            # 文件传输协议(分块传输,优化大文件)
            if data.startswith("FILE:"):
                info = data.split(":", 2)
                filename = info[1]
                filesize = int(info[2])
                print(f"[接收文件] {filename},大小:{filesize}字节")
                with open(f"接收_{filename}", "wb") as f:
                    received = 0
                    while received < filesize:
                        chunk = client_socket.recv(4096)
                        f.write(chunk)
                        received += len(chunk)
                print(f"[文件接收完成] 接收_{filename}")
                continue

            # 消息解密与显示
            plain = decrypt(data)
            print(f"\n[{addr} 密文] {data}")
            print(f"[{addr} 明文] {plain}")

            # 服务端回复
            reply = input("服务端回复:")
            cipher_reply = encrypt(reply)
            client_socket.send(cipher_reply.encode())
            print(f"[服务端 明文] {reply}")
            print(f"[服务端 密文] {cipher_reply}\n")
        except:
            break
    client_socket.close()
    print(f"[断开连接] {addr}")

# 主函数
def main():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(("0.0.0.0", 8888))
    server.listen(5)
    print("[服务启动] 监听端口8888,等待客户端连接...")
    while True:
        sock, addr = server.accept()
        thread = threading.Thread(target=handle_client, args=(sock, addr))
        thread.start()
        print(f"[活跃连接] {threading.active_count()-1}")

if __name__ == "__main__":
    main()

(2)客户端代码 client.py

import socket
import base64
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import hashlib

# AES加密密钥(与服务端保持一致)
PASSWORD = "123456"

def derive_key(pwd):
    return hashlib.sha256(pwd.encode()).digest()

def encrypt(text):
    key = derive_key(PASSWORD)
    cipher = AES.new(key, AES.MODE_CBC)
    encrypted = cipher.encrypt(pad(text.encode(), AES.block_size))
    return base64.b64encode(cipher.iv + encrypted).decode()

def decrypt(text):
    try:
        key = derive_key(PASSWORD)
        data = base64.b64decode(text)
        iv = data[:16]
        encrypted = data[16:]
        cipher = AES.new(key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(encrypted), AES.block_size).decode()
    except:
        return "解密失败"

# 大文件分块传输函数
def send_file(client):
    path = input("输入文件路径:")
    if not os.path.exists(path):
        print("文件不存在!")
        return
    filename = os.path.basename(path)
    filesize = os.path.getsize(path)
    # 发送文件信息
    client.send(f"FILE:{filename}:{filesize}".encode())
    # 分块发送文件
    with open(path, "rb") as f:
        while chunk := f.read(4096):
            client.send(chunk)
    print(f"[文件发送成功] {filename}")

# 主函数
def main():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ip = input("请输入服务端IP:")
    try:
        client.connect((ip, 8888))
        print("连接服务端成功!")
    except:
        print("连接失败!")
        return

    while True:
        print("\n1.发送消息 2.发送文件 3.退出")
        choice = input("请选择:")
        if choice == "1":
            msg = input("输入消息:")
            cipher = encrypt(msg)
            client.send(cipher.encode())
            print(f"[明文] {msg}")
            print(f"[密文] {cipher}")

            # 接收回复
            res = client.recv(1024).decode()
            plain_res = decrypt(res)
            print(f"[服务端密文] {res}")
            print(f"[服务端明文] {plain_res}")
        elif choice == "2":
            send_file(client)
        elif choice == "3":
            break
    client.close()

if __name__ == "__main__":
    main()

(3)图形界面程序 chat_gui.py

import socket
import threading
import tkinter as tk
from tkinter import scrolledtext, messagebox, filedialog
import base64
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import hashlib

# AES加密配置(与服务端/客户端一致)
PASSWORD = "123456"

def derive_key(pwd):
    return hashlib.sha256(pwd.encode()).digest()

def encrypt(text):
    key = derive_key(PASSWORD)
    cipher = AES.new(key, AES.MODE_CBC)
    encrypted = cipher.encrypt(pad(text.encode(), AES.block_size))
    return base64.b64encode(cipher.iv + encrypted).decode()

def decrypt(text):
    try:
        key = derive_key(PASSWORD)
        data = base64.b64decode(text)
        iv = data[:16]
        encrypted = data[16:]
        cipher = AES.new(key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(encrypted), AES.block_size).decode()
    except:
        return "解密失败"

# 图形界面主类
class ChatGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("AES加密Socket通信工具(GUI)")
        self.root.geometry("700x600")

        # 服务端/客户端模式选择
        self.mode = tk.StringVar(value="server")
        tk.Radiobutton(root, text="服务端模式", variable=self.mode, value="server").pack()
        tk.Radiobutton(root, text="客户端模式", variable=self.mode, value="client").pack()

        # 配置信息区域
        self.frame_config = tk.Frame(root)
        self.frame_config.pack(pady=5)
        tk.Label(self.frame_config, text="IP地址:").grid(row=0, column=0)
        self.ip_entry = tk.Entry(self.frame_config, width=15)
        self.ip_entry.insert(0, "0.0.0.0")
        self.ip_entry.grid(row=0, column=1, padx=2)
        tk.Label(self.frame_config, text="端口:").grid(row=0, column=2)
        self.port_entry = tk.Entry(self.frame_config, width=5)
        self.port_entry.insert(0, "8888")
        self.port_entry.grid(row=0, column=3, padx=2)
        tk.Label(self.frame_config, text="公共密钥:").grid(row=0, column=4)
        self.key_entry = tk.Entry(self.frame_config, width=8)
        self.key_entry.insert(0, "123456")
        self.key_entry.grid(row=0, column=5, padx=2)
        tk.Label(self.frame_config, text="昵称:").grid(row=0, column=6)
        self.name_entry = tk.Entry(self.frame_config, width=8)
        self.name_entry.insert(0, "用户")
        self.name_entry.grid(row=0, column=7, padx=2)

        # 文件存储配置
        self.frame_file = tk.Frame(root)
        self.frame_file.pack(pady=5)
        tk.Label(self.frame_file, text="接收目录:").grid(row=0, column=0)
        self.path_label = tk.Label(self.frame_file, text="C:\\Users\\Lenovo\\Desktop\\新建文件夹", fg="blue")
        self.path_label.grid(row=0, column=1, padx=2)
        self.select_btn = tk.Button(self.frame_file, text="选择目录", command=self.select_path)
        self.select_btn.grid(row=0, column=2, padx=2)

        # 连接状态
        self.status_label = tk.Label(root, text="未连接", fg="red")
        self.status_label.pack(pady=5)

        # 聊天记录区域
        self.text_area = scrolledtext.ScrolledText(root, width=80, height=25)
        self.text_area.pack(pady=10)

        # 消息输入区域
        self.frame_bottom = tk.Frame(root)
        self.frame_bottom.pack(pady=5)
        self.msg_entry = tk.Entry(self.frame_bottom, width=50)
        self.msg_entry.grid(row=0, column=0, padx=5)
        self.send_btn = tk.Button(self.frame_bottom, text="发送消息", command=self.send_msg, bg="green", fg="white")
        self.send_btn.grid(row=0, column=1, padx=2)
        self.file_btn = tk.Button(self.frame_bottom, text="发送文件", command=self.send_file, bg="blue", fg="white")
        self.file_btn.grid(row=0, column=2)

        # 控制按钮
        self.frame_control = tk.Frame(root)
        self.frame_control.pack(pady=5)
        self.stop_btn = tk.Button(self.frame_control, text="停止服务端/断开", command=self.stop_server, bg="red", fg="white")
        self.stop_btn.grid(row=0, column=0, padx=5)
        self.save_btn = tk.Button(self.frame_control, text="保存聊天", command=self.save_chat)
        self.save_btn.grid(row=0, column=1, padx=5)
        self.clear_btn = tk.Button(self.frame_control, text="清空", command=self.clear_chat)
        self.clear_btn.grid(row=0, column=2, padx=5)

        self.client_socket = None
        self.server_socket = None
        self.receive_thread = None
        self.save_path = "C:\\Users\\Lenovo\\Desktop\\新建文件夹"
        self.start_server()

    def select_path(self):
        path = filedialog.askdirectory()
        if path:
            self.save_path = path
            self.path_label.config(text=path)

    def start_server(self):
        if self.mode.get() == "server":
            self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.server_socket.bind((self.ip_entry.get(), int(self.port_entry.get())))
            self.server_socket.listen(5)
            self.status_label.config(text="监听中...", fg="orange")
            threading.Thread(target=self.accept_client, daemon=True).start()
        else:
            self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            try:
                self.client_socket.connect((self.ip_entry.get(), int(self.port_entry.get())))
                self.status_label.config(text=f"已连接: {self.name_entry.get()}", fg="green")
                threading.Thread(target=self.recv_msg, daemon=True).start()
                self.text_area.insert(tk.END, "[系统] 连接服务端成功!\n")
            except Exception as e:
                messagebox.showerror("错误", f"连接失败:{str(e)}")

    def accept_client(self):
        while True:
            self.client_socket, addr = self.server_socket.accept()
            self.status_label.config(text=f"已连接: {self.name_entry.get()}", fg="green")
            self.text_area.insert(tk.END, f"[系统] 客户端 {addr} 已连接\n")
            threading.Thread(target=self.recv_msg, daemon=True).start()
            break

    def recv_msg(self):
        while True:
            try:
                data = self.client_socket.recv(1024*1024).decode()
                if not data:
                    break
                # 文件传输处理
                if data.startswith("FILE:"):
                    info = data.split(":", 2)
                    filename = info[1]
                    filesize = int(info[2])
                    self.text_area.insert(tk.END, f"[文件] 正在接收:{filename}\n")
                    full_path = os.path.join(self.save_path, filename)
                    with open(full_path, "wb") as f:
                        received = 0
                        while received < filesize:
                            chunk = self.client_socket.recv(4096)
                            f.write(chunk)
                            received += len(chunk)
                    self.text_area.insert(tk.END, f"[文件] {filename} 接收完成,已保存至{full_path}\n")
                    continue
                # 消息解密显示
                plain = decrypt(data)
                self.text_area.insert(tk.END, f"[服务端/对方 密文] {data}\n")
                self.text_area.insert(tk.END, f"[服务端/对方 明文] {plain}\n\n")
            except Exception as e:
                self.text_area.insert(tk.END, f"[错误] 接收失败:{e}\n")
                break

    def send_msg(self):
        msg = self.msg_entry.get().strip()
        if not msg or not self.client_socket:
            messagebox.showwarning("提示", "请先连接服务端或输入消息")
            return
        cipher = encrypt(msg)
        self.client_socket.send(cipher.encode())
        self.text_area.insert(tk.END, f"[我 明文] {msg}\n")
        self.text_area.insert(tk.END, f"[我 密文] {cipher}\n\n")
        self.msg_entry.delete(0, tk.END)

    def send_file(self):
        if not self.client_socket:
            messagebox.showwarning("提示", "请先连接服务端")
            return
        path = filedialog.askopenfilename()
        if not path:
            return
        filename = os.path.basename(path)
        filesize = os.path.getsize(path)
        self.client_socket.send(f"FILE:{filename}:{filesize}".encode())
        with open(path, "rb") as f:
            while chunk := f.read(4096):
                self.client_socket.send(chunk)
        self.text_area.insert(tk.END, f"[文件] {filename} 发送成功!\n")

    def stop_server(self):
        if self.server_socket:
            self.server_socket.close()
        if self.client_socket:
            self.client_socket.close()
        self.status_label.config(text="已断开", fg="red")
        self.text_area.insert(tk.END, "[系统] 已断开连接\n")

    def save_chat(self):
        with open("chat_record.txt", "w", encoding="utf-8") as f:
            f.write(self.text_area.get("1.0", tk.END))
        messagebox.showinfo("提示", "聊天记录已保存!")

    def clear_chat(self):
        self.text_area.delete("1.0", tk.END)

if __name__ == "__main__":
    root = tk.Tk()
    ChatGUI(root)
    root.mainloop()

2.3 核心功能代码提取与讲解

2.3.1 AES对称加解密核心代码

# 1. 密钥派生:将密码转为32位AES-256密钥
def derive_key(pwd):
    return hashlib.sha256(pwd.encode()).digest()

# 2. 加密函数:AES-CBC模式 + 填充 + Base64编码
def encrypt(text):
    key = derive_key(PASSWORD)
    cipher = AES.new(key, AES.MODE_CBC)
    encrypted = cipher.encrypt(pad(text.encode(), AES.block_size))
    return base64.b64encode(cipher.iv + encrypted).decode()

# 3. 解密函数:拆分IV + 解密 + 解填充
def decrypt(text):
    try:
        key = derive_key(PASSWORD)
        data = base64.b64decode(text)
        iv = data[:16]
        encrypted = data[16:]
        cipher = AES.new(key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(encrypted), AES.block_size).decode()
    except:
        return "解密失败"
代码讲解
  1. 密钥派生:用SHA256将自定义密码转为固定32字节密钥,满足AES-256要求;
  2. 加密流程:随机生成IV → 明文PKCS7填充 → AES-CBC加密 → IV+密文合并Base64编码;
  3. 解密流程:Base64解码 → 拆分IV与密文 → AES解密 → 解填充还原明文;
  4. 异常捕获:防止密钥不匹配、数据损坏导致程序崩溃。

2.3.2 文件传输协议核心代码

(1)文件发送函数(send_file)
def send_file(self):
    if not self.client_socket:
        messagebox.showwarning("提示", "请先连接服务端")
        return
    # 选择本地文件
    path = filedialog.askopenfilename()
    if not path:
        return
    # 获取文件信息
    filename = os.path.basename(path)
    filesize = os.path.getsize(path)
    # 发送自定义协议头:FILE:文件名:文件大小
    self.client_socket.send(f"FILE:{filename}:{filesize}".encode())
    # 分块传输文件(4096字节/块,优化大文件传输)
    with open(path, "rb") as f:
        while chunk := f.read(4096):
            self.client_socket.send(chunk)
(2)文件解析与接收(recv_msg)
if data.startswith("FILE:"):
    # 解析协议头
    info = data.split(":", 2)
    filename = info[1]
    filesize = int(info[2])
    # 拼接保存路径
    full_path = os.path.join(self.save_path, filename)
    # 分块接收并写入文件
    with open(full_path, "wb") as f:
        received = 0
        while received < filesize:
            chunk = self.client_socket.recv(4096)
            f.write(chunk)
            received += len(chunk)
代码讲解
  1. 传输协议:用FILE:文件名:文件大小作为头标识,区分消息与文件;
  2. 分块传输:每次仅收发4096字节,解决大文件一次性传输卡顿、中断问题;
  3. 流式读写:二进制模式读写文件,保证文件完整性,支持文档/图片等格式。

2.3.3 核心函数功能一览表

函数名 所属模块 功能简要概括
derive_key 加解密 将密码哈希为AES-256可用的32字节密钥
encrypt 加解密 明文→AES-CBC加密→Base64字符串,用于网络传输
decrypt 加解密 接收密文→解码解密→还原明文,异常时返回错误
send_file 文件传输 图形化选择文件,按协议分块发送大文件
recv_msg 文件接收 解析文件协议,分块接收并保存文件到指定目录

2.4 命令行版程序运行结果(server.py/client.py)

屏幕截图 2026-05-07 183504

屏幕截图 2026-05-07 183504

  1. 连接与通信测试
    • 服务端启动后监听0.0.0.0:8888,客户端输入服务端IP192.168.43.18成功连接,日志显示[*] 已连接到服务端 192.168.43.18:8888
    • 客户端发送明文消息你好,我是20242219陈儒俊,程序自动生成AES加密密文,控制台同步展示明文与密文;
    • 服务端接收密文后自动解密,双向通信正常。
  2. 加密功能验证
    所有消息均采用AES-256-CBC模式加密,密钥123456派生有效,解密无乱码,符合实验要求。

2.5 GUI版程序运行与队友通信测试(chat_gui.py)

屏幕截图 2026-05-02 224847
屏幕截图 2026-05-02 224914
屏幕截图 2026-05-02 225228
屏幕截图 2026-05-02 225238
屏幕截图 2026-05-02 225445
屏幕截图 2026-05-02 225450

  1. 基础通信测试
    服务端配置0.0.0.0:8888、昵称20242219陈儒俊,队友(20242218段锦坤)以客户端连接,双方收发消息,界面同步展示明文与密文,解密正常。
  2. 文件传输功能测试
    测试文件第6讲 函数-上课版.doc(约68KB),采用分块传输,接收目录C:\Users\Lenovo\Desktop\新建文件夹,文件传输流畅、无损坏,验证大文件优化方案有效。
  3. 跨模式兼容测试
    GUI客户端可与命令行服务端互通,GUI服务端可接收命令行客户端连接,加解密与文件传输完全兼容,通信稳定。

3. 实验过程中遇到的问题和解决过程

  • 问题1:使用AES对称加密实现加解密功能时,程序直接报错无法运行
  • 问题1解决方案:缺少AES加密依赖库,执行pip install pycryptodome安装后,加解密功能正常生效。
  • 问题2:传输较大文件时速度极慢、卡顿甚至中断
  • 问题2解决方案:与队友合作,通过AI优化文件传输协议,改为4096字节分块传输,规范FILE:文件名:文件大小传输格式,解决大文件传输效率低的问题。

4. 图形界面程序(chat_gui.py)分析

4.1 关键代码功能

  1. Socket通信代码:实现TCP服务端监听/客户端连接、消息收发,支持多客户端;
  2. AES加解密代码:基于pycryptodome实现AES-256-CBC加密、密钥派生、数据填充/解填充;
  3. Tkinter界面代码:构建配置区、聊天区、输入区、控制按钮,可视化操作;
  4. 多线程代码:网络任务独立线程运行,避免界面卡死;
  5. 文件传输代码:文件选择、分块传输、路径配置、状态提示;
  6. 日志功能:明文/密文展示、聊天记录保存。

4.2 运行过程和结果

  1. 启动程序,选择模式并配置IP、端口、密钥、昵称;
  2. 服务端监听,客户端连接,状态显示“已连接”;
  3. 发送消息,界面同步展示明文/密文,接收方自动解密;
  4. 发送文件,分块传输并保存到指定目录,状态提示完整;
  5. 全程无报错、无乱码,与队友双向通信测试顺利完成。

其他(感悟、思考等)

  1. 掌握Python Socket网络编程与AES对称加密,理解TCP通信全流程;
  2. 解决加密库缺失、大文件传输问题,提升问题排查与解决能力;
  3. 团队协作完成双向通信测试,加深对客户端-服务端交互的理解;
  4. 学习GUI与网络编程结合,掌握多线程、tkinter界面设计;

代码托管

码云仓库地址:https://gitee.com/Mr-Men_door/code

posted @ 2026-05-07 19:55  这是昵称。。。  阅读(12)  评论(0)    收藏  举报