udp本身是无连接、不可靠的传输协议,直接用来传文件容易出现丢包、乱序问题,但通过“分块传输+校验+重传”的设计,依然能实现稳定的文件传输。
一、udp文件传输的核心设计思路
udp不保证数据送达,所以要先解决三个关键问题:
- 分块传输:将大文件拆分成固定大小的数据包(如1024字节),避免单次发送数据过大导致丢包;
- 包标识:每个数据包添加“序号+总包数+校验位”,服务端接收后能校验完整性、排序重组;
- 确认重传:服务端接收每个数据包后返回确认(ack),客户端未收到ack则重传该数据包,保证可靠性。
核心流程:
- 客户端:读取文件→分块→加标识→发送数据包→等待ack→重传失败包→发送结束标识;
- 服务端:接收数据包→校验→排序→重组→保存文件→返回ack。
二、完整实现代码
1. 服务端代码(接收文件)
import socket
import os
# 配置参数
udp_ip = "" # 监听所有本机ip
udp_port = 9999
buffer_size = 1024 # 数据包大小(需和客户端一致)
save_dir = "received_files" # 文件保存目录
# 创建保存目录
if not os.path.exists(save_dir):
os.makedirs(save_dir)
def udp_file_server():
# 1. 创建udp socket
server_socket = socket.socket(socket.af_inet, socket.sock_dgram)
server_socket.bind((udp_ip, udp_port))
# 允许端口复用,避免重启报错
server_socket.setsockopt(socket.sol_socket, socket.so_reuseaddr, 1)
print(f"udp文件服务端已启动,监听端口 {udp_port}...")
# 初始化接收状态
file_data = {} # 存储接收的数据包 {序号: 数据}
total_packets = 0 # 总包数
file_name = "" # 文件名
client_addr = none # 客户端地址
try:
while true:
# 2. 接收数据包(阻塞)
data, client_addr = server_socket.recvfrom(buffer_size + 32) # 预留标识位空间
if not data:
continue
# 解析数据包标识:"文件名|总包数|当前序号|数据"
try:
# 分割标识和数据(用|分隔,最后一段是文件数据)
parts = data.decode('utf-8', errors='ignore').split('|', 3)
if len(parts) != 4:
# 不是文件数据包,可能是结束标识
if data.decode('utf-8') == "transfer_finish":
print("客户端发送结束标识,开始重组文件...")
break
continue
file_name, total_packets_str, packet_num_str, file_chunk = parts
total_packets = int(total_packets_str)
packet_num = int(packet_num_str)
except exception as e:
print(f"数据包解析失败:{e}")
# 返回错误ack
server_socket.sendto(f"err|{packet_num}".encode('utf-8'), client_addr)
continue
# 3. 校验并保存数据包
if 1 <= packet_num <= total_packets:
file_data[packet_num] = file_chunk
# 返回成功ack
server_socket.sendto(f"ack|{packet_num}".encode('utf-8'), client_addr)
# 打印进度
progress = (len(file_data) / total_packets) * 100
print(f"接收进度:{progress:.1f}% ({len(file_data)}/{total_packets})", end='\r')
# 4. 重组并保存文件
if file_data and total_packets > 0:
# 按序号排序数据包
sorted_chunks = [file_data[i] for i in range(1, total_packets + 1) if i in file_data]
# 拼接所有数据
file_path = os.path.join(save_dir, file_name)
with open(file_path, 'wb') as f:
for chunk in sorted_chunks:
f.write(chunk.encode('utf-8')) # 若传二进制文件,需调整编码逻辑(见进阶部分)
print(f"\n文件接收完成!保存路径:{file_path}")
# 发送完成确认
server_socket.sendto("file_saved".encode('utf-8'), client_addr)
else:
print("\n未接收到完整文件数据")
except keyboardinterrupt:
print("\n服务端手动停止")
finally:
server_socket.close()
if __name__ == "__main__":
udp_file_server()
2. 客户端代码(发送文件)
import socket
import os
import time
# 配置参数
server_ip = "127.0.0.1" # 服务端ip(远程传输改实际ip)
server_port = 9999
buffer_size = 1024 # 每个数据包的文件数据大小
retry_times = 3 # 单个数据包最大重传次数
retry_interval = 0.5 # 重传间隔(秒)
def udp_file_client(file_path):
# 1. 检查文件是否存在
if not os.path.exists(file_path):
print(f"错误:文件 {file_path} 不存在")
return
# 获取文件名和文件大小
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
# 计算总包数(向上取整)
total_packets = (file_size + buffer_size - 1) // buffer_size
print(f"准备发送文件:{file_name},大小:{file_size}字节,总包数:{total_packets}")
# 2. 创建udp socket
client_socket = socket.socket(socket.af_inet, socket.sock_dgram)
client_socket.setsockopt(socket.sol_socket, socket.so_reuseaddr, 1)
# 设置超时(避免等待ack卡死)
client_socket.settimeout(2)
try:
# 3. 读取文件并分块发送
with open(file_path, 'r', encoding='utf-8') as f: # 二进制文件用'rb'(见进阶部分)
for packet_num in range(1, total_packets + 1):
# 读取当前块数据
file_chunk = f.read(buffer_size)
if not file_chunk:
break
# 构造数据包:"文件名|总包数|当前序号|数据"
packet_data = f"{file_name}|{total_packets}|{packet_num}|{file_chunk}".encode('utf-8')
retry_count = 0
ack_received = false
# 4. 发送并等待ack,失败则重传
while retry_count < retry_times and not ack_received:
try:
# 发送数据包
client_socket.sendto(packet_data, (server_ip, server_port))
# 等待ack
ack_data, _ = client_socket.recvfrom(128)
ack_parts = ack_data.decode('utf-8').split('|')
if ack_parts[0] == "ack" and int(ack_parts[1]) == packet_num:
ack_received = true
# 打印进度
progress = (packet_num / total_packets) * 100
print(f"发送进度:{progress:.1f}% ({packet_num}/{total_packets})", end='\r')
except socket.timeout:
retry_count += 1
print(f"\n数据包 {packet_num} 超时,重传 {retry_count}/{retry_times}")
time.sleep(retry_interval)
except exception as e:
print(f"\n数据包 {packet_num} 发送失败:{e}")
retry_count += 1
time.sleep(retry_interval)
if not ack_received:
print(f"\n错误:数据包 {packet_num} 重传{retry_times}次失败,传输终止")
return
# 5. 发送结束标识
client_socket.sendto("transfer_finish".encode('utf-8'), (server_ip, server_port))
# 等待服务端保存完成确认
try:
finish_ack, _ = client_socket.recvfrom(128)
if finish_ack.decode('utf-8') == "file_saved":
print("\n文件发送完成!服务端已保存")
except socket.timeout:
print("\n未收到服务端完成确认,但数据已发送完毕")
except keyboardinterrupt:
print("\n客户端手动停止")
finally:
client_socket.close()
if __name__ == "__main__":
# 替换为你要发送的文件路径(本地测试用绝对/相对路径)
target_file = "test.txt" # 示例:发送当前目录的test.txt
udp_file_client(target_file)
三、基础版使用步骤
- 准备测试文件:在客户端目录创建一个
test.txt文件(内容任意); - 启动服务端:运行服务端代码,控制台显示“udp文件服务端已启动”;
- 启动客户端:修改客户端代码中
target_file为实际文件路径,运行客户端; - 查看结果:服务端控制台显示传输进度,完成后文件会保存到
received_files目录。
四、关键优化:支持二进制文件(图片/视频/压缩包)
基础版仅支持文本文件,要传输图片、视频等二进制文件,需修改编码逻辑(核心是避免字符编码导致的数据损坏):
1. 客户端修改(读取二进制文件)
# 替换客户端文件读取部分
with open(file_path, 'rb') as f: # 改为二进制读取
for packet_num in range(1, total_packets + 1):
file_chunk = f.read(buffer_size)
if not file_chunk:
break
# 构造数据包:用特殊分隔符(如b'|||'),避免二进制数据冲突
packet_header = f"{file_name}|{total_packets}|{packet_num}".encode('utf-8')
packet_data = packet_header + b'|||' + file_chunk # 二进制拼接
2. 服务端修改(解析二进制数据)
# 替换服务端数据包解析部分
try:
# 分割头部和二进制数据(按b'|||'分割)
header, file_chunk = data.split(b'|||', 1)
# 解析头部(转字符串)
parts = header.decode('utf-8').split('|', 2)
file_name, total_packets_str, packet_num_str = parts
total_packets = int(total_packets_str)
packet_num = int(packet_num_str)
except exception as e:
print(f"二进制数据包解析失败:{e}")
continue
# 保存时直接写入二进制数据
with open(file_path, 'wb') as f:
for chunk in sorted_chunks:
f.write(chunk) # 无需encode,直接写二进制
五、避坑指南:常见问题与解决方案
1. 数据包丢包/重传失败
- 原因:udp无可靠性保证,网络波动易丢包;
- 解决方案:
- 增大
retry_times(如改为5),延长retry_interval; - 减小
buffer_size(如改为512),降低单包传输压力; - 远程传输时确保服务端端口已开放防火墙。
- 增大
2. 大文件传输卡顿
- 原因:循环发送未做速率控制,网络拥塞;
- 解决方案:在客户端发送每个数据包后添加
time.sleep(0.001),控制发送速率。
3. 数据包解析错误
- 原因:分隔符(|)与文件内容冲突;
- 解决方案:改用更复杂的分隔符(如
|||或随机字符串),或对头部做base64编码。
4. 端口被占用
- 解决方案:修改
udp_port(如改为10000),并在服务端添加so_reuseaddr选项(代码已包含)。
到此这篇关于python实现基于udp的文件传输的全过程的文章就介绍到这了,更多相关python基于udp的文件传输内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论