TCP服务端/客户端文件传输异常:需保持连接完成传输
解决TCP文件传输无需关闭连接的问题
你的核心问题在于TCP是流式协议——两端没有内置的“数据结束”标记,所以当前代码里的接收循环会一直阻塞在recv()调用上,直到连接被关闭(手动中断客户端或调用shutdown)才会退出。要保持连接复用,我们需要给传输流程加上明确的“边界标记”,最可靠的方式就是先传输文件的长度信息,让接收方知道要接收多少字节后就停止循环。
下面是修正后的代码实现,同时处理了文件不存在的异常情况:
1. 服务端代码(server.py)修改
首先记得导入struct模块,用来打包/解包文件长度(保证跨平台兼容性):
import struct # 原类中的相关方法修改 elif msg[:4] == 'file': client_command = msg[5:9] if client_command == 'GET ': file_name = msg[9:] try: with open(file_name, "rb") as f: # 先获取文件总大小 f.seek(0, 2) file_size = f.tell() f.seek(0, 0) # 把文件大小打包成4字节的网络字节序(大端)发送给客户端 self.client_socket.sendall(struct.pack('!I', file_size)) # 分块发送文件内容 l = f.read(self.BUFF_SIZE) while l: self.send_info(l) l = f.read(self.BUFF_SIZE) except FileNotFoundError: # 文件不存在时发送0作为标记 self.client_socket.sendall(struct.pack('!I', 0)) elif client_command == 'SEND': file_name = msg[10:] # 先接收4字节的文件大小 size_data = self.recv_info(4) if not size_data: return file_size = struct.unpack('!I', size_data)[0] if file_size == 0: print(f"客户端发送的文件不存在: {file_name}") return received_size = 0 with open(file_name, "wb") as f: # 循环接收直到达到文件总大小 while received_size < file_size: # 计算剩余需接收的字节数,避免最后一次接收冗余数据 remaining = file_size - received_size l = self.recv_info(min(remaining, self.BUFF_SIZE)) if not l: break f.write(l) received_size += len(l) # 修改recv_info方法,支持指定接收长度 def recv_info(self, buf_size=None): if buf_size is None: buf_size = self.BUFF_SIZE recv = self.client_socket.recv(buf_size) return recv def send_info(self, msg): info = bytes(msg) send = self.client_socket.sendall(info) return send
2. 客户端代码(client.py)修改
同样导入struct模块,同步修改传输逻辑:
import struct # 处理file命令的部分修改 answer = input() elif answer[:4] == 'file': s.send(answer.encode('iso-8859-1')) command = answer[5:9] if command == 'GET ': fileName = answer[9:] # 先接收4字节的文件大小 size_data = s.recv(4) if not size_data: print("连接已断开") return file_size = struct.unpack('!I', size_data)[0] if file_size == 0: print("服务端找不到该文件") return received_size = 0 with open(fileName, 'wb') as f: while received_size < file_size: remaining = file_size - received_size l = s.recv(min(remaining, 2048)) if not l: break f.write(l) received_size += len(l) print(f"文件 {fileName} 接收完成") elif command == 'SEND': fileName = answer[10:] try: with open(fileName, 'rb') as f: # 获取文件大小 f.seek(0, 2) file_size = f.tell() f.seek(0, 0) # 发送文件大小 s.sendall(struct.pack('!I', file_size)) # 分块发送文件内容 l = f.read(2048) while l: s.send(l) l = f.read(2048) print(f"文件 {fileName} 发送完成") except FileNotFoundError: # 文件不存在时发送0标记 s.sendall(struct.pack('!I', 0)) print(f"找不到文件: {fileName}") # 这里不再需要调用shutdown(SHUT_WR)了!
关键逻辑解释
- 文件长度前置传输:发送方先把文件大小打包成固定4字节的二进制数据发送,接收方解析后就明确知道需要接收多少字节,不会无限阻塞在
recv()上。 - 网络字节序兼容:使用
struct.pack('!I', ...)保证大小端格式统一,避免不同操作系统之间的解析错误。 - 剩余字节数计算:最后一次接收时只取剩余需要的字节数,防止接收多余数据影响后续命令的处理。
- 异常处理:增加了文件不存在的判断,避免一方因等待不存在的数据而阻塞。
后续优化文件名处理时,建议用分隔符(比如file GET|test.txt)替代切片,这样通过split('|')拆分命令和文件名会更可靠,不会因为文件名长度变化导致切片错误。
内容的提问来源于stack exchange,提问作者SN33DS




