杰瑞科技汇

Python socket如何高效传输文件?

核心原理

文件传输的基本流程如下:

Python socket如何高效传输文件?-图1
(图片来源网络,侵删)
  1. 建立连接:客户端和服务端使用 socket 建立一个 TCP 连接。
  2. 发送元数据:客户端首先将要传输的文件名和文件大小发送给服务端,服务端接收并保存这些信息。
  3. 发送文件内容:客户端以二进制模式打开文件,读取文件内容,并通过套接字发送。
  4. 接收文件内容:服务端循环接收来自套接字的数据,并将其写入到一个新的文件中。
  5. 关闭连接:传输完成后,关闭套接字和文件。

完整代码示例

我们将创建两个 Python 脚本:server.py (服务端) 和 client.py (客户端)。

服务端 (server.py)

服务端负责监听客户端的连接,接收文件信息,并将接收到的数据写入新文件。

# server.py
import socket
# --- 配置 ---
HOST = '0.0.0.0'  # 监听所有可用的网络接口
PORT = 9999       # 任意非特权端口
def start_server():
    """
    启动文件接收服务器
    """
    # 创建一个 TCP 套接字
    # socket.AF_INET 表示使用 IPv4
    # socket.SOCK_STREAM 表示使用 TCP 协议
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置 SO_REUSEADDR 选项,允许地址在端口被占用后立即重用
    # 这在快速重启服务器时非常有用,可以避免 "Address already in use" 错误
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    try:
        # 绑定套接字到指定的地址和端口
        server_socket.bind((HOST, PORT))
        # 开始监听传入的连接
        # backlog 参数指定了在拒绝连接之前,操作系统可以挂起的最大连接数
        server_socket.listen(5)
        print(f"[*] 服务器正在监听 {HOST}:{PORT}...")
        # 接受一个新连接
        # accept() 方法会阻塞,直到有客户端连接
        # 它返回一个新的套接字对象 (client_socket) 和客户端的地址 (address)
        client_socket, address = server_socket.accept()
        print(f"[*] 成功接受来自 {address} 的连接!")
        # --- 1. 接收文件名 ---
        # 首先接收文件名长度(假设文件名长度不超过 1024 字节)
        filename_length_bytes = client_socket.recv(4)
        if not filename_length_bytes:
            print("[!] 未能接收文件名长度,连接可能已断开。")
            return
        filename_length = int.from_bytes(filename_length_bytes, byteorder='big')
        # 接收文件名
        filename_bytes = client_socket.recv(filename_length)
        filename = filename_bytes.decode('utf-8')
        print(f"[*] 正在接收文件: {filename}")
        # --- 2. 接收文件大小 ---
        # 接收文件大小(同样,假设文件大小不超过 2GB)
        file_size_bytes = client_socket.recv(8)
        if not file_size_bytes:
            print("[!] 未能接收文件大小,连接可能已断开。")
            return
        file_size = int.from_bytes(file_size_bytes, byteorder='big')
        print(f"[*] 文件大小: {file_size / (1024*1024):.2f} MB")
        # --- 3. 接收文件内容 ---
        received_size = 0
        # 以二进制写入模式创建新文件
        with open(filename, 'wb') as f:
            print("[*] 开始接收文件内容...")
            # 循环接收数据,直到接收完整个文件
            while received_size < file_size:
                # 每次最多接收 4096 字节的数据
                chunk = client_socket.recv(4096)
                if not chunk:
                    # 如果没有接收到数据,说明客户端已断开连接
                    print("[!] 客户端异常断开。")
                    break
                f.write(chunk)
                received_size += len(chunk)
                # 打印接收进度
                progress = (received_size / file_size) * 100
                print(f"\r[*] 接收进度: {progress:.2f}%", end='', flush=True)
        print("\n[*] 文件接收完成!")
    except KeyboardInterrupt:
        print("\n[!] 服务器被用户中断。")
    except Exception as e:
        print(f"[!] 发生错误: {e}")
    finally:
        # 确保套接字被正确关闭
        if 'client_socket' in locals():
            client_socket.close()
        server_socket.close()
        print("[*] 服务器已关闭。")
if __name__ == "__main__":
    start_server()

客户端 (client.py)

客户端负责连接服务端,读取本地文件,并将文件信息和内容发送给服务端。

