杰瑞科技汇

Python socket如何高效传输文件?

基本原理

通过 Socket 传输文件,核心思想是:

Python socket如何高效传输文件?-图1
(图片来源网络,侵删)
  1. 建立连接:客户端和服务器通过 socket 建立一个 TCP 连接。
  2. 发送元数据:客户端在发送文件内容之前,先发送一些“元数据”(Metadata),例如文件名、文件大小,这样服务器在接收时可以知道将要接收多大的文件,并创建一个正确大小的空文件,然后填充内容。
  3. 发送文件内容:客户端以二进制模式('rb')打开文件,读取文件内容,并通过 socket 发送。
  4. 接收并写入文件:服务器接收文件名和文件大小信息,然后以二进制模式('wb')创建一个新文件,循环接收来自 socket 的数据块,并将其写入文件,直到接收到的总字节数等于文件大小。

关键点:为什么需要发送文件大小?

如果不发送文件大小,服务器将不知道何时停止接收数据。socket.recv() 方法在没有数据时会阻塞,如果文件发送完毕,服务器会一直等待下去,导致程序卡死,发送文件大小是解决这个问题的关键。


代码实现

我们将创建两个独立的 Python 脚本:

  1. server.py:服务器端脚本,用于接收文件。
  2. client.py:客户端脚本,用于发送文件。

服务器端代码 (server.py)

服务器会监听一个端口,等待客户端的连接,一旦连接建立,它会先接收文件名和文件大小,然后开始接收文件内容。

# server.py
import socket
# 服务器配置
HOST = '127.0.0.1'  # 本地回环地址,代表本机
PORT = 65432        # 选择一个未被占用的端口
# 创建一个 TCP socket (socket.AF_INET 表示 IPv4, socket.SOCK_STREAM 表示 TCP)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()  # 开始监听传入的连接
    print(f"服务器正在监听 {HOST}:{PORT}...")
    # accept() 会阻塞,直到有新的连接进来
    # 它返回一个新的 socket 对象 conn,用于与客户端通信
    # 以及客户端的地址 (address)
    conn, addr = s.accept()
    with conn:
        print(f"已连接到 {addr}")
        # 1. 先接收文件名 (假设文件名长度不超过 1024 字节)
        filename = conn.recv(1024).decode('utf-8')
        print(f"正在接收文件: {filename}")
        # 2. 再接收文件大小 (假设文件大小可以用一个整数表示)
        # 我们需要先接收文件大小,以便知道要读取多少字节
        file_size_bytes = conn.recv(8) # 8字节足够存储一个大的整数
        file_size = int.from_bytes(file_size_bytes, 'big')
        print(f"文件大小: {file_size} 字节")
        # 3. 接收文件内容
        received_size = 0
        with open(filename, 'wb') as f:
            while received_size < file_size:
                # 每次最多接收 4096 字节
                data = conn.recv(4096)
                if not data:
                    # 如果没有数据,说明连接已关闭
                    break
                f.write(data)
                received_size += len(data)
                # 打印接收进度
                print(f"已接收: {received_size}/{file_size} 字节", end='\r')
        print("\n文件接收完成!")

客户端代码 (client.py)

客户端连接到服务器,然后发送要传输的文件。

