
在Python Socket编程中,当尝试传输图片等二进制文件时,如果遇到UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position 5: invalid start byte这样的错误,通常意味着程序试图将非文本(二进制)数据按照文本(如UTF-8)编码进行解码。图片文件、音频文件等都是二进制数据流,它们不遵循任何文本编码规范。当服务器端或客户端接收到这些二进制数据后,如果错误地调用了.decode()方法,就会因为数据内容不符合UTF-8(或其他指定编码)的字符序列而抛出此错误。
原始代码中,服务器端对file_name和file_size都使用了.decode()方法:
file_name = client.recv(1024).decode() file_size = client.recv(1024).decode()
虽然文件名和文件大小通常是文本信息,但在没有明确分隔符的情况下,client.recv(1024)可能接收到部分文件数据,或者在接收文件名/大小的过程中,其后续紧跟着的二进制文件数据被错误地一起接收并尝试解码,从而引发UnicodeDecodeError。
除了错误的解码操作,更深层次的问题在于原始代码的传输协议设计不够健壮和明确。
立即学习“Python免费学习笔记(深入)”;
为了解决上述问题,我们需要设计一个明确且健壮的传输协议。常用的策略有两种:长度前缀法和空字节终止法。对于文件名和文件大小等字符串元数据,空字节终止法相对简单。对于文件内容,长度前缀法更为可靠和推荐。
空字节 b'\x00' 在C语言风格字符串中常用于表示字符串的结束,且在文件名或文件大小的字符串表示中通常不会出现。我们可以利用这一点来标记元数据的结束。
客户端发送时: 在发送文件名和文件大小的编码字节串后,追加一个空字节 b'\x00'。
client.send("received_image.png".encode() + b'\x00')
client.send(str(file_size).encode() + b'\x00')服务器端接收时: 循环接收数据直到遇到空字节,然后对接收到的数据进行解码。
def recv_until_null(sock):
buffer = b''
while True:
chunk = sock.recv(1) # 每次接收一个字节
if not chunk: # 连接已关闭
raise ConnectionError("Connection lost while receiving data.")
if chunk == b'\x00':
break
buffer += chunk
return buffer.decode('utf-8') # 解码接收到的字符串
file_name = recv_until_null(client)
file_size_str = recv_until_null(client)
file_size = int(file_size_str)长度前缀法要求在发送任何数据块之前,先发送该数据块的长度。这样接收方就能准确知道需要接收多少字节的数据。这对于二进制文件内容尤其重要。
协议示例:
为了简化,我们可以将文件大小作为文件内容的长度,并直接在传输文件内容前发送。
我们将结合长度前缀法(用于文件大小和内容)和空字节终止法(用于文件名,因为文件名长度通常不会太长且需要灵活处理)以及分块传输的策略来优化代码。
客户端将采用上下文管理器 (with open) 确保文件正确关闭,并分块读取文件以避免内存溢出。
import os
import socket
import struct # 用于处理固定长度的二进制数据
HOST = "localhost"
PORT = 9999
def send_file(client_socket, file_path, remote_file_name):
"""
发送文件到服务器。
协议:文件名长度(4字节) -> 文件名 -> 文件大小(8字节) -> 文件内容
"""
try:
file_size = os.path.getsize(file_path)
# 1. 发送文件名
file_name_bytes = remote_file_name.encode('utf-8')
file_name_len = len(file_name_bytes)
# 使用struct.pack将整数打包成固定字节长度的二进制数据
client_socket.sendall(struct.pack("!I", file_name_len)) # !I 表示大端无符号整数 (4字节)
client_socket.sendall(file_name_bytes)
# 2. 发送文件大小
client_socket.sendall(struct.pack("!Q", file_size)) # !Q 表示大端无符号长长整数 (8字节)
# 3. 分块发送文件内容
print(f"开始发送文件: {file_path} ({file_size} 字节)")
with open(file_path, "rb") as f:
bytes_sent = 0
while True:
chunk = f.read(4096) # 每次读取4KB
if not chunk:
break
client_socket.sendall(chunk)
bytes_sent += len(chunk)
# print(f"\r已发送: {bytes_sent}/{file_size} 字节", end="")
print(f"\n文件 {remote_file_name} 发送完成。")
except Exception as e:
print(f"客户端发送文件时发生错误: {e}")
if __name__ == "__main__":
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
client.connect((HOST, PORT))
print(f"已连接到服务器 {HOST}:{PORT}")
# 假设要发送的图片文件名为 "image.png"
# 远程保存的文件名为 "received_image.png"
send_file(client, "image.png", "received_image.png")
except ConnectionRefusedError:
print("连接被拒绝,请确保服务器已运行。")
except Exception as e:
print(f"客户端发生错误: {e}")
finally:
client.close()
print("客户端连接已关闭。")
服务器端需要匹配客户端的协议,先接收文件名长度和文件名,然后接收文件大小,最后循环接收文件内容直到接收到指定字节数。tqdm库用于显示传输进度。
import socket
import tqdm
import struct
import os
HOST = "localhost"
PORT = 9999
BUFFER_SIZE = 4096 # 每次接收的字节数
def recv_all(sock, n_bytes):
"""从socket接收指定数量的字节"""
data = b''
while len(data) < n_bytes:
packet = sock.recv(n_bytes - len(data))
if not packet:
return None # 连接关闭
data += packet
return data
def start_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen()
print(f"服务器正在监听 {HOST}:{PORT}...")
while True:
client_socket, addr = server.accept()
print(f"接受来自 {addr} 的连接。")
try:
# 1. 接收文件名长度
file_name_len_bytes = recv_all(client_socket, 4)
if file_name_len_bytes is None:
print("连接中断,无法接收文件名长度。")
continue
file_name_len = struct.unpack("!I", file_name_len_bytes)[0]
# 2. 接收文件名
file_name_bytes = recv_all(client_socket, file_name_len)
if file_name_bytes is None:
print("连接中断,无法接收文件名。")
continue
file_name = file_name_bytes.decode('utf-8')
print(f"接收文件名: {file_name}")
# 3. 接收文件大小
file_size_bytes = recv_all(client_socket, 8)
if file_size_bytes is None:
print("连接中断,无法接收文件大小。")
continue
file_size = struct.unpack("!Q", file_size_bytes)[0]
print(f"接收文件大小: {file_size} 字节")
# 4. 接收文件内容并写入文件
with open(file_name, "wb") as f:
progress = tqdm.tqdm(unit="B", unit_scale=True, unit_divisor=1000, total=file_size, desc=f"接收 {file_name}")
bytes_received = 0
while bytes_received < file_size:
remaining_bytes = file_size - bytes_received
data_to_recv = min(BUFFER_SIZE, remaining_bytes)
data = client_socket.recv(data_to_recv)
if not data:
# 客户端可能已关闭连接,但未发送完所有数据
print(f"\n警告: 客户端在发送完文件前断开连接。已接收 {bytes_received}/{file_size} 字节。")
break
f.write(data)
bytes_received += len(data)
progress.update(len(data))
progress.close()
if bytes_received == file_size:
print(f"文件 {file_name} 接收完成,大小: {os.path.getsize(file_name)} 字节。")
else:
print(f"文件 {file_name} 接收不完整。")
except Exception as e:
print(f"服务器处理客户端 {addr} 时发生错误: {e}")
finally:
client_socket.close()
print(f"与 {addr} 的连接已关闭。")
if __name__ == "__main__":
start_server()UnicodeDecodeError在Python Socket文件传输中是一个常见的错误,其根本原因在于对二进制数据进行了错误的文本解码,以及传输协议设计不明确。通过采用长度前缀法或空字节终止法来明确元数据和文件内容的边界,并始终将二进制数据作为字节流处理,可以构建出健壮、高效且可靠的文件传输系统。同时,遵循分块传输、错误处理和安全性等最佳实践,能够进一步提升程序的稳定性和实用性。
以上就是Python Socket文件传输中的Unicode解码错误及健壮性协议设计的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号