粘包问题开始先来一个知识点回顾:
执行命令的话,肯定是用我们学过的subprocess模块啦,但注意注意注意:
res = subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdout=subprocess.PIPE)
命令结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码,且只能从管道里读一次结果
由代码引申出下面的问题:
服务端–简单版:
import socket
# 1:买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print(phone)
# 2:绑定手机卡
phone.bind(('127.0.0.1',8081))#0-65535:0-1024给操作系统使用
# 3:开机,5:可以预览的网页数
phone.listen(5)
# 4:等电话连接
print('starting...')
conn,client_addr=phone.accept()
# 5:收,发消息
data=conn.recv(1024)#1:单位:bytes 2:1024代表最大接收1024个bytes
print('客户端的数据',data)
conn.send(data.upper())
# 6:挂电话
conn.close()
# 7:关机
phone.close()
客户端–简单版:
import socket
# 1:买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print(phone)
# 2:拨号
phone.connect((' 127.0.0.1',8081))
# 3:发,收消息
phone.send('hello'.encode('UTF-8'))
data =phone.recv(1024)
print(data)
# 4:关闭
phone.close()
粘包:
什么是粘包?
在执行命令时,你并不知道用户输入的信息有多大,而你的系统一开始是固定大小的,你输入的数据较小可能你就已经拿到了你想要得到的数据了,此时心情愉悦呀,想着我已经掌握网络编程基础了真不错,但是别高兴的太早:
此时执行一个结果比较长的命令,比如top -bn 1, 你发现依然可以拿到结果,但如果再执行一条df -h的话,就发现,你拿到并不是df命令的结果,而是上一条top命令的部分结果
因为,top命令的结果比较长,但客户端只recv(1024), 可结果比1024长呀,那怎么办,只好在服务器端的IO缓冲区里把客户端还没收走的暂时存下来,等客户端下次再来收,所以当客户端第2次调用recv(1024)就会首先把上次没收完的数据先收下来,再收df命令的结果。
那遇这类问题,有人就想着,有些同学说,直接把recv(1024)改大不就好了,改成5000\10000或whatever. 可我的亲,这么干的话,并不能解决实际问题,因为你不可能提前知道对方返回的结果数据大下,无论你改成多大,对方的结果都有可能比你设置的大,另外这个recv并不是真的可以随便改特别大的,有关部门建议的不要超过8192,再大反而会出现影响收发速度和不稳定的情况
这个现象叫做粘包,就是指两次结果粘到一起了。它的发生主要是因为socket缓冲区导致的
粘包问题只存在于TCP中,Not UDP
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
粘包问题–终极版
代码实现:
客户端,client
import socket
import struct
import json
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',9909))
while True:
#1、发命令
cmd=input('>>: ').strip() #ls /etc
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) #1024是一个坑
recv_data+=res
recv_size+=len(res)
print(recv_data.decode('utf-8'))
phone.close()
注意:如果是Windows系统需要把客户端倒数第二行的编码格式换为GBK,因为Windows系统不兼容,否则会错,如果是Mac系统其他系统的就不需要改
服务端:
import socket
import subprocess
import struct
import json
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',9908))
phone.listen(5)
print('starting...')
while True:
conn,client_addr=phone.accept()
print(client_addr)
while True:#通信循环
try:
# 1:收命令
cmd=conn.recv(8096)
if not cmd:break#适用于Linux操作系统
# 2:执行命令,拿到结果
obj=subprocess.Popen(cmd.decode('utf-8'), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout=obj.stdout.read()
stderr=obj.stderr.read()
# 3:把命令的结果返回给客户端
# 第一步:制作固定长度的报头
header_dic={
'filename':'a.txt',
'md5':'xxxdxxxx',
'total_size':len(stdout)+len(stderr)
}
header_json=json.dumps(header_dic)
header_bytes=header_json.encode('utf-8')
# 第二步:先发送报头的长度
conn.send(struct.pack('i',len(header_bytes)))
# 第三步:再发报文
conn.send(header_bytes)
# 第四步:再发送真实的数据
conn.send(stdout)
conn.send(stderr)
except ConnectionResetError:
break
conn.close()
phone.close()