只有TCP有粘包问题,而UDP永远不会粘包。我们先掌握一个socket收发消息的原理:
服务端可以1kb,1kb地发向客户端送数据,客户端的应用程序可以在缓存当中2kb,2kb地取走数据,当然也可以更多,或都更少。也就是说,应用程序看到的数据是来个整体。或者说是一个流。一条消息有多少字节对应用程序是不可见的,TCP协议是面向流的协议,这就是它容易粘包的问题原因。
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一个消息要提取多少字节的数据所造成的。
此外,发送方引起的粘粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往收集到足够多的数据后才一个TCP段。若连续几次需要发送的数据都很少,通常TCP会根据(Nagle)优化算法,把这些数据合成一个TCP段后发出去,这样接收方就收到了粘包数据。
粘包情况一:发送端要等缓冲区满才发送出去(即发送的数据量少且时间间隔短,会合到一起),造成粘包。
1、服务端
from socket import *
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
conn,client_addr=server.accept()
res1=conn.recv(1024)
print('第一次:',res1)
res2=conn.recv(1024)
print('第二次:',res2)
conn.close()
server.close()
2、客户端
from socket import *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
client.send(b'hello')
client.send(b'world')
client.close()
先启动服务端,后再启动客户端,服务端得到的结果为:
粘包情况二:客户端发关了一段数据,服务端只收了一小部分,服务端下次再收的时候不是从缓冲区拿上次遗留的数据,产生粘包。
情况一的,客户端不变,服务端略作修改,如下:
from socket import *
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
conn,client_addr=server.accept()
res1=conn.recv(2)
print('第一次:',res1)
res2=conn.recv(3)
print('第二次:',res2)
conn.close()
server.close()
先启动服务端,后再启动客户端,服务端得到的结果为:
现在我们来想一下如何处理粘包的方法。粘包的问题根源是接收端不知发送端将要传送的字节流,所以我们要让发送端在发送数据前,把要发送的字节流总大小让接收端知晓,然后接收端来个循环将其全部接收。这种方法比较低级,因为程序的运行速度运快于网络传输速度,所以在发送一段字节前,先发送该字节流的长度,这种方式会放大网络延迟带来的性能损耗。
目前比较合理的处理方法是:为字节流加上一个报头,将这个报告做成字典,字典里包含将要发送的真实数据详细信息。将这个字典JSON序列化,然后用struck(不了解struck可看最后补充)将序列化后的数据长度打包成4个字节(4个字节完全够用)
发送时:先发报头长度,再编码报头内容后发送,最后发真实数据。
接收时:用struct取出报头长度,然后根据长度取出报头,解码,反序列化。最后从反序列化的结果中取出待取数据的详细信息,然后取真实的数据内容。
来看实现的代码:
1、服务端
from socket import *
import subprocess
import struct
import json
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn,client_addr=server.accept()
print(conn,client_addr)#(连接对象,客户端的ip和端口)
while True:
try:
cmd=conn.recv(1024)
obj=subprocess.Popen(
cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout=obj.stdout.read()
stderr=obj.stderr.read()
#1、制作报头
header_dic={
'total_size':len(stdout)+len(stderr),
'md5':'dgdsfsdfdsdfsfewrewge',
'file_name':'a.txt'
}
header_json=json.dumps(header_dic)
header_bytes=header_json.encode('utf-8')
#2、先发送报头的长度
header_size=len(header_bytes)
conn.send(struct.pack('i',header_size))
#3、发送报头
conn.send(header_bytes)
#4、发送真实的数据
conn.send(stdout)
conn.send(stderr)
except ConnectionResetError:
break
conn.close()
server.close()
2、客户端
from socket import *
import json
import struct
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
cmd=input(">>:").strip()
if not cmd:continue
client.send(cmd.encode('utf-8'))
#1、接收报文头的长度
header_size=struct.unpack('i',client.recv(4))[0]
#2、接收报文
header_bytes=client.recv(header_size)
#3、解析报文
header_json=header_bytes.decode('utf-8')
header_dic=json.loads(header_json)
print(header_dic)
#4、获取真实数据的长度
totol_size=header_dic['total_size']
#5、获取数据
recv_size=0
res=b''
while recv_size<totol_size:
recv_date=client.recv(1024)
res+=recv_date
recv_size+=len(recv_date)
print(res.decode('gbk'))
client.close()
补充struct模块:
import struct
#struct是用来将整型的数字转成固定长度的bytes.
import json
header_dic={
'total_size':32322,
'md5':'gdssfsfsdfsf',
'filename':'a.txt'
}
#1、将报头字典序列化。
header_json=json.dumps(header_dic)
#2、将序列后的字典转成字节
header_bytes=header_json.encode('utf-8')
#3、获取序列的字字典转成字节的个数
header_size=len(header_bytes)
print(header_size)
#4、将这个个数转成固字长度的字节表示
obj=struct.pack('i',header_size)
print(obj,len(obj))
#、这个固定长度的字节经过反转后是一个元组。
res=struct.unpack('i',obj)
#、通过按索取值就可等到报头字典长度。
header_size=res[0]