粘包问题和解决粘包问题
一、粘包问题
1.1 粘包问题的概念
粘包:tcp会把数据量较小、时间间隔较短的数据,当做同一个包发送。
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
只有TCP有粘包问题,UDP永远不会粘包
udp是基于数据报的,udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
TCP是基于数据流的,tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
1.2 发生粘包的两种情况
- 发送端需要等缓冲区满才发送出去,造成粘包(发送时间间隔很短,数据又很小,会合到一起发送,产生粘包)
- 接收端不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
1.3 发生粘包情况举例
# 服务端
import socket
soc = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 绑定地址和端口号
soc.bind(('127.0.0.1',8005))
# 监听
soc.listen(5)
# 连接循环
while True:
print('等待客户端连接>>>')
conn,addr = soc.accept()
print('有个客户端连接上了>>>',addr)
while True: # 通信循环
try:
data = conn.recv(1024)
print(data)
data2 = conn.recv(1024)
print(data2)
data3 = conn.recv(1024)
print(data3)
except Exception:
break
# 关闭通路
conn.close()
# 关闭连接
soc.close()
等待客户端连接>>>
有个客户端连接上了>>> ('127.0.0.1', 51281)
b'a'
b'b'
b'c'
b'a'
b'b'
b'c'
b'a'
b'bc' # 这就出现了粘包
# 客户端
import socket
soc = socket.socket()
soc.connect(('127.0.0.1',8005))
while True:
in_s = input('请输入要发送的数据>>>').strip()
soc.send(b'a')
soc.send(b'b')
soc.send(b'c')
二、解决粘包问题
2.1 struct模块
它的作用就是把你输入进来的数字给转换成四位的字节,这样,无论你输入多大的数字,都可以转换成四位字节,这样接收端就固定收四个字节就好了。
import struct
# 把一个数字打包成固定长度的4字节
obj = struct.pack('i',1098)
print(obj)
print(len(obj))
l = struct.unpack('i',obj)[0]
print(l)
--------------------------------------------------------------------
b'Jx04x00x00'
4
1098
2.2 解决粘包的服务端和客户端
# 服务端
import socket
import subprocess
import struct
soc = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
soc.bind(('127.0.0.1',8005))
soc.listen(5)
while True: # 连接循环
print('等待客户端连接')
conn,addr = soc.accept()
print('有个客户端连接上了>>>',addr)
while True: # 通信循环
try:
data = conn.recv(1024)
if len(data) == 0:
break
print(data)
obj = subprocess.Popen(str(data,encoding='utf8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
# 执行正确的结果b格式,gbk编码(windows平台)
msg = obj.stdout.read()
# 发送的时候需要先把长度计算出来
# 头必须是固定长度
# 先发4位,头的长度
import json
dic = {'size':len(msg)}
dic_bytes = (json.dumps(dic).encode('utf8'))
# head_count是4个字节的长度
head_count = struct.pack('i',len(dic_bytes))
print(dic)
# 发送头部长度
conn.send(head_count)
# 发送头部内容的bytes格式
conn.send(dic_bytes)
# 发送内容
conn.send(msg)
except Exception:
break
# 关闭通道
conn.close()
# 关闭连接
soc.close()
首先我们已经获得了这个数据了,我们来构建一个他的数据头,新建了一个字典,然后把这个数据的长度放进了这个字典,然后用json模块,把这个字典转换成了字符类型。这个时候把这个字典(字符类型)的长度打包成head_count(四个字节长度),这个时候在把这个head_count发送给接收端,然后把被转换成字节类型的字典也传过去,最后再把数据传过去。
# 客户端
import socket
import struct
import json
soc = socket.socket()
soc.connect(('127.0.0.1',8005))
while True: # 通信循环
in_s = input('请输入要执行的命令>>>').strip()
# 发送数据的b格式
soc.send(in_s.encode('utf8'))
# 接收头部字典的长度
head_dic_len = soc.recv(4)
# 解出头部字典真正的长度
head_l = struct.unpack('i',head_dic_len)[0] # 解包出来是元组所以要取第0个值
# bytes格式字典长度,接收真正的头部字典
dic_bytes = soc.recv(head_l)
# 反序列化得到真正头部字典
head = json.loads(dic_bytes)
print(head)
l = head['size'] # 得到真正数据长度
count = 0
data_total = b''
print('l')
while count<l:
if l < 1024: # 如果接收的数据小于1024,直接接收数据大小
data = soc.recv(l)
else: # 如果接收的数据大于1024
if l-count >= 1024: # 总数据长度-count(目前收到多少,count就是多少),如果还大于1024,再收1024
data = soc.recv(1024)
else: # 总数据长度-count(目前收到多少,count就是多少),如果小于1024,只收剩下的部分
data = soc.recv(l-count)
data_total += data
count += len(data)
print(str(data_total,encoding='gbk')) #把收到的总数据用gbk解码转化为字符串
之前在服务端我们发送了三个数据,第一个是被转换成字节类型的字典的长度,第二个是被转成字节类型的字典,第三个就是我们最重要的数据本身了。
head_dic_len接受了第一个数据,把它解包,得到了转换成字节类型的字典的长度,这里注意一下,因为每次解包之后的到的内容都是一个元组,所以我们要在后面加上[ ],然后取第一个就行了。这个时候我们知道了转换成字节类型的字典的长度了,再一次接收,这次dic_bytes=soc.recv(转换成字节类型的字典的长度),这样就能拿到这个转换成字节类型的字典而不会多拿到后面传来的真正的数据了,接下来把这个转换成字节类型的字典用json的loads再转换为字典类型,取出里面的长度就好了,接下来的操作都和之前一样了,这里有点绕,需要仔细看清楚。
这下粘包问题,就真的彻底解决了