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

- 建立连接:客户端和服务器通过
socket建立一个 TCP 连接。 - 发送元数据:客户端需要先告诉服务器一些关于文件的基本信息,最重要的是文件名和文件大小,这样服务器才能正确地创建文件并知道何时接收完毕。
- 发送文件内容:客户端以流的形式,将文件的二进制内容一块一块地发送出去。
- 接收并保存文件:服务器先接收元数据,然后创建一个同名文件,并循环接收客户端发来的数据块,将它们写入文件,直到接收到的总字节数与文件大小一致。
- 关闭连接:传输完成后,双方关闭连接。
简单示例(基础版)
这个示例演示了最基本的功能,但存在一些潜在问题(如大文件处理、错误处理等),我们会在后续部分改进。
服务器端代码 (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("文件发送完成!")
如何运行:
- 将上述代码分别保存为
server.py和client.py。 - 在同一目录下创建一个名为
my_file.txt的文件,并写入一些内容。 - 先运行服务器:
python server.py - 再运行客户端:
python client.py - 服务器端会接收到文件,并保存在其运行目录下。
关键点解析与改进
上面的简单示例可以工作,但在生产环境中需要考虑更多细节。
为什么先发送文件名和大小?
- 文件名:服务器需要知道创建什么名字的文件。
- 文件大小:这是最重要的,TCP 是一个流式协议,数据像水流一样持续不断地到达,如果没有一个明确的结束标志(文件大小),服务器将不知道何时停止接收数据,
recv()会一直阻塞等待,导致程序卡死,文件大小为服务器提供了一个明确的结束条件。
recv() 和 sendall() 的工作方式
socket.recv(size):会尝试从接收缓冲区读取最多size字节的数据,但它一次返回的数据量可能小于size,尤其是在网络延迟或数据包不完整时,这就是为什么我们需要一个while循环来持续接收,直到接收完所有数据。socket.sendall(data):它会持续发送data中的所有字节,直到全部发送完毕或发生错误,这比socket.send()更可靠,因为send()可能只发送了部分数据就返回了。
错误处理
网络是不可靠的,连接可能会中断,代码中应该加入 try...except 块来捕获和处理可能发生的异常,如 ConnectionAbortedError, socket.error 等。

更健壮的实现(带进度条和错误处理)
下面是一个更完善的版本,包含了进度条(使用 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[+] 连接已关闭。")
改进点说明:
- 更清晰的元数据格式:客户端将文件名和大小用
<SEPARATOR>符号拼接后一次性发送,服务器再进行分割,这比分开两次发送更简洁。 - 进度条:使用
tqdm库可以直观地看到文件传输的进度。 - 更健壮的
try...finally:确保无论如何,socket都会被正确关闭。 - 监听所有接口:服务器端
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。
