socket系统调用会将要发送的数据从用户空间copy到内核空间,这样频繁的交换操作会耗费资源,为提高效率,会收集到较多的数据才一起发送。如果数据少的几个包一起发送就会造成粘包。
socket 为提高传输效率,发送方往往要收集到足够多的数据后才发送一次数据给对方。若连续几次需要send的数据都很少,通常TCP socket 会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据
发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
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是什么意思?
粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据。
s.send('hello'.encode('utf-8'))
s.send('feng'.encode('utf-8'))
data1=conn.recv(2) #一次没有收完整
data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的
在两个连续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