socktet粘包问题解决

socket粘包问题

socket系统调用会将要发送的数据从用户空间copy到内核空间,这样频繁的交换操作会耗费资源,为提高效率,会收集到较多的数据才一起发送。如果数据少的几个包一起发送就会造成粘包。

  • 只有tcp可能存在粘包问题:
    TCP基于字节流,没有消息边界、数据包的概念,应用层协议如果没有使用基于长度或基于终结符的消息边界,就会导致多个消息粘连,接收端无法。
  • udp永远不会粘包:
    udp保留了消息边界,每次操作发送一个IP数据报,不考虑分片。接收端每次都会收到一个完整的udp数据包,因此不会产生多个消息粘连的情况。

出现粘包的原因

socket 为提高传输效率,发送方往往要收集到足够多的数据后才发送一次数据给对方。若连续几次需要send的数据都很少,通常TCP socket 会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据

发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

  • TCP和UDP:

1、TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
2、UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
3、tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头

  • 为何TCP是可靠的,而UDP不可靠?
    1、tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的
    2、UDP发送数据,对端是不会返回确认信息的,因此不可靠

  • send(字节流)和recv(1024)及sendall是什么意思?

    • recv里指定的1024意思是从缓存里一次拿出1024个字节的数据
    • send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果字节流大小大于缓存剩余空间,那么数据丢失
    • sendall会循环调用send,数据不会丢失

粘包导致的问题

粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据。

粘包出现的场景

  • 1 发送端需要等到本机的缓冲区满了以后才发出去,造成粘包
    (发送数据时间间隔很短,数据很小,python使用了优化算法,合在一起,产生粘包)
s.send('hello'.encode('utf-8'))
s.send('feng'.encode('utf-8'))
  • 2 接收端不及时接受缓冲区的包,造成多个包接受
    (客户端发送一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,就产生粘包)
data1=conn.recv(2) #一次没有收完整
data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的

占包问题的解决方案

方案一、 使用sleep()睡眠

在两个连续send()之间增加sleep(),这样包会分开发送
在两个连续recv()之间增加sleep(),多个包分开接收
该解决方案会出现很多纰漏,因为你不知道什么时候传输完,时间暂停的长短都会有问题(长的话效率低,短的话不合适),所以这种方法是不合适的

方案二、 在数据包的头部加上内容的长度

为字节流加上自定义固定长度报头,报头中包含字节流长度,然后依次send到对端,对端在接受时,先从缓存中取出定长的报头,然后再取真实数据

使用struct模块对打包的长度为固定4个字节或者八个字节, struct.pack.format参数是“i”时,只能打包长度为10的数字.
为了解决长度限制的问题,可以使用JSON将长度转为JSON字符串并转为bytes用于网络传输

json.dumps(12345678900).encode('utf8')

把自己将要发送的字节流总大小让接收端知晓,伪码:

...
total_size = struct.pack('i', len(data)
coon.send(total_size) # 先发送data的总长度
coon.send(data)  # 再发送结果
...

接收端在接受时,先从缓存中取出定长的报头,然后一个死循环中取得所有数据:

...
total_size = struct.unpack('i', socket.recv(4))
while recv_size

本方案的问题在于缺乏的灵活度,仅能够通过报头获取数据的长度,而没有关于数据的其它信息

方案三

服务端将报头信息(数据长度、名称、md5等)优化,存储为字典并转化后发送给客户端;客户端接收到固定长度的报头信息解码为字典,接收数据。

服务端将报头信息优化:
1、 将要发送的数据用字典进行描述,包括数据的大小、文件名、md5校验值等需要的数据相关信息
2、 将字典编码为JSON字符串格式
3、 将JSON字符串编码为字节码,用于网络传输
4、 使用struck将字节码的长度转为固定长度

服务端伪码:

# body_data是需要发送的数据体
header = {'total_size': len(body_data),
          'filename': None,
          'md5': None}
header_json = json.dumps(header).encode('utf8')

# 发送报头长度和报头
self.request.send(struct.pack('i', len(header_json)))
self.request.send(header_json)

# 发送数据
self.request.sendall(f'{data}'.encode('utf8')) # 发送全部数据

客户端接收报头信息:
1、接收报头的固定长度(比如为4)信息,得到报头的长度
2、按报头长度接收报头,并解码为JSON字符串
3、反序列化,将JSON字符串解码为dict
4、使用报头dict信息获取需要接收的数据体的大小,死循环接收。

客户端伪码:

# 1. 接收固定长度的报头
header_length = struct.unpack('i', tcpCliSock.recv(4))[0]
print(f'header_length={header_length}')

# 2. 接收报头,并解码、JSON反序列化得到dict
header_json = tcpCliSock.recv(header_length).decode('utf8')
header = json.loads(header_json)

length = header['total_size']
while recv_num < length:
    mes = tcpCliSock.recv(1024).decode('utf8')
    recv_num += len(mes)
    data += mes

你可能感兴趣的:(网络,python,socket)