网络编程-粘包问题以及解决方案

粘包问题以及解决方案

# 粘包以及解决方法:

# 粘包的概念
# 所谓粘包问题是因为在流传输中,接收方一次接收数据,因为不知道消息之间的界限,
# 不知道一次性提取多少字节的数据而造成的不能体现一个完整的消息数据的现象

# 粘包产生的场景:
# 双方发送一段数据,有且只有一段数据,就关闭连接,这样就不会出现粘包问题
# 如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就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()
服务端

 

  

 

posted @ 2017-09-20 16:23  Adamanter  阅读(301)  评论(0编辑  收藏  举报