基本原理
通过 Socket 传输文件,核心思想是:

- 建立连接:客户端和服务器通过
socket建立一个 TCP 连接。 - 发送元数据:客户端在发送文件内容之前,先发送一些“元数据”(Metadata),例如文件名、文件大小,这样服务器在接收时可以知道将要接收多大的文件,并创建一个正确大小的空文件,然后填充内容。
- 发送文件内容:客户端以二进制模式(
'rb')打开文件,读取文件内容,并通过socket发送。 - 接收并写入文件:服务器接收文件名和文件大小信息,然后以二进制模式(
'wb')创建一个新文件,循环接收来自socket的数据块,并将其写入文件,直到接收到的总字节数等于文件大小。
关键点:为什么需要发送文件大小?
如果不发送文件大小,服务器将不知道何时停止接收数据。socket.recv() 方法在没有数据时会阻塞,如果文件发送完毕,服务器会一直等待下去,导致程序卡死,发送文件大小是解决这个问题的关键。
代码实现
我们将创建两个独立的 Python 脚本:
server.py:服务器端脚本,用于接收文件。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)
客户端连接到服务器,然后发送要传输的文件。

# 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}' 未找到。")
如何运行
-
准备文件:在你的工作目录下创建一个名为
my_file.txt的文件,并写入一些内容,写入 "Hello, this is a test file for socket transfer!"。 -
启动服务器: 打开一个终端,运行服务器脚本。
python server.py
你会看到输出:
服务器正在监听 127.0.0.1:65432... -
启动客户端: 打开另一个终端,运行客户端脚本。
python client.py
你会看到客户端的输出:
已连接到服务器 127.0.0.1:65432 文件 'my_file.txt' 发送完成。 -
查看服务器输出: 回到服务器的终端,你会看到:
已连接到 ('127.0.0.1', 54321) # 端口号可能不同 正在接收文件: my_file.txt 文件大小: 45 字节 已接收: 45/45 字节 文件接收完成! -
验证结果: 在服务器的终端所在目录下,你会发现多了一个
my_file.txt文件,其内容与客户端发送的文件完全一致。
代码解释与优化建议
-
with语句:with语句用于确保socket和文件对象在使用完毕后被正确关闭,即使发生错误也是如此,这是一种良好的编程习惯。 -
阻塞 vs. 非阻塞:默认情况下,
socket的accept()和recv()是阻塞的,意味着程序会停在那里,直到有事件发生(如连接到来或数据到达),对于简单的示例来说,这是没问题的,但在更复杂的应用中,你可能需要使用select、poll或asyncio来实现非阻塞 I/O,以处理多个客户端。 -
数据包粘包问题:我们的代码通过先发送固定大小的元数据(文件名和文件大小)来避免“粘包”问题,客户端发送
filename,然后发送file_size,最后发送文件内容,服务器端严格按照这个顺序接收,因此不会混淆,如果发送的是多个不定长的数据包,情况会更复杂,需要额外的协议来分隔数据包。 -
缓冲区大小 (
recv(4096)):4096是一个常用的缓冲区大小,你可以根据网络状况和文件大小调整它,更大的缓冲区可以提高传输效率,但会占用更多内存。 -
错误处理:客户端代码中包含了基本的错误处理,如
ConnectionRefusedError和FileNotFoundError,使程序更健壮。
更高级的改进
- 多线程/多进程:如果要让服务器能同时为多个客户端服务,可以使用
threading或multiprocessing模块,每当accept()一个新连接时,就创建一个新的线程或进程来处理这个客户端的请求。 - 进度条:可以使用
tqdm库来创建一个美观的进度条,替代简单的print输出。 - 断点续传:可以通过记录已接收的字节数,并在客户端和服务器之间协商,实现文件传输中断后的续传功能。
- 加密:对于敏感文件,可以在传输前使用
cryptography等库对数据进行加密,确保传输过程的安全性。
