Python网络编程之粘包问题
粘包问题
【一】概要
- 粘包问题是在网络通信中常见的一种情况,它指的是发送方发送的多个小数据包在传输过程中被接收方一次性接收,导致数据粘在一起,难以正确解析。粘包问题通常出现在基于流的传输协议(如TCP)中,因为这些协议将数据视为一串字节流而不是消息。
【二】常用方法
- 通过struck模块
sock.recv(10240) # 接收的最大字节数就是1024, 这个数字不能够写的过大,问题就来了,发送的数据量比较大的时候,客户端没办法收到完整数据,我们的思路是:分多次接收
"""到底分多少次接收完所有的数据?"""
发送的总数据量字节大小 / 每次接收的最大字节数 = 总次数
import struct
res=struct.pack('i', 10240) # 二进制、固定长度的四个字节的二进制
obj=len(res)
struct.unpack('i', obj)[0] # 得到的就是字节的总大小
在发送数据的时候,多发送四个字节的大小,这个四个字节代表了数据的总大小,客户端接收数据的时候,先接收4个字节、sock.recv(4), 对这四个字节解包、得到总数据大小,然后在接收这个四个字节之后的数据才是真实数据,
【三】详解
【1】粘包问题
- 粘包问题常见于TCP流式协议中,而不常见于UDP报式协议,是因为报式协议将数据打包成一组数据
【1.1】流式协议粘包问题
- 客户端发送了三条数据
'''流式协议-服务端'''
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen()
conn, addr = server.accept()
print(conn.recv(1024)) # 接收1024个字节 # 而客户端发送的三条数据相加并未达到1024,将都会由第一个接收而接收
print(conn.recv(5)) # 后续的接收将没有数据
print(conn.recv(5))
conn.close()
'''
b'hellohellohello'
b''
b''
'''
'''流式协议-客户端'''
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
'''流式协议,短时间,客户端可以分多次发送数据给服务端,服务端一次性接收完数据'''
client.send(b'hello')
client.send(b'hello')
client.send(b'hello')
client.close()
【1.2】解决方法一(根据长度设置接收的字节)
- 当你知道,每一条数据的字节长度,可以根据长度设置接收的字节数,但当传输视频数据或者文件大小较大的数据时,就不太现实了
- 理论上,是可以通过字节数来接收较大的数据的,但耗时耗力且容错低,一旦1个字节出问题,整个数据将无法正常接收
'''流式协议-服务端'''
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen()
conn, addr = server.accept()
print(conn.recv(5))
print(conn.recv(5))
print(conn.recv(5))
conn.close()
'''
b'hello'
b'hello'
b'hello'
'''
'''流式协议-客户端'''
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
client.send(b'hello')
client.send(b'hello')
client.send(b'hello')
client.close()
【1.3】解决办法二(通过struct模块)
【1.3.1】struct模块
- 通过
struct
模块,你可以将数据打包成二进制流或从二进制流中解包出数据。
常用方法
pack(format, v1, v2, ...)
:- 将给定的数据根据指定的格式(format)打包成二进制数据。
- 例如:
struct.pack('Ihf', 42, 3.14, 2.718)
将会把一个整数、一个单精度浮点数和一个双精度浮点数打包成二进制数据。
unpack(format, buffer)
:- 从二进制数据中解包出数据,返回一个包含解包数据的元组。
- 例如:
struct.unpack('Ihf', b'\x2a\x00\x00\x00\x8f\xc2r@z\xe1z@')
将解包出对应的整数、单精度浮点数和双精度浮点数。
calcsize(format)
:- 返回一个字符串格式的大小,表示给定格式的 struct 所需的字节数。
- 格式化字符:
format
参数中可以使用不同的格式化字符,例如I
表示无符号整数,f
表示浮点数等。- struct — Interpret bytes as packed binary data — Python 3.12.1 documentation
【1.3.2】解决思路
-
sturct.pack('i',int)
:打包出来的二进制数据,固定四个字节长度 -
在发送数据的时候,先将struct打包的四个字节出入,这个四个字节代表了数据的总大小,客户端接收数据的时候,先接收4个字节、sock.recv(4), 对这四个字节解包、得到总数据大小,然后在接收这个四个字节之后的数据才是真实数据,
'''服务端'''
import struct
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen()
conn, addr = server.accept()
# 【2】进行连接,由客户端向服务端先发送消息
first_msg = conn.recv(1024)
print(first_msg.decode('utf8'))
# 发送的信息不确定长度
send_msg = '红红火火恍恍惚惚红红火火恍恍惚惚会后悔'
# 编码后的长度更加不确定
send_msg = send_msg.encode('utf8')
# 【3】将数据长度打包成4个字节
pack = struct.pack('i', len(send_msg))
# 【4】先将4个字节长度打包
conn.send(pack)
# 【7】发送真正的数据
conn.send(send_msg)
conn.close()
'''客户端'''
import struct
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
# 【1】客户端发送数据,与服务端建立连接
client.send(b'client')
# 【5】接收pack对象
pack = client.recv(4)
# 【6】解包,拿到data的长度
size_pack = struct.unpack('i', pack) # 用什么格式打包的,就用什么格式解包
print(size_pack) # 是个元组,所以可以索引取值 # (57,)
# 计数器
count = 0
data = b''
# 【8】接收真正的数据
while count < size_pack[0]:
txt = client.recv(5)
# 计数器加上拿到的数据长度
# 当计数器的长度大于数据长度,说明数据结束了
count += len(txt)
# 【9】将拿到的二进制数据拼接
data += txt
# 【10】解码得到数据
print(data.decode('utf8'))
# 红红火火恍恍惚惚红红火火恍恍惚惚会后悔 # 顺利拿到完整的数据
client.close()
【1.3.3】常见情况小练习(客户端下载服务端的数据)
- 服务端
import json
import socket
import struct
import os
IP = '127.0.0.1'
PORT = 8090
# 创建socket对象
server = socket.socket()
# 监听
server.bind((IP, PORT))
# 半连接池
server.listen(2)
BASE_DIR = r'D:\Files\Python全栈28期\教师笔记\day35_37_网络编程\day37\视频'
file_list = os.listdir(BASE_DIR)
file_dict = dict(enumerate(file_list, 1))
send_file_list = '\n'.join(file_list)
conn, addr = server.accept()
conn.send(send_file_list.encode('utf8'))
while True:
msg = conn.recv(1024)
msg = msg.decode('utf8')
print(msg)
if msg == 'q':
conn.close()
break
if '【video_choice】' in msg:
msg = msg[14:]
if not (int(msg) in file_dict if msg.isdigit() else False):
conn.send('【Number_False】编号有误!'.encode('utf8'))
continue
choice_video = file_dict[int(msg)]
video_path = os.path.join(BASE_DIR, choice_video)
with open(video_path, 'rb') as f:
video_data = f.read()
data_dict = {
'file_name': choice_video,
'data_len': len(video_data),
'BASE_DIR': BASE_DIR
}
data_json = json.dumps(data_dict)
data_bytes = data_json.encode('utf8')
data_pack = struct.pack('i', len(data_bytes))
conn.send(data_pack)
conn.send(data_bytes)
conn.send(video_data)
server.close()
import json
import os.path
import socket
import struct
IP = '127.0.0.1'
PORT = 8090
client = socket.socket()
client.connect((IP, PORT))
# 接收数据
while True:
msg = client.recv(1024)
msg = msg.decode('utf8')
print(f"服务端反馈信息:\n{msg}")
if "Number_False" in msg:
video_choice = input("输入有误!请重新输入视频编号:").strip()
video_choice = '【video_choice】' + video_choice
if len(msg) == 4 and isinstance(msg, bytes):
data_pack = client.recv(4)
recv_size = struct.unpack('i', data_pack)[0]
recv_start = 0
data_json = ''
video_data = ''
while recv_start < recv_size:
msg_from_server = client.recv(1024)
recv_size += len(msg_from_server)
msg_from_server = msg_from_server.decode('utf8')
data_json = json.loads(msg_from_server)
break
while recv_size < data_json['data_len']:
msg_from_server = client.recv(1024)
recv_size += len(msg_from_server)
print(msg_from_server, end='')
video_choice = input(f"请输入拷贝的视频文件名【默认为{data_json['file_name']}_new】:").strip()
if len(video_choice) == 0:
video_choice = f"{data_json['file_name']}_new"
new_path = os.path.join(data_json['BASE_DIR'], video_choice, '.mp4')
with open(new_path, 'ab') as fp:
fp.write(msg_from_server)
true_msg = f"文件已拷贝,请在{new_path} 查看!"
client.send(true_msg.encode('utf8'))
break
elif "File_False" in msg:
video_choice = input("输入有误!请重新输入拷贝的视频文件名:").strip()
video_choice = '【filename】' + video_choice
else:
video_choice = input("请输入视频编号:").strip()
video_choice = '【video_choice】' + video_choice
client.send(video_choice.encode('utf8'))
【2】为什么UDP不会出现粘包问题
- UDP并没有像TCP那样的字节流概念,它是基于数据报(Datagram)的,每个UDP报文都是一个独立的数据包,没有先后顺序的概念。
'''服务端'''
import socket
# 常量设置
IP = '127.0.0.1'
PORT = 8080
# 创建对象
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 报式协议 # 也就是UDP协议
# 监听
server.bind((IP, PORT))
# 接收信息
data, addr = server.recvfrom(1024)
print(data)
# 回复信息
with open('【8.0】进程锁(互斥锁).mp4', 'rb') as f:
data = f.read()
server.sendto(data, addr)
'''超过大小,直接报错'''
# OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。
# 关闭连接
server.close()
'''客户端'''
import socket
# 常量设置
IP = '127.0.0.1'
PORT = 8080
# 创建对象
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 发送消息
client.sendto(b'client', (IP, PORT))
# 接收消息
data, addr = client.recvfrom(1024)
with open('copy.mp4','wb') as f:
f.write(data)
# 关闭连接
client.close()