Python socket粘包问题(最终解决办法)
套接字:
就是将传输层以下的协议封装成子接口
对于应用程序来说只需调用套接字的接口,写出的程序自然是遵循tcp或udp协议的
实现第一个功能个:
实现:通过客户端向服务端发送命令,调取windows下面的cmd窗口,将服务端执行命令的结构,返回并显示在
客户端窗口上。
subprocess:
1.可以将执行结果返回
2.返回值是bytes类型
(基于这两点,可以应用在server端,将服务端的返回直接以bytes的格式直接send给客户端,
实现在客户端的显示)
问题1:粘包问题
粘包问题:实际是由TCP协议在传输数据时的2大特性来的
TCP协议又叫流式协议,在数据传输上,只要客户端发送相应的数据请求,
服务端会将数据像流水一样不断的send给客户端
基于这个特点,就会存在一个问题,当客户端给服务端发送一条命令,服务端成功接收并将命令的
结果返回到客户端的时候,由于客户端recv()的数量限制,可以一次不能完全取出,
这个时候就会存在,下次输入命令客户端首先拿到的返回值就是上次残留的没有收完的数据
基于粘包问题的解决思路就是:
发数据之前先把报头发给对方,让对方先知道要收的报头的长度,后面再传数据文件
自定义报头:
为甚么要自定义报头:
因为struck path(‘i’,3443242)
1.’i‘:类型不同,后面数字的长度大小也不同,大小是有限的(当超出范围时会报错)
2.因为报头里面含有的内容可能不仅仅只有total_siz还有filename、hash等等,知识单纯的把total_size
当做报头传入不合理,所以我们要自定义报头
Server端配置
from socket import * import socket,subprocess,struct,json server = socket.socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8080)) server.listen(5) while True: conn,client = server.accept() print(client) while True: try: cmd = conn.recv(1024) if len(cmd) == 0: break obj=subprocess.Popen( cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) out=obj.stdout.read() err=obj.stderr.read() #制作报头 header_dic={ 'filename':'a.txt', 'total_size':len(out)+len(err), 'hash':'abc32i5o24' } #对包头进行序列化 header_json=json.dumps(header_dic) #字符串格式 header_bytes=header_json.encode('utf-8') #1.先发型报头的长度 len(header_bytes) struck为固定的4个字节 conn.send(struct.pack('i',len(header_bytes))) #2.发送报头 conn.send(header_bytes) #3.发送真是数据 conn.send(out) conn.send(err) except ConnectionResetError: break conn.close() server.close()
Client端配置
from socket import * import socket,struct,json client=socket.socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: cmd=input('输入你要操作的命令:') client.send(cmd.encode('utf-8')) if len(cmd) == 0:continue #1.先收报头的四个字节,首先拿到报头传来的长度-》bytes header=client.recv(4) #i类型足够了 header为bytes类型 header_size=struct.unpack('i',header)[0] #拿到元祖形式,取第一个就是整个报头的长度
print(header_size) #为报头的长度值 #2.再收报头(对应服务端的conn.send(header_bytes)) header_bytes=client.recv(header_size) #根据报头的固定长度去收接收 #3.解析包头(就是将header_bytes文件先解码成json格式) header_str=header_bytes.decode('utf-8') header_dic=json.loads(header_str) print(header_dic) total_size=header_dic['total_size'] print(total_size) recv_size=0 #定义一个初始的接收变量为0,只是个计数变量,为了统计与总的total_size的len大小 res=b'' while recv_size < total_size: recv_data=client.recv(1024) #每次传过来的recv_data是bytes类型 res+=recv_data recv_size+=len(recv_data) #循环增加每次接收值的长度 # cmd=client.recv(1024) print(res.decode('gbk')) client.close()
粘包问题的最终解决方案,分析:
服务端:
目的为了自定义报头(报头中不仅包含长度,可能还有文件名等信息) subprocess ... 1.制作报头(字典形式) header_dic={ 'filename':'a.txt', 'total_size':len(out)+len(err), 'hash':'abc32i5o24' } 2.通过json将报头序列化再encode为bytes类型 header_json=json.dumps(header_dic) #字符串类型 header_bytes=header_json.encode('utf-8') #bytes类型 3.发送报头的长度,struck目的是固定封装好的报头为4个字节长度 =====对应客户端刚开始 header=client.recv(4) struck: 1.将数字转为bytes类型,保证发送过去的是bytes类型 2.固定4个字节 第一次发:conn.send(struck.pack('i',len(header_bytes))) 先传给客户端固定了收的时候报头的长度,不至于报头和其他内容粘在一起 4.发送报头 =======对应客户端接收报头 header_bytes=client.recv(header_size) 第二次发:conn.send(header_bytes) 5.发送真实的数据 conn.send(out) conn.send(err)
客户端:(bytes--int) 1.通过服务端返回的字节,拿到报头的的长度 header=client.recv(4) #header是4个bytes字节 header_size=struck.unpack('i',header)[0] #字节头-拿到int大小 2.再收报头 header_bytes=client.recv(header_size) #bytes类型 3.解析报头(1.先把内存中存放的bytes类型,用decode('utf-8')解码为字符串) header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) #json反序列化出字典格式 (对应server第2步) print(header_dic) 4.拿到字典中的报头大小 total_size=header_dic['total_size'] print(total_size)
struck功能辅助理解:所以客户端刚开始接收4 大小是足够把报头接收完的。
# struck工鞥理解: import struct #将整型转成bytes res=struct.pack('i',1232435436) print(res,len(res)) #res:为bytes类型,还是固定长度4(一般情况已经可以包含很多) #将bytes转成整型 aa=struct.unpack('i',res) print(aa,aa[0],type(aa)) # print(len(res)) #4 一般情况多数bytes 4个长度足够了 """结果 b'\xecxuI' 4 (1232435436,) 1232435436 <class 'tuple'> 4 """