粘包问题
一.粘包问题
什么是粘包?
粘包指的是数据与数据之间没有明确的分界线,导致不能正确读取数据!
要理解粘包问题,需要先了解TCP协议传输数据时的具体流程,TCP协议也称之为流式协议(UDP称为数据报协议)
应用程序无法直接操作硬件,应用程序想要发送数据则必须将数据交给操作系统,而操作系统需要需要同时为所有应用程序提供数据传输服务,也就意味着,操作系统不可能立马就能将应用程序的数据发送出去,就需要为应用程序提供一个缓冲区,用于临时存放数据,具体流程如下:
发送方:
当应用程序调用send函数时,应用程序会将数据从应用程序拷贝到操作系统缓存,再由操作系统从缓冲区读取数据并发送出去
接收方:
对方计算机收到数据也是操作系统先收到,至于应用程序何时处理这些数据,操作系统并不清楚,所以同样需要将数据先存储到操作系统的缓冲区中,当应用程序调用recv时,实际上是从操作系统缓冲区中将数据拷贝到应用程序的过程
上述过程对于TCP与UDP都是相同的不同之处在于:
UDP:
UDP在收发数据时是基于数据包的,即一个包一个包的发送,包与包之间有着明确的分界,到达对方操作系统缓冲区后也是一个一个独立的数据包,接收方从操作系统缓冲区中将数据包拷贝到应用程序
这种方式存在的问题:
1.发送方发送的数据长度每个操作系统会有不同的限制,数据超过限制则无法发送
2.接收方接收数据时如果应用程序的提供的缓存容量小于数据包的长度将造成数据丢失,而缓冲区大小不可能无限大

TCP:
当我们需要传输较大的数据,或需要保证数据完整性时,最简单的方式就是使用TCP协议了
与UDP不同的是,TCP增加了一套校验规则来保证数据的完整性,会将超过TCP包最大长度的数据拆分为多个TCP包 并在传输数据时为每一个TCP数据包指定一个顺序号,接收方在收到TCP数据包后按照顺序将数据包进行重组,重组后的数据全都是二进制数据,且每次收到的二进制数据之间没有明显的分界

基于这种工作机制TCP在三种情况下会发送粘包问题
1.当单个数据包较小时接收方可能一次性读取了多个包的数据
2.当整体数据较大时接收方可能一次仅读取了一个包的一部分内容
3.另外TCP协议为了提高效率,增加了一种优化机制,会将数据较小且发送间隔较短的数据合并发送,该机制也会导致发送方将两个数据包粘在一起发送
二.粘包的解决方案
理解了粘包问题后,接下来就是如何来解决它了
1.基础解决方案
解决方案:
在发送数据前先发送数据长度
客户端:
import socket
c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
cmd = input(">>>:").strip()
c.send(cmd.encode("utf-8"))
data = c.recv(1024)
length = int(data.decode("utf-8"))
print(length)
size = 0
res = b""
while size < length:
temp = c.recv(1024)
size += len(temp)
res += temp
print(res.decode("gbk"))
服务器:
import socket
import subprocess
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()
while True:
client, addr = server.accept()
while True:
cmd = client.recv(1024).decode("utf-8")
p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
data = p.stdout.read()+p.stderr.read()
length = str(len(data))
client.send(length.encode("utf-8"))
print(length)
client.sendall(data)
上述方案 看起来解决了粘包问题
但是由于negle优化机制的存在,长度信息和数据还是有可能会粘包,而接受方并不知道长度信息具体几个字节,所以现在的问题是如何能够长度信息做成一个固定长度的bytes数据
我们可以将字符串拼接为一个固定长度的字符 但是这样太麻烦,struct模块为我们提供了一个功能,可以将整数类型转换为固定长度的bytes,此时就派上用场了
# struct模块使用
import struct
# 整型转bytes
res = struct.pack("i",100)
print(res)
print(len(res))
# bytes转整型
res2 = struct.unpack("i",res) # 返回一个元组
print(res2)
print(res2[0])
解决方案修正
客户端:
import socket
import struct
c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
cmd = input(">>>:").strip()
c.send(cmd.encode("utf-8"))
data = c.recv(4)
length = struct.unpack("i",data)[0]
print(length)
size = 0
res = b""
while size < length:
temp = c.recv(1024)
size += len(temp)
res += temp
print(res.decode("gbk"))
服务器:
import socket
import subprocess
import struct
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()
while True:
client, addr = server.accept()
while True:
cmd = client.recv(1024).decode("utf-8")
p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
data = p.stdout.read()+p.stderr.read()
length = len(data)
len_data = struct.pack("i",length)
client.send(len_data)
print(length)
client.send(data)
2.自定义报头解决粘包
上述方案已经完美解决了粘包问题,但是扩展性不高,例如我们要实现文件上传下载,不光要传输文件数据,还需要传输文件名字,md5值等等,如何能实现呢?
具体思路:
发送端:
1.先将所有的额外信息打包到一个头中
2.然后先发送头部数据
3.最后发送真实数据
接收端:
1.接收固定长度的头部长度数据
2.根据长度数据获取头部数据
3.根据头部数据获取真实数据
CMD程序自定义报头:
客户端:
import socket
import struct
import json
c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
cmd = input(">>>:").strip()
c.send(cmd.encode("utf-8"))
# 头部数据
data = c.recv(4)
head_length = struct.unpack("i",data)[0]
head_data = c.recv(head_length).decode("utf-8")
head = json.loads(head_data)
print(head)
# 真实数据长度
data_length = head["data_size"]
#接收真实数据
size = 0
res = b""
while size < data_length:
temp = c.recv(1024)
size += len(temp)
res += temp
print(res.decode("gbk"))
服务器:
import socket
import subprocess
import struct
import json
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()
while True:
client, addr = server.accept()
while True:
cmd = client.recv(1024).decode("utf-8")
p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
# 真实数据
data = p.stdout.read() + p.stderr.read()
# 头部数据
head = {"data_size":len(data),"额外信息":"额外的值"}
head_data = json.dumps(head).encode("utf-8")
#头部长度
head_len = struct.pack("i",len(head_data))
#逐个发送
client.send(head_len)
client.send(head_data)
client.send(data)
至此粘包问题就完美解决了


浙公网安备 33010602011771号