杰瑞科技汇

Python socket传文件,如何高效实现?

核心原理

使用 Socket 传输文件,本质上就是将文件内容作为数据,通过 TCP/IP 协议从一个客户端发送到一个服务器,这个过程可以分解为以下几个步骤:

Python socket传文件,如何高效实现?-图1
(图片来源网络,侵删)
  1. 建立连接:客户端和服务器通过 socket 建立一个 TCP 连接。
  2. 发送元数据:客户端需要先告诉服务器一些关于文件的基本信息,最重要的是文件名文件大小,这样服务器才能正确地创建文件并知道何时接收完毕。
  3. 发送文件内容:客户端以流的形式,将文件的二进制内容一块一块地发送出去。
  4. 接收并保存文件:服务器先接收元数据,然后创建一个同名文件,并循环接收客户端发来的数据块,将它们写入文件,直到接收到的总字节数与文件大小一致。
  5. 关闭连接:传输完成后,双方关闭连接。

简单示例(基础版)

这个示例演示了最基本的功能,但存在一些潜在问题(如大文件处理、错误处理等),我们会在后续部分改进。

服务器端代码 (server.py)

# server.py
import socket
# 服务器配置
HOST = '127.0.0.1'  # 本地回环地址,表示只在本地测试
PORT = 65432        # 选择一个未被占用的端口
# 创建一个 TCP socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    print(f"服务器正在监听 {HOST}:{PORT}...")
    # 接受客户端连接
    conn, addr = s.accept()
    with conn:
        print(f"已连接到 {addr}")
        # 1. 先接收文件名
        filename = conn.recv(1024).decode('utf-8')
        print(f"准备接收文件: {filename}")
        # 2. 再接收文件大小
        file_size_bytes = conn.recv(1024)
        file_size = int.from_bytes(file_size_bytes, 'big')
        print(f"文件大小: {file_size} bytes")
        # 3. 接收文件内容
        received_size = 0
        with open(filename, 'wb') as f:
            print("开始接收文件内容...")
            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} bytes ({(received_size/file_size)*100:.2f}%)")
        print(f"文件 {filename} 接收完成!")

客户端代码 (client.py)

