粘报问题原因:
发生黏包主要是因为接收者不知道发送者发送内容的长度,因为tcp协议是根据数据流的,计算机操作系统有缓存机制,所以当出现连续发送或连续接收的时候,发送的长度和接收的长度不匹配的情况下就会出现黏包。(Nagle算法:将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包)下面说几个处理方法:
基于tcp协议特点的黏包现象成因
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束。此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
解决方案:
可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。
struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
import struct m = struct.pack('i', 11111111) # pack封装成包.pack(fmt,values):fmt表示存储类型i,values表示值 print(m) j = struct.unpack('i', m) # unpack把包解封 print(j)
结果:
b'xc7x8axa9x00' (11111111,)
其中fmt可以选择的类型如下:
使用struct解决黏包
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。
发送时 | 接收时 |
先发送struct转换好的数据长度4字节 | 先接受4个字节使用struct转换成数字来获取要接收的数据长度 |
再发送数据 |
再按照长度接收数据 |
实例(简单的文件传输):
服务器:
import socket import subprocess import struct import json phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(('127.0.0.1',9909)) #0-65535:0-1024给操作系统使用 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':'xxdxxx', '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: #适用于windows操作系统 break conn.close() phone.close()
客户端:
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('gbk')) phone.close()
分析总结:可以看出先定制报头,把有用的信息,比如数据长度,文件名等制成报头。然后用Pack封装成定长的包,在客户接受由于定长所以recv(4),然后通过unpack的解封,可以得到报头中的相关信息,比如数据长度,然后通过数据长度可以精确的接受文件数据。就算后面再传信息也不用担心粘包问题。