You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TCP跨客户端文件传输遇ConnectionResetError及接收不全问题求助

问题描述

我的架构是先建立UDP连接,再通过TCP实现客户端间的文件传输——由TCP客户端向作为TCP服务端的另一客户端发起文件请求,完成文件发送与下载。目前发送端运行正常,但接收端存在问题:要么无法完整接收文件,要么抛出ConnectionResetError异常,有时文件延迟接收完成后仍触发该异常。需要适配任意大小文件传输,纠结接收端是否该用while循环,求解决方案。

报错信息
Exception in thread Thread-5 (file_receiving_thread):
Traceback (most recent call last):
  File "/opt/homebrew/Cellar/python@3.11/3.11.2_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/threading.py", line 1038, in _bootstrap_inner
    self.run()
  File "/opt/homebrew/Cellar/python@3.11/3.11.2_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/threading.py", line 975, in run
    self._target(*self._args, **self._kwargs)
  File "/App.py", line 205, in file_receiving_thread
    data = s.recvfrom(4096)
           ^^^^^^^^^^^^^^^^
ConnectionResetError: [Errno 54] Connection reset by peer
< Connection with client A closed. >
问题根源分析
  1. TCP套接字误用UDP方法:接收端用了recvfrom()(UDP专属方法)处理TCP连接,TCP应使用recv()。
  2. 发送端一次性读取大文件:f.read()会把整个大文件加载到内存,既低效又可能导致发送不完整,同时未处理TCP粘包/拆包问题。
  3. 连接关闭时机错误:发送端线程结束后立即关闭连接,此时接收端可能还在读取数据,直接触发ConnectionResetError;接收端无文件传输结束标识,会一直阻塞等待数据。
  4. 缺少完整性校验:未在传输前发送文件大小,接收端无法判断是否接收完整。
修复后的代码

接收端线程(修正TCP方法+按文件大小接收)

import time

def file_receiving_thread(s, fileName):
    print(f"< Downloading {fileName} ... >")
    
    # 先接收文件大小(8字节大端序表示64位整数)
    file_size_data = b''
    while len(file_size_data) < 8:
        chunk = s.recv(8 - len(file_size_data))
        if not chunk:
            print("< 连接中断,文件接收失败 >")
            return
        file_size_data += chunk
    file_size = int.from_bytes(file_size_data, byteorder='big')
    
    received_size = 0
    with open(f'downloaded_{fileName}', 'wb') as f:
        while received_size < file_size:
            # 每次接收不超过4096字节的块,适配大文件
            chunk_size = min(4096, file_size - received_size)
            data = s.recv(chunk_size)
            if not data:
                print("< 连接中断,文件接收不完整 >")
                return
            f.write(data)
            received_size += len(data)
    print(f'< {fileName} downloaded successfully! 已接收{received_size}/{file_size}字节 >')

发送端线程(分块发送+先传文件大小)

def file_sender_thread(tcp_conn, tcp_client_address, requested_file_name_decoded):
    global my_dir_path
    global myTbl

    # 检查文件是否存在
    file_path = os.path.join(my_dir_path, requested_file_name_decoded)
    absolute_file_path = os.path.abspath(file_path)
    if not os.path.isfile(absolute_file_path):
        print(f"< {requested_file_name_decoded} not found in directory {my_dir_path} >")
        tcp_conn.sendall(b'ERROR: File not found')
        tcp_conn.close()
        return

    try:
        # 获取并发送文件大小
        file_size = os.path.getsize(absolute_file_path)
        tcp_conn.sendall(file_size.to_bytes(8, byteorder='big'))
        
        with open(absolute_file_path, 'rb') as f:
            print(f'< Transferring {requested_file_name_decoded}... 总大小{file_size}字节 >')
            while True:
                # 分块读取文件,每次4096字节
                data = f.read(4096)
                if not data:
                    break
                # 确保所有数据发送完成
                tcp_conn.sendall(data)
        print(f'< {requested_file_name_decoded} transferred successfully! >')
        
    except Exception as e:
        print(f'传输错误: {e} ({tcp_client_address})')
        tcp_conn.sendall(b'ERROR: Transfer failed')
    finally:
        # 延迟关闭连接,给接收端足够时间读取最后一批数据
        time.sleep(0.5)
        tcp_conn.close()

TCP监听线程(修正TCP数据读取逻辑)

def tcp_listen_for_thread(my_tcp_port):
    tcp_socket = socket(AF_INET, SOCK_STREAM)
    tcp_socket.bind(('localhost', my_tcp_port))
    tcp_socket.listen()
    print(f"< TCP服务端已启动,监听端口{my_tcp_port} >")

    while True:
        tcp_conn, tcp_client_address = tcp_socket.accept()
        print(f"< Accepting connection request from {tcp_client_address} >")

        # TCP用recv()读取文件名,替代UDP的recvfrom()
        requested_file_name_data = b''
        chunk = tcp_conn.recv(4096)
        if chunk:
            requested_file_name_data += chunk
        requested_file_name_decoded = requested_file_name_data.decode().strip()

        # 启动发送线程,不阻塞监听逻辑
        file_to_send_thread = threading.Thread(target=file_sender_thread, args=(tcp_conn, tcp_client_address, requested_file_name_decoded,))
        file_to_send_thread.start()
        print(f'< 已为客户端{tcp_client_address}启动文件发送线程 >')

TCP请求线程(优化连接关闭逻辑)

def tcp_talk_thread(fileName, fileOwner, owners_tcp_IP, owners_tcp_port):
    try:
        with socket(AF_INET, SOCK_STREAM) as s:
            s.connect((owners_tcp_IP, owners_tcp_port))
            print(f"< Connection with client {fileOwner} established. >")
            # 发送文件名
            s.sendall(fileName.encode())
            # 启动接收线程并等待完成
            file_to_receive_thread = threading.Thread(target=file_receiving_thread, args=(s, fileName,))
            file_to_receive_thread.start()
            file_to_receive_thread.join()
        print(f'< Connection with client {fileOwner} closed. >')
    except Exception as e:
        print(f'< 连接或接收错误: {e} >')
关键修复点总结
  • TCP/UDP方法区分:TCP套接字统一使用recv()和sendall(),避免误用UDP的recvfrom()。
  • 分块传输:发送和接收都采用4096字节固定块,适配任意大小文件,避免内存溢出。
  • 文件大小预传输:发送前先传递文件大小,接收端可精确判断传输是否完成,避免无限循环。
  • 连接关闭优化:发送端传输完成后延迟关闭连接,确保接收端读取完所有数据。
  • 异常处理增强:增加传输过程中的异常捕获,及时反馈错误信息。

内容的提问来源于stack exchange,提问作者Oscar5

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 08:35:09