""" 校园内网 FTP 文件传输实训——客户端 ====================================== 运行位置:学生机,或者小组中担任“客户端”的电脑。 程序会自动完成: 1. 输入教师机IPv4地址和本组编号; 2. 选择一个现有文件,或自动生成测试文本; 3. 连接教师机2121端口; 4. 使用student / 123456登录; 5. 把文件上传到教师机ftp_share目录; 6. 列出服务器上的文件; 7. 将刚才上传的文件重新下载; 8. 比较上传前后文件的SHA-256摘要。 运行命令: python ftp_client.py 本客户端只使用Python标准库,不需要安装pyftpdlib。 """ import re from ftplib import FTP, all_errors from hashlib import sha256 from pathlib import Path # 必须与ftp_server.py中的PORT保持一致。 PORT = 2121 USERNAME = "student" PASSWORD = "123456" # FTP调试级别: # 0表示不显示协议细节;1显示部分信息;2显示完整FTP命令与响应。 # 教师讲解USER、PASS、STOR、RETR时,可以临时改为2。 DEBUG_LEVEL = 0 def normalize_group_id(raw_group_id: str) -> str: """把组号转换成适合放入远程文件名的安全字符串。""" # 推荐输入G01、G02等。如果直接回车,则使用G00。 group_id = raw_group_id.strip() or "G00" # 文件名只保留英文字母、数字、下划线和短横线;其余字符替换成下划线。 group_id = re.sub(r"[^0-9A-Za-z_-]", "_", group_id) return group_id[:20] def choose_upload_file(group_id: str) -> Path: """让学生选择文件;直接回车时自动生成一个测试文本。""" raw_path = input("要上传的文件路径(直接回车则生成测试文件):").strip() if raw_path: # 允许学生从Windows文件资源管理器复制带引号的路径。 upload_file = Path(raw_path.strip('"')).expanduser() if not upload_file.is_file(): raise FileNotFoundError(f"找不到要上传的文件:{upload_file}") return upload_file # 学生不提供文件时,在脚本旁边生成组号唯一的测试文件。 upload_file = Path(__file__).with_name(f"{group_id}_upload.txt") upload_file.write_text( f"{group_id}:来自客户端的一条智能家居实训记录。\n", encoding="utf-8", ) print(f"[CREATE] 已生成测试文件:{upload_file}") return upload_file def file_sha256(path: Path) -> str: """分块计算文件的SHA-256,避免大文件一次全部读入内存。""" digest = sha256() with path.open("rb") as source: while True: chunk = source.read(64 * 1024) if not chunk: break digest.update(chunk) return digest.hexdigest() def main() -> None: """完成FTP连接、上传、列目录、下载和摘要校验。""" server_ip = input("FTP服务器IPv4地址:").strip() if not server_ip: print("[ERROR] 服务器IP不能为空。") return group_id = normalize_group_id(input("请输入组号,例如G01:")) try: upload_file = choose_upload_file(group_id) # 远程文件名前添加组号,避免全班上传同名文件互相覆盖。 # 自动生成的测试文件已经带组号,因此不再重复添加。 prefix = f"{group_id}_" remote_name = ( upload_file.name if upload_file.name.startswith(prefix) else f"{prefix}{upload_file.name}" ) # 下载文件保存在本脚本旁边,并使用downloaded_前缀与原文件区分。 download_file = Path(__file__).with_name(f"downloaded_{remote_name}") # with语句结束时会自动关闭FTP连接。 with FTP() as ftp: ftp.set_debuglevel(DEBUG_LEVEL) print(f"[CONNECT] 正在连接 {server_ip}:{PORT} ……") ftp.connect(server_ip, PORT, timeout=10) print(f"[LOGIN] 正在使用账号 {USERNAME} 登录……") ftp.login(USERNAME, PASSWORD) # 被动模式下,由客户端主动连接服务器的数据端口, # 通常比主动模式更适合有防火墙的校园网络。 ftp.set_pasv(True) # STOR是FTP上传命令;rb表示以二进制只读方式打开本地文件。 with upload_file.open("rb") as source: ftp.storbinary(f"STOR {remote_name}", source) print(f"[UPLOAD] 上传成功:{upload_file.name} -> {remote_name}") # NLST用于获取远程目录中的文件名列表。 remote_files = ftp.nlst() print("[REMOTE] 教师机当前文件列表:") for name in remote_files: print(" ", name) # RETR是FTP下载命令;wb表示以二进制写入方式创建本地文件。 with download_file.open("wb") as target: ftp.retrbinary(f"RETR {remote_name}", target.write) print(f"[DOWNLOAD] 下载成功:{remote_name} -> {download_file.name}") # 对比原文件和下载文件的SHA-256,验证内容是否完全一致。 original_hash = file_sha256(upload_file) downloaded_hash = file_sha256(download_file) same = original_hash == downloaded_hash print(f"[SHA256] 原文件:{original_hash}") print(f"[SHA256] 下载件:{downloaded_hash}") print(f"[VERIFY] SHA-256一致:{same}") if not same: print("[WARNING] 文件内容不一致,请检查是否发生覆盖或传输异常。") except FileNotFoundError as exc: print(f"[ERROR] {exc}") except all_errors as exc: # all_errors包括FTP协议错误、网络错误、超时和连接关闭等异常。 print(f"[ERROR] FTP连接或传输失败:{type(exc).__name__}: {exc}") print(" 请查看《端口检查与排错说明.txt》逐层检查。") if __name__ == "__main__": main()