Python socket如何高效传输文件?-图2
(图片来源网络,侵删)
# client.py
import socket
# 服务器配置
HOST = '127.0.0.1'  # 必须与服务器地址一致
PORT = 65432        # 必须与服务器端口一致
# 要发送的文件路径
file_to_send = 'my_file.txt' # 确保这个文件存在于客户端的同一目录下
try:
    # 创建一个 TCP socket
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((HOST, PORT))
        print(f"已连接到服务器 {HOST}:{PORT}")
        # 1. 先发送文件名
        s.sendall(file_to_send.encode('utf-8'))
        # 2. 再发送文件大小
        # 获取文件大小
        file_size = 0
        try:
            with open(file_to_send, 'rb') as f:
                f.seek(0, 2)  # 移动到文件末尾
                file_size = f.tell()  # 获取当前位置,即文件大小
                f.seek(0)  # 回到文件开头
        except FileNotFoundError:
            print(f"错误: 文件 '{file_to_send}' 未找到。")
            exit()
        # 将整数转换为字节并发送
        s.sendall(file_size.to_bytes(8, 'big')) # 使用8字节,确保能表示大文件
        # 3. 发送文件内容
        with open(file_to_send, 'rb') as f:
            while True:
                # 每次读取 4096 字节
                chunk = f.read(4096)
                if not chunk:
                    # 文件读取完毕
                    break
                s.sendall(chunk)
        print(f"文件 '{file_to_send}' 发送完成。")
except ConnectionRefusedError:
    print(f"错误: 无法连接到 {HOST}:{PORT},请确保服务器正在运行。")
except FileNotFoundError:
    print(f"错误: 文件 '{file_to_send}' 未找到。")

如何运行

  1. 准备文件:在你的工作目录下创建一个名为 my_file.txt 的文件,并写入一些内容,写入 "Hello, this is a test file for socket transfer!"。

  2. 启动服务器: 打开一个终端,运行服务器脚本。

    python server.py

    你会看到输出:

    服务器正在监听 127.0.0.1:65432...
  3. 启动客户端: 打开另一个终端,运行客户端脚本。

    python client.py

    你会看到客户端的输出:

    已连接到服务器 127.0.0.1:65432
    文件 'my_file.txt' 发送完成。
  4. 查看服务器输出: 回到服务器的终端,你会看到:

    已连接到 ('127.0.0.1', 54321)  # 端口号可能不同
    正在接收文件: my_file.txt
    文件大小: 45 字节
    已接收: 45/45 字节
    文件接收完成!
  5. 验证结果: 在服务器的终端所在目录下,你会发现多了一个 my_file.txt 文件,其内容与客户端发送的文件完全一致。


代码解释与优化建议

  1. with 语句with 语句用于确保 socket 和文件对象在使用完毕后被正确关闭,即使发生错误也是如此,这是一种良好的编程习惯。

  2. 阻塞 vs. 非阻塞:默认情况下,socketaccept()recv()阻塞的,意味着程序会停在那里,直到有事件发生(如连接到来或数据到达),对于简单的示例来说,这是没问题的,但在更复杂的应用中,你可能需要使用 selectpollasyncio 来实现非阻塞 I/O,以处理多个客户端。

  3. 数据包粘包问题:我们的代码通过先发送固定大小的元数据(文件名和文件大小)来避免“粘包”问题,客户端发送 filename,然后发送 file_size,最后发送文件内容,服务器端严格按照这个顺序接收,因此不会混淆,如果发送的是多个不定长的数据包,情况会更复杂,需要额外的协议来分隔数据包。

  4. 缓冲区大小 (recv(4096))4096 是一个常用的缓冲区大小,你可以根据网络状况和文件大小调整它,更大的缓冲区可以提高传输效率,但会占用更多内存。

  5. 错误处理:客户端代码中包含了基本的错误处理,如 ConnectionRefusedErrorFileNotFoundError,使程序更健壮。

更高级的改进

  • 多线程/多进程:如果要让服务器能同时为多个客户端服务,可以使用 threadingmultiprocessing 模块,每当 accept() 一个新连接时,就创建一个新的线程或进程来处理这个客户端的请求。
  • 进度条:可以使用 tqdm 库来创建一个美观的进度条,替代简单的 print 输出。
  • 断点续传:可以通过记录已接收的字节数,并在客户端和服务器之间协商,实现文件传输中断后的续传功能。
  • 加密:对于敏感文件,可以在传输前使用 cryptography 等库对数据进行加密,确保传输过程的安全性。
分享:
扫描分享到社交APP
上一篇
下一篇