什么是粘包?
简单来说,粘包就是连续向对端发送两个或者两个以上的数据包,对端在一次收取中收到的数据包数量可能大于1个,当大于1个时,可能时几个包加上某个包的部分。当然,也可能收到的数据只是一个包的部分,这种情况一般也叫做半包。
多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发送方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
出现粘包的原因?
出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
先说接收方原因:接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区中取数据,这样就一次取到了多包数据。
再说由发送导致的粘包:粘包并不是 TCP 协议造成的,它的出现是因为应用层协议设计者对 TCP 协议的错误理解。我们将从 TCP 协议以及应用层协议出发,分析我们经常提到的 TCP 协议中的粘包是如何发生的。
TCP 协议是面向字节流的协议,它可能会组合或者拆分应用层协议的数据;
应用层协议没有定义消息的边界导致数据的接收方无法拼接数据;
TCP 协议是面向连接的、可靠的、基于字节流的传输层通信协议,应用层交给 TCP 协议的数据并不会以消息为单位向目的主机传输,这些数据在某些情况下会被组合成一个数据段发送给目标的主机。
解决方案?
发送数据的时候,先发送大小,然后再发送内容。
server端代码:
def send_file(self, data, flag_bytes=False):
if flag_bytes:
data_len = len(data)
self.new_socket.send(struct.pack('I', data_len)) # 先发长度(大小)
self.new_socket.send(data) # 再发内容
else:
# 字符串
data_bytes = data.encode('utf8')
data_len = len(data_bytes)
self.new_socket.send(struct.pack('I', data_len))
self.new_socket.send(data_bytes)
def recv_file(self, new_socket):
# struct.unpack(fmt, string)
#
# 顾名思义,解包。比如pack打包,然后就可以用unpack解包了。返回一个由解包数据(string)
# 得到的一个元组(tuple), 即使仅有一个数据也会被解包成元组。
file_len = new_socket.recv(4) # 拿到长度
file_content_len = struct.unpack('I', file_len)[0] # [0]是因为unpack得到的是一个元组,[0]才是你要的数(file_len)
file_content: bytes = new_socket.recv(file_content_len)
return file_content.decode('utf8')
client端代码:
def send_file(self, new_socket, data: str):
data_bytes = data.encode('utf8')
data_len = len(data_bytes)
# struct.pack(fmt,v1,v2,.....)
#
# 将v1,v2等参数的值进行一层包装,包装的方法由fmt指定。被包装的参数必须严格符合fmt。最后返回一个包装后的字符串。
new_socket.send(struct.pack('I', data_len)) # 发长度
new_socket.send(data_bytes) # 发内容
def recv_file(self):
file_len = self.client_socket.recv(4) # socket.recv()指接收对方发来的数据,4表示最多收4字节
file_content_len = struct.unpack('I', file_len)[0]
file_content: bytes = self.client_socket.recv(file_content_len)
return file_content.decode('utf8')