# client.py
import socket
import os
# --- 配置 ---
HOST = '127.0.0.1'  # 服务器的 IP 地址 (本地回环地址)
PORT = 9999       # 服务器的端口
def send_file(filepath):
    """
    向服务器发送文件
    :param filepath: 要发送的文件的路径
    """
    if not os.path.exists(filepath):
        print(f"[!] 错误: 文件 '{filepath}' 不存在。")
        return
    # 创建一个 TCP 套接字
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        # 连接到服务器
        print(f"[*] 正在连接到 {HOST}:{PORT}...")
        client_socket.connect((HOST, PORT))
        print("[*] 连接成功!")
        # --- 1. 准备并发送文件名 ---
        filename = os.path.basename(filepath)
        filename_bytes = filename.encode('utf-8')
        # 首先发送文件名的长度 (4字节)
        client_socket.sendall(len(filename_bytes).to_bytes(4, byteorder='big'))
        # 然后发送文件名本身
        client_socket.sendall(filename_bytes)
        # --- 2. 准备并发送文件大小 ---
        file_size = os.path.getsize(filepath)
        # 发送文件大小 (8字节,可以表示最大 2GB 的文件)
        client_socket.sendall(file_size.to_bytes(8, byteorder='big'))
        # --- 3. 发送文件内容 ---
        sent_size = 0
        # 以二进制读取模式打开文件
        with open(filepath, 'rb') as f:
            print(f"[*] 开始发送文件: {filename}")
            # 循环读取并发送文件内容
            while True:
                # 每次最多读取 4096 字节的数据
                chunk = f.read(4096)
                if not chunk:
                    # 如果读取到文件末尾,则结束循环
                    break
                client_socket.sendall(chunk)
                sent_size += len(chunk)
                # 打印发送进度
                progress = (sent_size / file_size) * 100
                print(f"\r[*] 发送进度: {progress:.2f}%", end='', flush=True)
        print("\n[*] 文件发送完成!")
    except ConnectionRefusedError:
        print(f"[!] 连接被拒绝,请确保服务器正在运行在 {HOST}:{PORT}。")
    except Exception as e:
        print(f"[!] 发生错误: {e}")
    finally:
        # 确保套接字被正确关闭
        client_socket.close()
        print("[*] 连接已关闭。")
if __name__ == "__main__":
    # 替换为你想要发送的文件路径
    file_to_send = "my_test_file.txt" 
    send_file(file_to_send)

如何运行

  1. 准备一个测试文件:在与 client.py 相同的目录下,创建一个名为 my_test_file.txt 的文件,并写入一些内容。

    Python socket如何高效传输文件?-图2
    (图片来源网络,侵删)
  2. 启动服务端: 打开一个终端,运行服务端脚本。

    python server.py

    你会看到输出:

    [*] 服务器正在监听 0.0.0.0:9999...
  3. 启动客户端: 打开另一个终端,运行客户端脚本。

    python client.py

    你会看到客户端的输出,显示连接和发送进度,服务端终端会显示接收进度。

  4. 检查结果: 传输完成后,你会在服务端所在的目录下看到一个名为 my_test_file.txt 的新文件,其内容与客户端发送的文件完全一致。


关键点解释

  1. 二进制模式 ('rb''wb')

    • 文件在计算机中本质上是二进制数据,文本模式 ('r', 'w') 会在不同操作系统下处理换行符(\n vs \r\n),可能导致文件损坏。
    • 必须使用二进制模式 ('rb' 用于读取,'wb' 用于写入) 来传输文件,确保数据原封不动地传输。
  2. 元数据 (文件名和大小)

    • 为什么先发文件名和大小?因为服务端在接收文件内容时,需要知道要创建什么名字的文件,以及需要接收多少数据才算完成,否则,服务端要么无法命名文件,要么不知道何时结束接收,可能会一直等待或出错。
    • 发送长度:我们首先发送文件名和文件大小的长度(固定为4字节和8字节),这样服务端就知道接下来要读取多少字节的数据来获取文件名和大小,这是一种非常可靠的做法。
  3. 循环接收/发送 (while 循环)

    • 网络传输不是一蹴而就的。socket.recv()socket.send() 每次最多只能处理一个固定大小的数据块(在我们的例子中是 4096 字节)。
    • while 循环确保了我们能持续地、分块地接收或发送整个文件,直到所有数据都处理完毕。
  4. 进度显示 (\rflush=True)

    • \r 是回车符,它会将光标移动到当前行的开头,这样下一次 print 就会覆盖掉上一次的内容,从而实现单行动画效果。
    • flush=True 强制立即将输出刷新到终端,否则进度更新可能会被缓冲,导致显示不及时。
  5. 错误处理和资源释放 (try...finally)

    • 网络编程充满了不确定性(如连接中断、文件不存在等)。
    • 使用 try...except 来捕获和处理这些异常,防止程序崩溃。
    • 使用 finally 块来确保无论是否发生异常,socket.close() 都会被执行,避免资源泄露。

进阶改进方向

  • 多线程/异步:如果服务器需要同时为多个客户端服务,可以使用 threading 模型为每个客户端连接创建一个新线程。
  • 断点续传:可以修改协议,在发送文件前先检查服务端是否已有同名文件,并计算已接收的字节数,然后从该位置继续发送。
  • 加密传输:对于敏感文件,可以在发送前使用 cryptography 等库对数据进行加密,接收后再解密。
  • 更高效的协议:对于非常大的文件,可以考虑使用 UDP 协议(需要自己实现可靠性机制)或成熟的文件传输协议如 SFTP/SCP (基于 SSH)。
分享:
扫描分享到社交APP
上一篇
下一篇