黏包现象
1 黏包现象演示
服务端
#服务端 import socket sk = socket.socket() # 注册主机到网络 sk.bind( ("127.0.0.1",9000) ) sk.listen() # 三次握手 conn,addr = sk.accept() # 收发数据的逻辑 # ... conn.send("hello,".encode("utf-8")) #发送第一次包 conn.send("world".encode("utf-8")) #发送第二次包 # 四次挥手 conn.close() # 退回端口 sk.close()
客户端
import socket sk = socket.socket() sk.connect( ("127.0.0.1",9000) ) # 收发数据的逻辑 print(sk.recv(10)) #接收第一次,每一个包的大小是10个字节 print(sk.recv(10)) #接收第二次,每一个包的大小是10个字节 sk.close()
执行
[root@node10 tcp]# python3 server.py [root@node10 tcp]# python3 client.py b'hello,worl' #发现第二个包,黏到第一个包了 b'd'
2 黏包现象原因分析
tcp协议在发送数据时,会出现黏包现象
- 数据粘包是因为在客户湍/服务器端都会有一个数据缓中区缓中区用来临时保存数据,为了保证能够完整的接收到数据,因此缓中区都会设置的比较大。
- 在收发效据频繁时,由于tcp传输消息的无边界,不清楚应该截取多少长度,导致客户端/服务器端,都有可能把多条数据当成是一条数据进行截取,造成黏包
黏包的两种情况
- 黏包现象一:在发送端,由于两个数据短,发送的时间隔较短,所以在发送端形成包
- 黏包现象二:在接收端,由于两个数据几乎同时被发送到对方的缓存中,所有在接收端形成了黏包
- 总结:发送端,包之间时间间隔短或者接收端,接受不及时,就会黏包,核心是因为tcp对数据无边界戳取,不会按照发送的顺序判断
3 解决1,发送两个包的时间设置时间间隔
服务端
#服务端 import socket import time sk = socket.socket() # 注册主机到网络 sk.bind( ("127.0.0.1",9000) ) sk.listen() # 三次握手 conn,addr = sk.accept() # 收发数据的逻辑 # ... conn.send("hello,".encode("utf-8")) time.sleep(0.2) conn.send("world".encode("utf-8")) # 四次挥手 conn.close() # 退回端口 sk.close()
客户端
import socket sk = socket.socket() sk.connect( ("127.0.0.1",9000) ) # 收发数据的逻辑 print(sk.recv(10)) print(sk.recv(10)) sk.close()
执行
[root@node10 tcp]# python3 server.py [root@node10 tcp]# python3 client.py b'hello,' b'world'
数据正常
但是服务端把数据发送到缓冲区,是正常的,但是客户端处理数据太慢,也会导致出现粘包现象
服务端
#服务端 import socket import time sk = socket.socket() # 注册主机到网络 sk.bind( ("127.0.0.1",9000) ) sk.listen() # 三次握手 conn,addr = sk.accept() # 收发数据的逻辑 # ... conn.send("hello,".encode("utf-8")) time.sleep(0.2) conn.send("world".encode("utf-8")) # 四次挥手 conn.close() # 退回端口 sk.close()
客户端
import socket import time sk = socket.socket() sk.connect( ("127.0.0.1",9000) ) time.sleep(0.5) # 收发数据的逻辑 print(sk.recv(10)) print(sk.recv(10)) sk.close()
执行
[root@node10 tcp]# python3 server.py [root@node10 tcp]# python3 client.py b'hello,worl' b'd'
又出现了粘包现象
解决黏包现象的方法:让发送端不要发送时间间隔慢一点,同时让接收端接收数据不要延迟太久,但是这种方式不是根本解决方式
根本原因是tcp再接收数据时是没有边界的不知道截取多少个长度时第一次发送过来的数据,但是udp不会发生黏包现象,是因为UDP再发送数据,会首先计算一个数据的长度,是多少就发送多少
4 解决2,计算发送长度
服务端
#服务端 import socket sk = socket.socket() # 注册主机到网络 sk.bind( ("127.0.0.1",9000) ) sk.listen() # 三次握手 conn,addr = sk.accept() # 收发数据的逻辑 # ... conn.send("6".encode("utf8")) #计算出第一个包的长度是6字节,把这个数值发送给客户端 conn.send("hello,".encode("utf-8")) conn.send("world".encode("utf-8")) # 四次挥手 conn.close() # 退回端口 sk.close()
客户端
import socket sk = socket.socket() sk.connect( ("127.0.0.1",9000) ) n = int(sk.recv(1).decode("utf-8")) #接收服务端发送过来的字节长度,强制转换为整型 # 收发数据的逻辑 print(sk.recv(n)) print(sk.recv(10)) sk.close()
执行
[root@node10 tcp]# python3 server.py [root@node10 tcp]# python3 client.py b'hello,' b'world'
因为tcp 无边界的特征: 按照recv后面括号里面的值来进行接受数据,完全有可能在客户端 因为延迟问题,导致两次发送的数据黏在一起,因为tcp无边界,按照实际10个字节进行截图,根本分不清是第几次发送的. 从而黏包.
解决的方式:告诉客户端,直接你要截取的字节数是多少,按照send发送的实际字节数,进行截取.
当发送文件的字节过大,需要不停的修改接收端的值
5 解决3,使用8字节固定传输
#服务端 import socket sk = socket.socket() # 注册主机到网络 sk.bind( ("127.0.0.1",9000) ) sk.listen() # 三次握手 conn,addr = sk.accept() # 收发数据的逻辑 # ... conn.send("00000120".encode("utf8")) #计算出第一个包的长度是8位写法的字节长度,把这个数值发送给客户端 msg = "hello," * 20 conn.send(msg.encode("utf-8")) conn.send("world".encode("utf-8")) # 四次挥手 conn.close() # 退回端口 sk.close()
客户端
import socket sk = socket.socket() sk.connect( ("127.0.0.1",9000) ) n = int(sk.recv(8).decode("utf-8")) #接收服务端发送过来的字节长度,固定8字节,强制转换为整型 # 收发数据的逻辑 print(sk.recv(n)) print(sk.recv(10)) sk.close()
执行
[root@node10 tcp]# python3 server.py [root@node10 tcp]# python3 client.py b'hello,hello,hello,hello,hello,hello,hello,hello,hello,hello,hello,hello,hello,hello,hello,hello,hello,hello,hello,hello,' b'world'
使用8个字节进行固定发送的时,可以解决平常大多数问题,但是没有从根本解决所有数据能都发送接收的情况.长度上还是受限制.
6 struct模块
- pack 把任意长度的数字转化成具有固定长度的4个字节的值,组成字节流
- unpack 把4个字节的值恢复成原有数据,最终返回的是一个元组
i => int 要转化的这个数据类型是整型
res = struct.pack("i",130000000) print(res) print(len(res)) res = struct.unpack("i",res) print(res)
执行
[root@node10 python]# python3 test.py b'x80xa4xbfx07' 4 (130000000,)
7 使用struct解决黏包现象
服务端
#服务端 import socket import struct sk = socket.socket() # 注册主机到网络 sk.bind( ("127.0.0.1",9000) ) sk.listen() # 三次握手 conn,addr = sk.accept() # 收发数据的逻辑 inp = input ("please input your vaule>>>>:") msg = inp.encode("utf-8") #使用pack方法,直接算出4个字节长度的二进制字节流 res = struct.pack("i",len(msg)) conn.send(res) conn.send(msg) conn.send("world".encode("utf-8")) # 四次挥手 conn.close() # 退回端口 sk.close()
客户端
import socket import struct sk = socket.socket() sk.connect( ("127.0.0.1",9000) ) #发送的已经固定的4个字节 n = sk.recv(4) #获取元组中的第一个值,因为返回的是元组 n = struct.unpack("i",n)[0] # 收发数据的逻辑 print(sk.recv(n)) print(sk.recv(10)) sk.close()
执行
[root@node10 tcp]# python3 server.py please input your vaule>>>>:cdhaueqfebca ferngcdoascj [root@node10 tcp]# python3 client.py b'cdhaueqfebca ferngcdoascj' b'world'
粘包问题解决