# client.py
import socket
# 服务器配置
HOST = '127.0.0.1'  # 必须与服务器端一致
PORT = 65432
# 要发送的文件路径
FILE_TO_SEND = 'my_file.txt' # 请确保这个文件存在于客户端的同一目录下
# 创建一个 TCP socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    print(f"已连接到服务器 {HOST}:{PORT}")
    # 1. 获取文件信息
    filename = FILE_TO_SEND
    file_size = os.path.getsize(filename)
    # 2. 先发送文件名
    s.sendall(filename.encode('utf-8'))
    print(f"已发送文件名: {filename}")
    # 3. 再发送文件大小
    # 使用 'big' 字节序,确保跨平台兼容性
    s.sendall(file_size.to_bytes((file_size.bit_length() + 7) // 8, 'big'))
    print(f"已发送文件大小: {file_size} bytes")
    # 4. 发送文件内容
    with open(filename, 'rb') as f:
        print("开始发送文件内容...")
        while True:
            chunk = f.read(4096) # 每次读取 4096 字节
            if not chunk:
                break
            s.sendall(chunk)
    print("文件发送完成!")

如何运行:

  1. 将上述代码分别保存为 server.pyclient.py
  2. 在同一目录下创建一个名为 my_file.txt 的文件,并写入一些内容。
  3. 先运行服务器python server.py
  4. 再运行客户端python client.py
  5. 服务器端会接收到文件,并保存在其运行目录下。

关键点解析与改进

上面的简单示例可以工作,但在生产环境中需要考虑更多细节。

为什么先发送文件名和大小?

  • 文件名:服务器需要知道创建什么名字的文件。
  • 文件大小:这是最重要的,TCP 是一个流式协议,数据像水流一样持续不断地到达,如果没有一个明确的结束标志(文件大小),服务器将不知道何时停止接收数据,recv() 会一直阻塞等待,导致程序卡死,文件大小为服务器提供了一个明确的结束条件。

recv()sendall() 的工作方式

  • socket.recv(size):会尝试从接收缓冲区读取最多 size 字节的数据,但它一次返回的数据量可能小于 size,尤其是在网络延迟或数据包不完整时,这就是为什么我们需要一个 while 循环来持续接收,直到接收完所有数据。
  • socket.sendall(data):它会持续发送 data 中的所有字节,直到全部发送完毕或发生错误,这比 socket.send() 更可靠,因为 send() 可能只发送了部分数据就返回了。

错误处理

网络是不可靠的,连接可能会中断,代码中应该加入 try...except 块来捕获和处理可能发生的异常,如 ConnectionAbortedError, socket.error 等。

Python socket传文件,如何高效实现?-图2
(图片来源网络,侵删)

更健壮的实现(带进度条和错误处理)

下面是一个更完善的版本,包含了进度条(使用 tqdm 库)和基本的错误处理。

安装 tqdm: pip install tqdm

改进后的服务器端 (server_robust.py)

# server_robust.py
import socket
import os
import tqdm
HOST = '0.0.0.0' # 监听所有可用的网络接口
PORT = 65432
BUFFER_SIZE = 4096 # 4KB
# 创建 socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(5)
print(f"[*] 服务器正在监听 {HOST}:{PORT}...")
# 接受一个客户端连接
client_socket, address = s.accept()
print(f"[+] 客户端 {address} 已连接。")
# 接收文件名
filename = client_socket.recv(BUFFER_SIZE).decode('utf-8')
# 接收文件大小
file_size = client_socket.recv(BUFFER_SIZE).decode('utf-8')
# 转换文件大小为整数
file_size = int(file_size)
# 开始接收文件
progress = tqdm.tqdm(range(file_size), f"接收 {filename}", unit="B", unit_scale=True, unit_divisor=1024)
with open(filename, "wb") as f:
    for _ in progress:
        # 读取 4KB 的数据
        bytes_read = client_socket.recv(BUFFER_SIZE)
        if not bytes_read:
            # 文件接收完毕
            break
        # 写入文件
        f.write(bytes_read)
        # 更新进度条
        progress.update(len(bytes_read))
client_socket.close()
s.close()
print(f"\n[+] 文件 {filename} 接收完毕。")

改进后的客户端 (client_robust.py)

# client_robust.py
import socket
import os
import tqdm
HOST = '127.0.0.1'
PORT = 65432
BUFFER_SIZE = 4096 # 4KB
SEPARATOR = "<SEPARATOR>" # 用于分隔文件名和文件大小
FILE_TO_SEND = "my_file.txt" # 要发送的文件
# 确保文件存在
if not os.path.exists(FILE_TO_SEND):
    print(f"[!] 错误: 文件 {FILE_TO_SEND} 不存在。")
    exit()
# 获取文件大小
filesize = os.path.getsize(FILE_TO_SEND)
# 创建客户端 socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
    print(f"[*] 正在连接到 {HOST}:{PORT}...")
    s.connect((HOST, PORT))
    print("[+] 连接成功!")
    # 发送文件名和文件大小
    # 格式: "filename<SEPARATOR>filesize"
    s.send(f"{FILE_TO_SEND}{SEPARATOR}{filesize}".encode('utf-8'))
    # 开始发送文件
    progress = tqdm.tqdm(range(filesize), f"发送 {FILE_TO_SEND}", unit="B", unit_scale=True, unit_divisor=1024)
    with open(FILE_TO_SEND, "rb") as f:
        for _ in progress:
            # 读取 4KB 的数据
            bytes_read = f.read(BUFFER_SIZE)
            if not bytes_read:
                # 文件发送完毕
                break
            # 发送数据
            s.sendall(bytes_read)
            # 更新进度条
            progress.update(len(bytes_read))
except Exception as e:
    print(f"[!] 发生错误: {e}")
finally:
    s.close()
    print("\n[+] 连接已关闭。")

改进点说明:

  1. 更清晰的元数据格式:客户端将文件名和大小用 <SEPARATOR> 符号拼接后一次性发送,服务器再进行分割,这比分开两次发送更简洁。
  2. 进度条:使用 tqdm 库可以直观地看到文件传输的进度。
  3. 更健壮的 try...finally:确保无论如何,socket 都会被正确关闭。
  4. 监听所有接口:服务器端 HOST = '0.0.0.0' 允许同一局域网内的其他电脑通过服务器的 IP 地址连接。

高级实践:多线程/多文件传输

如果你想同时处理多个客户端连接,或者在一个连接中传输多个文件,可以使用多线程。

多线程服务器 (server_threaded.py)

# server_threaded.py
import socket
import threading
import tqdm
def handle_client(client_socket, address):
    print(f"[+] 新连接来自 {address}")
    try:
        # 接收文件名和大小 (使用 SEPARATOR 分隔)
        received = client_socket.recv(4096).decode('utf-8')
        filename, filesize_str = received.split("<SEPARATOR>")
        filesize = int(filesize_str)
        progress = tqdm.tqdm(range(filesize), f"从 {address} 接收 {filename}", unit="B", unit_scale=True)
        with open(filename, "wb") as f:
            for _ in progress:
                bytes_read = client_socket.recv(4096)
                if not bytes_read:
                    break
                f.write(bytes_read)
                progress.update(len(bytes_read))
        print(f"\n[+] 从 {address} 接收文件 {filename} 完成。")
    except Exception as e:
        print(f"[!] 处理客户端 {address} 时出错: {e}")
    finally:
        client_socket.close()
        print(f"[-] 与 {address} 的连接已关闭。")
HOST = '0.0.0.0'
PORT = 65432
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen(5)
print(f"[*] 多线程服务器正在监听 {HOST}:{PORT}...")
while True:
    client_socket, address = server.accept()
    # 为每个客户端连接创建一个新线程
    thread = threading.Thread(target=handle_client, args=(client_socket, address))
    thread.start()
    print(f"[*] 活动连接数: {threading.active_count() - 1}") # 减去主线程

这个服务器可以同时接受多个客户端的连接,并为每个连接创建一个独立的线程来处理文件传输,互不干扰,客户端代码可以使用前面的 client_robust.py

分享:
扫描分享到社交APP
上一篇
下一篇