python 黏包现象及其解决方案
一、数据缓冲区
缓冲区(buffer),它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,显然缓冲区是具有一定大小的。
二、为什么要缓冲区 (详情参考:https://www.cnblogs.com/mlgjb/p/7991903.html)
1.可以解除高速设备与低速设备的不匹配,高速设备需要等待低速设备的制约关系,数据可以直接送往缓冲区,高速设备不用再等待低速设备,提高了计算机的效率。
2.可以减少数据的读写次数,如果每次数据只传输一点数据,就需要传送很多次,这样会浪费很多时间,因为开始读写与终止读写所需要的时间很长,如果将数据送往缓冲区,待缓冲区满后再进行传送会大大减少读写次数,这样就可以节省很多时间。
三、粘包现象
1、小数据传输粘包
源代码:
import socket server=socket.socket() server.bind(('127.0.0.1',8001)) server.listen() conn,addr=server.accept() msg_1=conn.recv(1024).decode("utf-8") msg_2=conn.recv(1024).decode("utf-8") print("客户端>>>",msg_1) print("客户端>>>",msg_2) conn.close() server.close()
import socket import time client=socket.socket() client.connect(('127.0.0.1', 8001)) client.send(b'123') # time.sleep(0.1) 加入时间缓冲,让每次传入和接受的数据有序 client.send(b'456') client.close()
黏包现象粗略的解释:数据在传输过程中,未来得及按照先后次序传输和接受,数据都进入了数据缓冲区中,再接受的数据时候,不知道按照怎么的数据长度接受,就按照给定的1024的长度接收,因此出现了黏包.
解决方案:
方案一、传输过程中每次都告诉对方应该如何接收数据。
方案二、把传输和接受的间隔加大,保证每次都能顺利的满足一个接着一个传输。(time.sleep(0.1))
2、大数据传输粘包(模拟cmd指令)
源代码:
import subprocess import socket import time server=socket.socket()#创建socket对象 server.bind(('127.0.0.1',8001))#绑定ip_port server.listen()#监听 conn,addr=server.accept()#等待连接,获取连接通道和地址 while 1: time.sleep(0.1)#减少内存占用 cmd_msg=conn.recv(1024).decode("utf-8") #判断是否结束doc指令读取 if cmd_msg=="exit" or cmd_msg=="exit()": break else: #创建读取cmd指令对象 obj_sub=subprocess.Popen( cmd_msg, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) out_info=obj_sub.stdout.read() err_info=obj_sub.stderr.read() if err_info: #不存在该命令则输出错误信息,并打印字节数 conn.send(err_info) print(len(err_info)) else: #存在输入的命令则发送指令执行的结果,并打印字节数 conn.send(out_info) print(len(out_info)) conn.close() server.close()
import socket client=socket.socket()#创建客户端对象 client.connect(('127.0.0.1', 8001))#连接服务器的ip_port while 1: cmd=input("cmd>>>") client.send(cmd.encode("utf-8")) #判断是否为退出指令 if cmd=="exit" or cmd=="exit()": break else: # 接受客服端返回信息 out=client.recv(1024).decode("gbk") print(out) client.close()
结果显示:
1 C:\Python36\python.exe "F:/qishi/day 28 黏包 合法性链接/黏包现象/黏包现象2客户端.py" 2 cmd>>>ipconfig -all 3 4 Windows IP 配置 5 6 主机名 . . . . . . . . . . . . . : DESKTOP-MT7JLPA 7 主 DNS 后缀 . . . . . . . . . . . : 8 节点类型 . . . . . . . . . . . . : 混合 9 IP 路由已启用 . . . . . . . . . . : 否 10 WINS 代理已启用 . . . . . . . . . : 否 11 12 无线局域网适配器 WLAN: 13 14 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 15 连接特定的 DNS 后缀 . . . . . . . : 16 描述. . . . . . . . . . . . . . . : Qualcomm Atheros AR9485WB-EG Wireless Network Adapter 17 物理地址. . . . . . . . . . . . . : BC-30-7D-96-2D-2B 18 DHCP 已启用 . . . . . . . . . . . : 是 19 自动配置已启用. . . . . . . . . . : 是 20 21 无线局域网适配器 本地连接* 1: 22 23 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 24 连接特定的 DNS 后缀 . . . . . . . : 25 描述. . . . . . . . . . . . . . . : Microsoft Wi-Fi Direct Virtual Adapter 26 物理地址. . . . . . . . . . . . . : 1E-30-7D-96-2D-2B 27 DHCP 已启用 . . . . . . . . . . . : 是 28 自动配置已启用. . . . . . . . . . : 是 29 30 以太网适配器 以太网: 31 32 连接特定的 DNS 后缀 . . . . . 33 cmd>>>dir 34 . . : 35 描述. . . . . . . . . . . . . . . : Realtek PCIe GBE Family Controller 36 物理地址. . . . . . . . . . . . . : C4-54-44-F5-84-6A 37 DHCP 已启用 . . . . . . . . . . . : 是 38 自动配置已启用. . . . . . . . . . : 是 39 本地链接 IPv6 地址. . . . . . . . : fe80::64a8:6af:a71b:edad%5(首选) 40 IPv4 地址 . . . . . . . . . . . . : 192.168.12.51(首选) 41 子网掩码 . . . . . . . . . . . . : 255.255.255.0 42 获得租约的时间 . . . . . . . . . : 2018年11月26日 15:01:29 43 租约过期的时间 . . . . . . . . . : 2018年11月27日 15:01:28 44 默认网关. . . . . . . . . . . . . : 192.168.12.254 45 DHCP 服务器 . . . . . . . . . . . : 192.168.12.254 46 DHCPv6 IAID . . . . . . . . . . . : 63198276 47 DHCPv6 客户端 DUID . . . . . . . : 00-01-00-01-23-7C-84-72-C4-54-44-F5-84-6A 48 DNS 服务器 . . . . . . . . . . . : 114.114.114.114 49 TCPIP 上的 NetBIOS . . . . . . . : 已启用 50 51 cmd>>>
C:\Python36\python.exe "F:/qishi/day 28 黏包 合法性链接/黏包现象/黏包现象2服务器.py" ipconfig -all 命令执行结果长度 1912 dir 命令执行结果长度 505
现象简述:首先执行了一次ipconfig -all,他的结果数据长度是1912,而我们接受的数据长度是1024,之后又执行了一次dir,dir结果长度是505,我们再一次接受的数据,依旧是ipconfig -all 结果数据.出现了黏包现象.
黏包现象成因:执行的数据进入缓冲区,并且数据的大小大于接受数据的大小,因此我们一次接受只能接受1024个字节,之后又执行一个命令后,命令的结果又会进入数据缓冲区,所有我们接受的数据显示的是上一个命令残余的数据.我们每次接受的数据都是1024,还有可能出现多命令的数据同时出现的风险.
解决方案:
方案一、按照发送数据的长度接受数据,由于缓冲区数据容量有限,我们采用循环接受数据的方法接受大数据。
方案二、把数据的长度信息与数据信息合并一次性发给接受端,接收端先提出数据长度,再按照数据长度接受数据。
方案一源代码:
1 import subprocess 2 import socket 3 import time 4 server=socket.socket()#创建socket对象 5 6 server.bind(('127.0.0.1',8001))#绑定ip_port 7 server.listen()#监听 8 conn,addr=server.accept()#等待连接,获取连接通道和地址 9 10 while 1: 11 time.sleep(0.1)#减少内存占用 12 cmd_msg=conn.recv(1024).decode("utf-8") 13 #判断是否结束doc指令读取 14 if cmd_msg=="exit" or cmd_msg=="exit()": 15 break 16 else: 17 #创建读取cmd指令对象 18 obj_sub=subprocess.Popen( 19 cmd_msg, 20 shell=True, 21 stdout=subprocess.PIPE,#标准化输出 22 stderr=subprocess.PIPE #标准化错误输出 23 ) 24 out_info=obj_sub.stdout.read() 25 26 err_info=obj_sub.stderr.read() 27 28 29 if err_info: 30 #不存在该命令则输出错误信息,并打印字节数 31 data_len = len(err_info) 32 all_send_datalen=0 33 print(f"{cmd_msg} 命令执行结果长度", data_len) 34 #把数据长度发送给接收端 35 conn.send(str(data_len).encode("utf-8")) 36 while all_send_datalen<data_len:#当发送的数据小于数据总长就不断的发送 37 #递增式改变截取位置 38 every_send_data=err_info[all_send_datalen:all_send_datalen+1024] 39 conn.send(err_info) 40 all_send_datalen+=len(every_send_data) 41 42 else: 43 #存在输入的命令则发送指令执行的结果,并打印字节数 44 data_len = len(out_info) 45 46 all_send_datalen = 0 47 print(f"{cmd_msg} 命令执行结果长度:", data_len) 48 49 # 把数据长度发送给接收端 50 conn.send(str(data_len).encode("utf-8")) 51 while all_send_datalen < data_len: # 当发送的数据小于数据总长就不断的放送 52 # 递增式改变截取位置 53 #每1024个字节发一次 54 every_send_data = out_info[all_send_datalen:all_send_datalen + 1024] 55 conn.send(every_send_data) 56 57 all_send_datalen += len(every_send_data) 58 59 conn.close() 60 server.close()
1 import socket 2 client=socket.socket()#创建客户端对象 3 client.connect(('127.0.0.1', 8001))#连接服务器的ip_port 4 5 while 1: 6 7 cmd=input("cmd>>>") 8 client.send(cmd.encode("utf-8")) 9 #判断是否为退出指令 10 if cmd=="exit" or cmd=="exit()": 11 break 12 else: 13 # 客客户端接受返回信息 14 data_len=client.recv(1024).decode("utf-8") 15 int_data_len=int(data_len) 16 print(int_data_len) 17 #接受的字节个数,计数比较 18 all_recv_datalen=0 19 # 用于接收到的字节拼接 20 all_data=b'' 21 #循环接受数据 22 while all_recv_datalen<int_data_len: 23 # 每1024个字节接收一次 24 every_recv_data=client.recv(1024) 25 all_recv_datalen+=len(every_recv_data) 26 all_data += every_recv_data 27 # 输出打印 28 print(all_data.decode("gbk")) 29 30 client.close()
方案二struct打包:
struct操作简介
import struct #打包pack #struct.pack(格式,数据) # a=231546789 # b=struct.pack("i",a) # print(b)#b'\xa5\x1f\xcd\r' #解包unpack,结果是元祖 #struct.unpack(格式,数据) c=struct.unpack("i",b'\xa5\x1f\xcd\r') print(c) #(231546789,) a=c[0] print(a)#231546789
源码:
import socket import subprocess import struct server = socket.socket() ip_port = ('127.0.0.1',8001) data_full_len = 0 #统计发送数据的长度 server.bind(ip_port) server.listen() conn,addr = server.accept() while 1: from_client_cmd = conn.recv(1024).decode('utf-8') sub_obj = subprocess.Popen( from_client_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) #subprocess对象.read 得到的结果是bytes类型的 cmd_res = sub_obj.stdout.read() data_len = len(cmd_res) #总数据长度 data_len_str = str(data_len) print('结果长度>>>',data_len) #将真实数据长度打包成4个字节的数据 struct_data_len = struct.pack('i',data_len) conn.send(struct_data_len + cmd_res)
import json import socket import struct client = socket.socket() ip_port = ('127.0.0.1',8001) client.connect(ip_port) all_recv_len = 0 all_data_byte = b'' while 1: client_cmd = input('请输入系统指令>>>') client.send(client_cmd.encode('utf-8')) #先接收4个字节,这4个字节是真实数据长度加工成的 recv_data_len = client.recv(4) #将4个字节长度的数据,解包成后面真实数据的长度 real_data_len = struct.unpack('i',recv_data_len)[0] print(real_data_len) server_result = client.recv(real_data_len) print(server_result.decode('gbk'))
加强版:
1 import struct 2 import socket 3 import subprocess 4 import time 5 6 server=socket.socket() 7 server.bind(("127.0.0.1",8001)) 8 server.listen() 9 conn,addr=server.accept() 10 while 1: 11 time.sleep(0.1) 12 cmd_msg=conn.recv(1024).decode("utf-8") 13 obj_sub=subprocess.Popen( 14 cmd_msg, 15 shell=True, 16 stdout=subprocess.PIPE, 17 stderr=subprocess.PIPE 18 ) 19 cmd_out=obj_sub.stdout.read() 20 cmd_erro=obj_sub.stderr.read() 21 if obj_sub.stdout: 22 out_len=len(cmd_out) 23 print(cmd_msg+"\t"+str(out_len)) 24 b_out_len=struct.pack("i",out_len) 25 conn.send(b_out_len+cmd_out) 26 27 else: 28 err_len = len(cmd_erro) 29 int(cmd_msg + "\t" + str(err_len)) 30 b_err_len = struct.pack("i", err_len) 31 conn.send(b_err_len+cmd_erro) 32 conn.close() 33 server.close()
1 import struct 2 import socket 3 import subprocess 4 import time 5 6 client=socket.socket() 7 client.connect(("127.0.0.1", 8001)) 8 while 1: 9 time.sleep(0.1) 10 cmd=input("cmd>>>").encode("utf-8") 11 client.send(cmd) 12 #接受数据 13 data_len_pack=client.recv(4) 14 data_len=struct.unpack("i",data_len_pack)[0] 15 print(data_len) 16 data=client.recv(data_len).decode("gbk") 17 print(data) 18 client.close()