- 什么是粘包?
回答这个问题首先要先解释一下TCP、UDP的工作原理
TCP:(transport control protocol,传输控制协议)流式协议。在socket中TCP协议是按照字节数进行数据的收发,数据的发送方发出的数据往往接收方不知道数据到底长度是多长,而TCP协议由于本身为了提高传输的效率,发送方往往需要收集到足够的数据才会进行发送。使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
UDP:(user datagram protocol,用户数据报协议)数据报协议。在socket中udp协议收发数据是以数据报为单位,服务端和客户端收发数据是以一个单位,所以不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
TCP协议不会丢失数据,UDP协议会丢失数据。
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
- 什么情况下会发生粘包?
第一种情况:由于TCP协议的优化算法,当单个数据包较小的时候,会等到缓冲区满才会发生数据包前后数据叠加在一起的情况。然后取的时候就分不清了到底是哪段数据,这是第一种粘包。
第二种情况:当发送的单个数据包较大超过缓冲区时,收数据方一次就只能取一部分的数据,下次再收数据方再收数据将会延续上次为接收数据。这是第二种粘包。
粘包的本质问题就是接收方不知道发送数据方一次到底发送了多少数据,解决问题的方向也是从控制数据长度着手,也就是如何设置缓冲区的问题
- 粘包示例
- 粘包出现在客户端
数据比较小
'''服务端'''
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 9901))
server.listen(5)
conn, addr = server.accept()
res1 = conn.recv(1024)
print('第一次', res1)
res2 = conn.recv(1024)
print('第二次', res2)
结果:
第一次 b'helloworld'
第二次 b''
'''客户端'''
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 9901))
client.send('hello'.encode('utf-8'))
client.send('world'.encode('utf-8'))
粘包现象一定发生?不一定
'''服务端'''
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 9901))
server.listen(5)
conn, addr = server.accept()
res1 = conn.recv(1024)
print('第一次', res1)
res2 = conn.recv(1024)
print('第二次', res2)
结果:
第一次 b'hello'
第二次 b'world'
'''客户端'''
import socket
import time
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 9901))
client.send('hello'.encode('utf-8'))
time.sleep(1)
client.send('world'.encode('utf-8'))
必须是时间间隔比较小且数据比较少才会出现粘包现象
- 粘包出现在服务端
限制接收的字节
'''服务端'''
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 9901))
server.listen(5)
conn, addr = server.accept()
res1 = conn.recv(1)
print('第一次', res1)
res2 = conn.recv(1024)
print('第二次', res2)
结果:
第一次 b'h'
第二次 b'ello'
'''客户端'''
import socket
import time
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 9901))
client.send('hello'.encode('utf-8'))
time.sleep(5)
client.send('world'.encode('utf-8'))
服务端等待的时间大于客户端等待的时间
'''服务端'''
import socket
import time
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 9901))
server.listen(5)
conn, addr = server.accept()
res1 = conn.recv(1)
print('第一次', res1)
time.sleep(6)
res2 = conn.recv(1024)
print('第二次', res2)
结果:
第一次 b'h'
第二次 b'elloworld'
'''客户端'''
import socket
import time
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 9901))
client.send('hello'.encode('utf-8'))
time.sleep(5)
client.send('world'.encode('utf-8'))
如果能准备知道客户端发送的数据字节长度,服务端设置一样的接收字节长度,就可以避免出现粘包现象
- 如何解决粘包问题?
简单版本:使用struct 模块(结构体)
'''服务端'''
import socket
import struct
import subprocess
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8090))
phone.listen(5)
print('starting======')
while True:
conn, client_addr = phone.accept() # conn是套接字对象
print(client_addr)
while True:
try:
# 1、收命令
cmd = conn.recv(1024)
if not cmd: break
# 2、执行命令,拿到结果
obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stdout.read()
# 3、把命令的结果返回给客户端
# 第一步:制作固定长度的报头
total_size = len(stdout) + len(stderr)
header = struct.pack('i', total_size) # 'i'代表一种特定的格式类型,固定长度为4
# 第二步:把报头(固定长度)发送给客户端
conn.send(header)
# 第三步:再发送真实的数据
conn.send(stdout)
conn.send(stderr)
except ConnectionResetError:
break
conn.close()
phone.close()
'''客户端'''
import socket
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8090))
while True:
# 1、发命令
cmd = input('>>').strip()
if not cmd: continue
phone.send(cmd.encode('utf-8'))
# 2、拿命令的结果,并打印
# 第一步:先收报头
header = phone.recv(4)
# 第二步:从报头中解析处对真实数据的描述信息(数据的长度)
total_size = struct.unpack('i', header)[0]
# 第三步:接收真实的数据
recv_size = 0
recv_data = b''
while recv_size < total_size:
res = phone.recv(1024)
recv_data += res
recv_size += len(res)
print(recv_data.decode('GBK'))
phone.close()
终极版
'''服务端'''
import json
import socket
import struct
import subprocess
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8090))
phone.listen(5)
print('starting======')
while True:
conn, client_addr = phone.accept() # conn是套接字对象
print(client_addr)
while True:
try:
# 1、收命令
cmd = conn.recv(1024)
if not cmd: break
# 2、执行命令,拿到结果
obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stdout.read()
# 3、把命令的结果返回给客户端
# 第一步:制作固定长度的报头
header_dict = {
'filename': 'a.txt',
'md5': 'xxx',
'total_size': len(stdout) + len(stderr)
}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode('utf-8')
# 第二步:把报头(固定长度)发送给客户端
conn.send(struct.pack('i', len(header_bytes)))
# 第三步:再发送真实的数据
conn.send(header_bytes)
except ConnectionResetError:
break
conn.close()
phone.close()
'''客户端'''
import json
import socket
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8090))
while True:
# 1、发命令
cmd = input('>>').strip()
if not cmd: continue
phone.send(cmd.encode('utf-8'))
# 2、拿命令的结果,并打印
# 第一步:先收报头长度
obj = phone.recv(4)
header_size = struct.unpack('i', obj)[0]
# 第二步:再收报头
header_bytes = phone.recv(header_size)
# 第三步:从报头中解析处对真实数据的描述信息
header_json = header_bytes.decode('utf-8')
header_dic = json.loads(header_json)
print(header_dic)
total_size = header_dic['total_size']
# 第四步:接收真实的数据
recv_size = 0
recv_data = b''
while recv_size < total_size:
res = phone.recv(1024)
recv_data += res
recv_size += len(res)
print(recv_data.decode('GBK'))
phone.close()