一:什么是粘包?
粘包指的是数据与数据之间没有明确的分界线,导致不能正确读取数据。(只有TCP有粘包现象,UDP永远不会黏包)
要理解粘包问题,需要先理解TCP协议传输数据时的具体流程,TCP也称之为流式协议(UDP称为数据报协议)
发送端可以一K一K的发送数据,而接收端的应用程序可以两K两K的踢走数据,当然也有可能一次提走8K9K的数据,或者只提走几个字节的数据,也就是说,应用程序所看到的数据就是一个整体,或者说是一个数据流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据.
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
二:粘包问题出现的具体原因
应用程序无法直接操作硬件,应用程序想要发送数据必须要将数据交给操作系统,而操作系统许哟啊同时为所有应用程序提供数据传输服务,也就意味着操作系统不可能立马将应用程序的数据发送出去,就需要为应用程序提供一个缓冲区,用于临时存放数据,具体流程如下:
发送方:
当应用程序调用send函数时,应用程序会将数据从与应用程序拷贝到操作系统缓存,再由操作系统从缓冲区读取数据并发送出去。
接收方:
对方计算机收到数据也是操作系统先收到,至于应用程序何时处理这些数据,操作系统并不清楚,所以同样需要将数据先存储到操作系统的缓冲区中,当应用程序调用recv时,实际上是从操作系统缓冲区中将数据拷贝到应用程序的过程。
UDP:
UDP在收发数据时是基于数据包的,即一个包一个包的发送,包与包之间有着明确的分界,到达对方操作系统缓冲区之后也是一个一个独立的数据包,接收方从操作系统缓冲区中奖数据包拷贝到应用程序。
这种方式存在的问题是:
1.发送方发送的数据长度每个操作会有不同的限制,数据超过限制则无法发送
2.接收方接收数据时如果应用程序提供的缓存容量小于数据包的长度将造成数据丢失,而缓冲区大小不可能无限大
TCP:
当我们需要传输较大的数据,或者需要保证数据的完整性时,最简单的方式就是使用TCP协议,TCP增加了一套校验规则来保证数据的完整性,会超过TCP包最大长度的数据拆分为多个TCP包,并在传输数据时为每一个TCP数据包指定一个顺序号,接收方在收到TCP数据包后按照顺序将数据包进行重组,重组后的数据全是二进制数据,且每次收到的二进制数据之间没有明确的分界。
TCP在两种情况下会发生粘包问题:
1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据很小,会合到一起,产生粘包)
2.接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务器下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
三:如何解决粘包
1.基础解决方案:在发送数据前先发送数据长度
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)
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"))
由于negle优化机制的存在,长度信息和数据还是有可能会粘包,而接收方并不知道长度信息具体几个字节,所以现在的问题是如何将长度做成一个固定长度的bytes数据。(程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗)
2.为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时先从缓存中取出定长的报头,然后再取出真实数据。
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 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)
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"))
3.自定义报头解决粘包
上述方案已经完美解决了念包问题,但是扩展性不高,例如上传下载文件的话,不仅要传输文件数据,还需要传输文件名字,md5值等等,实现思路如下:
发送端:
1.先将所有的额外信息打包到一个头中
2.然后先发送头部数据
3.最后发送真实数据
接收端:
1.接收固定长度的头部长度数据
2.根据长度数据获取头部数据
3.根据头部数据获取真实数据
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)
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"))