网络编程-粘包问题以及解决方案
粘包问题以及解决方案
# 粘包以及解决方法: # 粘包的概念 # 所谓粘包问题是因为在流传输中,接收方一次接收数据,因为不知道消息之间的界限, # 不知道一次性提取多少字节的数据而造成的不能体现一个完整的消息数据的现象 # 粘包产生的场景: # 双方发送一段数据,有且只有一段数据,就关闭连接,这样就不会出现粘包问题 # 如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包 # 如果双方建立连接,需要在连接后一段时间内发送不同结构数据,就可能粘包 # 粘包产生的原因: # 在tcp流传输中出现,以下从发送和接收两方面来看造成粘包的原因。UDP不会出现粘包,因为它有消息边界 # 1 发送端需要等缓冲区满才发送出去,造成粘包。就是这段数据不够塞满缓存,用下段数据塞满了,丧失了该段数据的完整性 # 2 接收方不及时接收缓冲区的包,造成多个包接收。就是接收了一个包,又接收了几个包,搞到一块了,也丧失了一段数据的完整性 # 粘包的后果: # 丧失了该段数据的完整性,那段数据变多了,或者变少了,变得不完整了 # 如何防止粘包: # 所以我们的任务是保证传输的数据就是那一段==> # 完整性就是数据的开始,结束,数据的长度==> # 所以你想到了统计传输数据字节数,按照字节数传输完整 # 其实,只要传输的个数小于等于接收函数conn.recv(1024)内的参数,都是不会产生粘包的,但是超过了肯定粘包 import struct #i 是4字节 data_size=1008 res=struct.pack("i",data_size) print(res) print(len(res)) # b'\xf0\x03\x00\x00' # 4 res1=struct.unpack("i",res) print(res1) res2=struct.unpack("i",res)[0] print(res2) # (1008,) # 1008 head_dic={"data_size":1688,"filename":"a.txt","hash":None}
struct.pack这个函数的参数是无限的 第一个参数是定义打包的格式 第二个参数开始,所有参数都是要打包的内容~ 而第一个格式参数的具体写法参见下表: Format c Type Python Note x pad byte no value c char string of length 1 b signedchar integer B unsignedchar integer ? _Bool bool (1) h short integer H unsignedshort integer i int integer I unsignedint integer or long l long integer L unsignedlong long q longlong long (2) Q unsignedlonglong long (2) f float float d double float s char[] string p char[] string P void* long 还有相应的大/小端的问题: @ native native = native standard < little-endian standard > big-endian standard ! network (= big-endian) standard 大/小端标记可以省略,貌似默认是小端 你的例子中,L表示无符号的长整形值 所以按你的写法打包出来的就应该是一个小端的无符号长整型数据
粘包客户端和服务端:
1 # 粘包案例client.py 2 import socket,time 3 import subprocess 4 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 5 6 ip_port=('127.0.0.1',8080) 7 phone.connect(ip_port) 8 9 # 10 phone.send('helloworld'.encode('utf-8')) 11 time.sleep(3) 12 phone.send('i am ada'.encode('utf-8'))
1 # 粘包案例server.py 2 import socket,time 3 import subprocess 4 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 5 6 ip_port=('127.0.0.1',8080) 7 phone.bind(ip_port) 8 phone.listen(5) 9 conn,addr=phone.accept() 10 11 #接收参数都大于客户端的字节数,不会粘包 12 # data1=conn.recv(1024) 13 # data2=conn.recv(1024) 14 15 data1=conn.recv(5) #b'h' 16 time.sleep(5) 17 data2=conn.recv(1024) #b'elloworldSB' 18 19 20 print('第一个包',data1) 21 print('第二个包',data2)
实现粘包方案的客户端和服务端
1 #client.py 2 import socket 3 import struct 4 import json 5 phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 6 # 拨通电话 7 # ip_port = ('127.0.0.1', 8080) 8 ip_port = ('192.168.16.114', 8081) 9 phone.connect(ip_port) 10 # 通信循环 11 while True: 12 # 发消息 13 cmd = input('>>: ').strip() 14 if not cmd: continue 15 phone.send(bytes(cmd, encoding='utf-8')) 16 17 # part1:先收报头的长度 18 head_struct=phone.recv(4) 19 head_len=struct.unpack('i',head_struct)[0] 20 21 # part2:再收报头 22 head_bytes=phone.recv(head_len) 23 head_json=head_bytes.decode('utf-8') 24 25 head_dic=json.loads(head_json) 26 print(head_dic) 27 data_size = head_dic['data_size'] 28 29 #part3:收数据 30 recv_size = 0 31 recv_data = b'' 32 while recv_size < data_size: 33 data = phone.recv(1024) 34 recv_size += len(data) 35 recv_data += data 36 37 print(recv_data.decode('utf-8')) 38 phone.close()
1 #coding:utf-8 2 # server.py 3 # 这里的思路是: 4 # 服务端,将长度等未来需要的参数都送进字典结构,并json字符串化,编码为字节传送过去 5 # 客户端,将接收到的先把字节解码,再反序列化,转换成字典结构,取出变量 6 7 #买手机 8 import socket 9 import struct 10 import json 11 import subprocess 12 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 13 #绑定电话卡 14 ip_port=('192.168.16.114',8081) 15 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 16 phone.bind(ip_port) 17 #开机 18 phone.listen(5) 19 #等待电话 20 21 #链接循环 22 while True: 23 conn,addr=phone.accept() 24 print('client addr',addr) 25 #通讯循环 26 while True: 27 try: 28 cmd=conn.recv(1024) 29 res=subprocess.Popen(cmd.decode('utf-8'), 30 shell=True, 31 stdout=subprocess.PIPE, 32 stderr=subprocess.PIPE) 33 out_res=res.stdout.read() 34 err_res=res.stderr.read() 35 data_size=len(out_res)+len(err_res) 36 head_dic={'data_size':data_size} 37 head_json=json.dumps(head_dic) 38 head_bytes=head_json.encode('utf-8') 39 40 #part1:先发报头的长度 41 head_len=len(head_bytes) 42 conn.send(struct.pack('i',head_len)) 43 #part2:再发送报头 44 conn.send(head_bytes) 45 #part3:最后发送数据部分 46 conn.send(out_res) 47 conn.send(err_res) 48 49 except Exception: 50 break 51 52 conn.close() 53 phone.close()
简单的cmd输入客户端和服务端:
1 # 这个案例是简单的命令行输入实现,cmd_client.py 2 import socket 3 import struct 4 phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 5 6 7 ip_port = ('127.0.0.1', 8081) 8 phone.connect(ip_port) 9 # 通信循环 10 while True: 11 # 发消息 12 cmd = input('>>: ').strip() 13 if not cmd: continue 14 phone.send(bytes(cmd, encoding='utf-8')) 15 16 #收报头 17 baotou=phone.recv(4) 18 data_size=struct.unpack('i',baotou)[0] 19 20 # 收数据 21 recv_size=0 22 recv_data=b'' 23 while recv_size < data_size: 24 data=phone.recv(1024) #接收0-1024字节之间任意个数并返回,不是只返回1024个 25 recv_size+=len(data) 26 recv_data+=data 27 28 print(recv_data.decode('utf-8')) 29 phone.close()
1 #coding:utf-8 2 # 这个案例是简单的命令行输入实现,cmd_server.py 3 # 解决思路:利用struct.pack('i',data_size))封装数据长度并字节化作为报头传给客户端,所以先传报头,再传数据 4 # 在客户端通过struct.unpack('i',baotou)[0],解压字节变成正常数字来使用,所以先接报头,再接数据 5 6 7 import socket 8 import struct 9 import subprocess 10 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 11 #绑定电话卡 12 ip_port=('127.0.0.1',8081) 13 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 14 phone.bind(ip_port) 15 #开机 16 phone.listen(5) 17 #等待电话 18 19 #链接循环 20 while True: 21 conn,addr=phone.accept() 22 print('client addr',addr) 23 #通讯循环 24 while True: 25 try: 26 cmd=conn.recv(1024) 27 res=subprocess.Popen(cmd.decode('utf-8'), 28 shell=True, 29 stdout=subprocess.PIPE, 30 stderr=subprocess.PIPE) 31 out_res=res.stdout.read() 32 err_res=res.stderr.read() 33 data_size=len(out_res)+len(err_res) 34 #发送报头 35 conn.send(struct.pack('i',data_size)) 36 #发送数据部分 37 conn.send(out_res) 38 conn.send(err_res) 39 40 except Exception: 41 break 42 43 conn.close() 44 phone.close()
简单上传下载客户端和服务端:
1 # upload and download client.py 2 3 import socket 4 import struct 5 import json 6 import os 7 8 9 10 class MYTCPClient: 11 address_family = socket.AF_INET 12 13 socket_type = socket.SOCK_STREAM 14 15 allow_reuse_address = True 16 17 max_packet_size = 8192 18 19 coding='utf-8' 20 21 request_queue_size = 5 22 23 def __init__(self, server_address, connect=True): 24 self.server_address=server_address 25 self.socket = socket.socket(self.address_family, 26 self.socket_type) 27 if connect: 28 try: 29 # 连接服务器 30 self.client_connect() 31 except: 32 # 关闭服务器 33 self.client_close() 34 raise 35 36 def client_connect(self): 37 self.socket.connect(self.server_address) 38 39 def client_close(self): 40 self.socket.close() 41 42 def run(self): 43 44 while True: 45 inp=input(">>: ").strip() 46 # 如果字符串为空,继续循环 47 if not inp:continue 48 49 print("inp:") 50 # put / users / alex / desktop / file_upload / hello.mp4 51 52 l=inp.split() 53 print("inp.split():",l) 54 # ['put', '/users/alex/desktop/file_upload/hello.mp4'] 55 56 cmd=l[0] 57 print("l=inp.split() de l[0]:",l[0]) 58 # l = inp.split() de l[0]: put 59 # 反射反省 60 61 # 类对象是否有cmd这个东西,cmd拿到的应该是命令关键字put 62 # 这个类有没有put 这个函数属性 63 if hasattr(self,cmd): 64 print("hasattr(self,cmd):",hasattr(self,cmd)) 65 # hasattr(self, cmd): True 66 67 # 获得这个函数属性东西 68 func=getattr(self,cmd) 69 print("getattr(self,cmd):", getattr(self, cmd)) 70 # getattr(self, cmd): < bound method MYTCPClient.put of 71 # < __main__.MYTCPClient object at 0x104d12240 >> 72 73 # 执行这个函数东西 74 func(l) 75 print("func(l):", func(l)) 76 # none 77 78 # 上传 79 def put(self,args): 80 81 # 命令行应该是put + path 82 # 取出命令各元素 83 cmd=args[0] 84 85 filename=args[1] 86 print("run cmd/filename:",cmd,filename) 87 # run cmd/filename :put /users/alex/desktop/file_upload/hello.mp4 88 89 if not os.path.isfile(filename): 90 # 如果文件不存在,返回 91 print('file:%s is not exists' %filename) 92 return 93 else: 94 # 如果文件存在,获取文件大小,此时获取文件大小 95 filesize=os.path.getsize(filename) 96 97 # 此时文件信息包涵:命令+包涵文件名的文件路径+大小 98 head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize} 99 print("head_dic:",head_dic) 100 # head_dic: {'cmd': 'put', 'filesize': 24597595, 'filename': 'hello.mp4'} 101 102 # 字符串序列化 103 head_json=json.dumps(head_dic) 104 # 字节类型 105 head_json_bytes=bytes(head_json,encoding=self.coding) 106 107 # 打包,i代表int 数据 108 head_struct=struct.pack('i',len(head_json_bytes)) 109 110 # 发送长度 111 self.socket.send(head_struct) 112 113 # 发送数据 114 self.socket.send(head_json_bytes) 115 116 # 统计目前发送的数据量 117 send_size=0 118 with open(filename,'rb') as f: 119 for line in f: 120 self.socket.send(line) 121 send_size+=len(line) 122 print(send_size) 123 else: 124 # 发送完毕打印上传成功 125 print('upload successful') 126 127 128 client=MYTCPClient(('127.0.0.1',9003)) 129 130 client.run()
1 # upload and download server.py 2 3 import socket 4 5 # 传递字符串时,不必担心太多的问题,而当传递诸如int、char之类的基本数据的时候, 6 # 就需要有一种机制将某些特定的结构体类型打包成二进制流的字符串然后再网络传输, 7 # 而接收端也应该可以通过某种机制进行解包还原出原始的结构体数据 8 import struct 9 import json 10 import subprocess 11 import os 12 13 14 # ftp服务器类,为什么要用类 15 class MYTCPServer: 16 17 # address即使用IP 18 address_family = socket.AF_INET 19 20 # 基于tcp流套接字 21 socket_type = socket.SOCK_STREAM 22 23 # 允许重用地址 24 allow_reuse_address = True 25 26 # 最大包大小1024*8=8192 27 max_packet_size = 8192 28 29 coding='utf-8' 30 31 # 请求队列量 32 request_queue_size = 5 33 34 #上传文件地址,同时也应该是下载地址,应该可以自定义 35 server_dir='/Users/Alex/desktop/file_upload' 36 37 # 初始化函数 38 def __init__(self, server_address, bind_and_activate=True): 39 """Constructor. May be extended, do not override. 40 :param:绑定地址(ip+port),是否绑定激活(默认绑定),便于自定义地址 41 """ 42 self.server_address=server_address 43 44 # 将全局变量拿过来 45 self.socket = socket.socket(self.address_family, 46 self.socket_type) 47 48 # 是否绑定,默认绑定 49 if bind_and_activate: 50 try: 51 # 如果绑定,就绑定 52 self.server_bind() 53 54 # 就激活监听 55 self.server_activate() 56 except: 57 self.server_close() 58 raise 59 60 def server_bind(self): 61 """Called by constructor to bind the socket. 62 """ 63 # 如果允许重用 64 if self.allow_reuse_address: 65 # 设置重用 66 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 67 68 # 直接执行绑定传过来的地址端口 69 self.socket.bind(self.server_address) 70 71 # gethostname()返回运行程序所在的计算机的主机名: 72 self.server_address = self.socket.getsockname() 73 print("self.socket.getsockname():",self.socket.getsockname()) 74 # self.socket.getsockname(): ('127.0.0.1', 9002) 75 # bogon/localhost 76 77 def server_activate(self): 78 """Called by constructor to activate the server. 79 """ 80 # 监听 81 self.socket.listen(self.request_queue_size) 82 83 def server_close(self): 84 """Called to clean-up the server. 85 """ 86 # 关闭服务器 87 self.socket.close() 88 89 # 封装接收conn,data的请求,获取请求 90 def get_request(self): 91 """Get the request and client address from the socket. 92 """ 93 # 返回accept 94 return self.socket.accept() 95 96 # 关闭请求 97 def close_request(self, request): 98 """Called to clean up an individual request. 99 调去清理关闭个人请求 100 """ 101 request.close() 102 103 # 初始化后进行主函数 104 def run(self): 105 106 # 套接字循环 107 while True: 108 109 self.conn,self.client_addr=self.get_request() 110 print('from client 用户数据: ',self.client_addr) 111 # from client 用户数据: ('127.0.0.1', 59479) 112 113 # 通信循环 114 while True: 115 try: 116 # 参数4的特殊意义: 117 head_struct = self.conn.recv(4) 118 if not head_struct:break 119 120 # 解包 121 print("self.conn.recv(4):",head_struct) 122 # self.conn.recv(4): b'=\x00\x00\x00' 123 124 # 获取文件大小 125 head_len = struct.unpack('i', head_struct)[0] 126 print("head_len:",head_len) 127 # head_len: 61 128 129 # 根据文件大小接收数据 130 head_json = self.conn.recv(head_len).decode(self.coding) 131 132 # 反序列化取出字典 133 head_dic = json.loads(head_json) 134 135 print(head_dic) 136 # {'cmd': 'put', 'filesize': 24597595, 'filename': 'hello.mp4'} 137 #head_dic={'cmd':'put','filename':'a.txt','filesize':123123} 138 cmd=head_dic['cmd'] 139 140 # 执行put函数 141 if hasattr(self,cmd): 142 func=getattr(self,cmd) 143 func(head_dic) 144 except Exception: 145 break 146 147 def put(self,args): 148 # 将服务器文件上传路径与上传文件名拼接并正常化路径 149 file_path=os.path.normpath(os.path.join( 150 self.server_dir, 151 args['filename'] 152 )) 153 154 # 文件大小 155 filesize=args['filesize'] 156 157 158 print('----->', file_path) 159 # / Users /Alex/desktop/file_upload/hello.mp4 160 161 # 接收初始化量,判断接收的多少 162 recv_size=0 163 164 # 根据路径打开服务器文件区域,根据要求新建文件,写入 165 with open(file_path,'wb') as f: 166 while recv_size < filesize: 167 168 recv_data=self.conn.recv(self.max_packet_size) 169 f.write(recv_data) 170 recv_size+=len(recv_data) 171 # 查看接收的多少,打印进度条 172 print('recvsize:%s filesize:%s' %(recv_size,filesize)) 173 174 175 176 177 178 179 tcpserver1=MYTCPServer(('127.0.0.1',9003)) 180 181 tcpserver1.run()