一、什么是粘包
须知:只有TCP有粘包现象,UDP永远不会粘包
粘包不一定会发生
如果发生了:1.可能是在客户端已经粘了
2.客户端没有粘,可能是在服务端粘了
首先需要掌握一个socket收发消息的原理
应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。(因为TCP是流式协议,不知道啥时候开始,啥时候结束)。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
socket缓冲区的问题:
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
这些I/O缓冲区特性可整理如下:
1.I/O缓冲区在每个TCP套接字中单独存在;
2.I/O缓冲区在创建套接字时自动生成;
3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
4.关闭套接字将丢失输入缓冲区中的数据。
输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:
1.unsigned optVal;
2.int optLen = sizeof(int);
3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal, &optLen);
4.printf("Buffer length: %d ", optVal);
socket缓冲区解释
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 重用ip地址和端口
server.bind(('127.0.0.1',8010))
server.listen(3)
print(server.getsockopt(socket.SOL_SOCKET,socket.SO_SNDBUF)) # 输出缓冲区大小
print(server.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF)) # 输入缓冲区大小
须知:只有TCP有粘包现象,UDP永远不会粘包!具体原因
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
二、发生粘包的两种情况
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会当做一个包发出去,产生粘包)
2 phone = socket(AF_INET,SOCK_STREAM)
3 phone.setsockopt(SOL_SOCKET,SOCK_STREAM,1)
4 phone.bind(('127.0.0.1',8080))
5 phone.listen(5)
6 print('start running...')
7
8 coon,addr = phone.accept() #等待连接
9
10 data1 = coon.recv(10)
11 data2 = coon.recv(10)
12
13 print('------------>',data1.decode('utf-8'))
14 print('------------>',data2.decode('utf-8'))
15 coon.close()
16 phone.close()
2 import time
3 phone = socket(AF_INET,SOCK_STREAM)
4 phone.connect(('127.0.0.1',8080))
5
6 phone.send('hello'.encode('utf-8'))
7 phone.send('helloworld'.encode('utf-8'))
8 phone.close()
接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
2 phone = socket(AF_INET,SOCK_STREAM)
3 phone.setsockopt(SOL_SOCKET,SOCK_STREAM,1)
4 phone.bind(('127.0.0.1',8080))
5 phone.listen(5)
6 print('start running...')
7
8 coon,addr = phone.accept() #等待连接
9
10 data1 = coon.recv(2) #一次没有接收完整
11 data2 = coon.recv(10) #下一次接收的时候会先取旧的数据,然后取新的
12 # data3 = coon.recv(1024) #接收等5秒后的信息
13 print('------------>',data1.decode('utf-8'))
14 print('------------>',data2.decode('utf-8'))
15 # print('------------>',data3.decode('utf-8'))
16 coon.close()
17 phone.close()
2 import time
3 phone = socket(AF_INET,SOCK_STREAM)
4 phone.connect(('127.0.0.1',8080))
5
6 phone.send('hello'.encode('utf-8'))
7 time.sleep(5)
8 phone.send('haiyan'.encode('utf-8'))
9 phone.close()
三、解决粘包的方法
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
2 import subprocess
3 import struct
4 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
5 phone.bind(('127.0.0.1',8080)) #绑定手机卡
6 phone.listen(5) #阻塞的最大数
7 print('start runing.....')
8 while True: #链接循环
9 coon,addr = phone.accept()# 等待接电话
10 print(coon,addr)
11 while True: #通信循环
12 # 收发消息
13 cmd = coon.recv(1024) #接收的最大数
14 print('接收的是:%s'%cmd.decode('utf-8'))
15 #处理过程
16 res = subprocess.Popen(cmd.decode('utf-8'),shell = True,
17 stdout=subprocess.PIPE, #标准输出
18 stderr=subprocess.PIPE #标准错误
19 )
20 stdout = res.stdout.read()
21 stderr = res.stderr.read()
22 #先发报头(转成固定长度的bytes类型,那么怎么转呢?就用到了struct模块)
23 #len(stdout) + len(stderr)#统计数据的长度
24 header = struct.pack('i',len(stdout)+len(stderr))#制作报头
25 coon.send(header)
26 #再发命令的结果
27 coon.send(stdout)
28 coon.send(stderr)
29 coon.close()
30 phone.close()
2 import struct
3 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
4 phone.connect(('127.0.0.1',8080)) #连接服
5 while True:
6 # 发收消息
7 cmd = input('请你输入命令>>:').strip()
8 if not cmd:continue
9 phone.send(cmd.encode('utf-8')) #发送
10 #先收报头
11 header_struct = phone.recv(4) #收四个
12 unpack_res = struct.unpack('i',header_struct)
13 total_size = unpack_res[0] #总长度
14 #后收数据
15 recv_size = 0
16 total_data=b''
17 while recv_size<total_size: #循环的收
18 recv_data = phone.recv(1024) #1024只是一个最大的限制
19 recv_size+=len(recv_data) #
20 total_data+=recv_data #
21 print('返回的消息:%s'%total_data.decode('gbk'))
22 phone.close()
四、解决粘包问题升级版:完整的解决了
2 import subprocess
3 import struct
4 import json
5 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
6 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
7 phone.bind(('127.0.0.1',8080)) #绑定手机卡
8 phone.listen(5) #阻塞的最大数
9 print('start runing.....')
10 while True: #链接循环
11 coon,addr = phone.accept()# 等待接电话
12 print(coon,addr)
13 while True: #通信循环
14 # 收发消息
15 cmd = coon.recv(1024) #接收的最大数
16 print('接收的是:%s'%cmd.decode('utf-8'))
17 #处理过程
18 res = subprocess.Popen(cmd.decode('utf-8'),shell = True,
19 stdout=subprocess.PIPE, #标准输出
20 stderr=subprocess.PIPE #标准错误
21 )
22 stdout = res.stdout.read()
23 stderr = res.stderr.read()
24 # 制作报头
25 header_dic = {
26 'total_size': len(stdout)+len(stderr), # 总共的大小
27 'filename': None,
28 'md5': None
29 }
30 header_json = json.dumps(header_dic) #字符串类型
31 header_bytes = header_json.encode('utf-8') #转成bytes类型(但是长度是可变的)
32 #先发报头的长度
33 coon.send(struct.pack('i',len(header_bytes))) #发送固定长度的报头
34 #再发报头
35 coon.send(header_bytes)
36 #最后发命令的结果
37 coon.send(stdout)
38 coon.send(stderr)
39 coon.close()
40 phone.close()
2 import struct
3 import json
4 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
5 phone.connect(('127.0.0.1',8080)) #连接服务器
6 while True:
7 # 发收消息
8 cmd = input('请你输入命令>>:').strip()
9 if not cmd:continue
10 phone.send(cmd.encode('utf-8')) #发送
11 #先收报头的长度
12 header_len = struct.unpack('i',phone.recv(4))[0] #吧bytes类型的反解
13 #在收报头
14 header_bytes = phone.recv(header_len) #收过来的也是bytes类型
15 header_json = header_bytes.decode('utf-8') #拿到json格式的字典
16 header_dic = json.loads(header_json) #反序列化拿到字典了
17 total_size = header_dic['total_size'] #就拿到数据的总长度了
18 #最后收数据
19 recv_size = 0
20 total_data=b''
21 while recv_size<total_size: #循环的收
22 recv_data = phone.recv(1024) #1024只是一个最大的限制
23 recv_size+=len(recv_data) #有可能接收的不是1024个字节,或许比1024多呢,
24 # 那么接收的时候就接收不全,所以还要加上接收的那个长度
25 total_data+=recv_data #最终的结果
26 print('返回的消息:%s'%total_data.decode('gbk'))
27 phone.close()
五、struct模块
先介绍一下struct模块:
该模块可以把一个类型,如数字,转成固定长度的bytes
import struct # 将一个数字转化成等长度的bytes类型。 ret = struct.pack('i', 183346) print(ret, type(ret), len(ret)) # 通过unpack反解回来 ret1 = struct.unpack('i',ret)[0] print(ret1, type(ret1), len(ret1)) # 但是通过struct 处理不能处理太大 ret = struct.pack('l', 4323241232132324) print(ret, type(ret), len(ret)) # 报错
方案一:low版。
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总数按照固定字节发送给接收端后面跟上总数据,然后接收端先接收固定字节的总字节流,再来一个死循环接收完所有数据。
import subprocess
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1:
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
# 1 制作固定报头
total_size = len(correct_msg) + len(error_msg)
header = struct.pack('i', total_size)
# 2 发送报头
conn.send(header)
# 发送真实数据:
conn.send(correct_msg)
conn.send(error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
# 但是low版本有问题:
# 1,报头不只有总数据大小,而是还应该有MD5数据,文件名等等一些数据。
# 2,通过struct模块直接数据处理,不能处理太大。
import struct
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080))
while 1:
cmd = input('>>>').strip()
if not cmd: continue
phone.send(cmd.encode('utf-8'))
# 1,接收固定报头
header = phone.recv(4)
# 2,解析报头
total_size = struct.unpack('i', header)[0]
# 3,根据报头信息,接收真实数据
recv_size = 0
res = b''
while recv_size < total_size:
recv_data = phone.recv(1024)
res += recv_data
recv_size += len(recv_data)
print(res.decode('gbk'))
phone.close()
方案二:可自定制报头版。
我们可以把报头做成字典,字典里包含将要发送的真实数据的描述信息(大小啊之类的),然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。
我们在网络上传输的所有数据 都叫做数据包,数据包里的所有数据都叫做报文,报文里面不止有你的数据,还有ip地址、mac地址、端口号等等,其实所有的报文都有报头,这个报头是协议规定的,看一下
发送时:
先发报头长度
再编码报头内容然后发送
最后发真实内容
接收时:
先手报头长度,用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的描述信息,然后去取真实的数据内容
import subprocess
import struct
import json
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1:
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
# 1 制作固定报头
total_size = len(correct_msg) + len(error_msg)
header_dict = {
'md5': 'fdsaf2143254f',
'file_name': 'f1.txt',
'total_size':total_size,
}
header_dict_json = json.dumps(header_dict) # str
bytes_headers = header_dict_json.encode('utf-8')
header_size = len(bytes_headers)
header = struct.pack('i', header_size)
# 2 发送报头长度
conn.send(header)
# 3 发送报头
conn.send(bytes_headers)
# 4 发送真实数据:
conn.send(correct_msg)
conn.send(error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
import struct
import json
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080))
while 1:
cmd = input('>>>').strip()
if not cmd: continue
phone.send(cmd.encode('utf-8'))
# 1,接收固定报头
header_size = struct.unpack('i', phone.recv(4))[0]
# 2,解析报头长度
header_bytes = phone.recv(header_size)
header_dict = json.loads(header_bytes.decode('utf-8'))
# 3,收取报头
total_size = header_dict['total_size']
# 3,根据报头信息,接收真实数据
recv_size = 0
res = b''
while recv_size < total_size:
recv_data = phone.recv(1024)
res += recv_data
recv_size += len(recv_data)
print(res.decode('gbk'))
phone.close()
FTP上传下载文件的代码(简单版)
import struct
import json sk = socket.socket()
# buffer = 4096 # 当双方的这个接收发送的大小比较大的时候,就像这个4096,就会丢数据,这个等我查一下再告诉大家,改小了就ok的,在linux上也是ok的。
buffer = 1024 #每次接收数据的大小
sk.bind(('127.0.0.1',8090))
sk.listen()
conn,addr = sk.accept()
#接收
head_len = conn.recv(4)
head_len = struct.unpack('i',head_len)[0] #解包
json_head = conn.recv(head_len).decode('utf-8') #反序列化
head = json.loads(json_head)
filesize = head['filesize']
with open(head['filename'],'wb') as f:
while filesize:
if filesize >= buffer: #>=是因为如果刚好等于的情况出现也是可以的。
content = conn.recv(buffer)
f.write(content)
filesize -= buffer
else:
content = conn.recv(buffer)
f.write(content)
break
conn.close()
sk.close()
tcp_server.py
import json
import socket
import struct
sk = socket.socket()
sk.connect(('127.0.0.1',8090))
buffer = 1024 #读取文件的时候,每次读取的大小
head = {
'filepath':r'D:打包程序', #需要下载的文件路径,也就是文件所在的文件夹
'filename':'xxx.mp4', #改成上面filepath下的一个文件
'filesize':None,
}
file_path = os.path.join(head['filepath'],head['filename'])
filesize = os.path.getsize(file_path)
head['filesize'] = filesize
# json_head = json.dumps(head,ensure_ascii=False) #字典转换成字符串
json_head = json.dumps(head) #字典转换成字符串
bytes_head = json_head.encode('utf-8') #字符串转换成bytes类型
print(json_head)
print(bytes_head)
#计算head的长度,因为接收端先接收我们自己定制的报头,对吧
head_len = len(bytes_head) #报头长度
pack_len = struct.pack('i',head_len)
print(head_len)
print(pack_len)
sk.send(pack_len) #先发送报头长度
sk.send(bytes_head) #再发送bytes类型的报头
#即便是视频文件,也是可以按行来读取的,也可以readline,也可以for循环,但是读取出来的数据大小就不固定了,影响效率,有可能读的比较小,也可能很大,像视频文件一般都是一行的二进制字节流。
#所有我们可以用read,设定一个一次读取内容的大小,一边读一边发,一边收一边写
with open(file_path,'rb') as f:
while filesize:
if filesize >= buffer: #>=是因为如果刚好等于的情况出现也是可以的。
content = f.read(buffer) #每次读取出来的内容
sk.send(content)
filesize -= buffer #每次减去读取的大小
else: #那么说明剩余的不够一次读取的大小了,那么只要把剩下的读取出来发送过去就行了
content = f.read(filesize)
sk.send(content)
break
sk.close()
tcp_client.py
FTP上传下载文件的代码(升级版)(注:咱们学完网络编程就留FTP作业,这个代码可以参考,当你用函数的方式写完之后,再用面向对象进行改版却没有思路的时候再来看,别骗自己昂~~)
import struct
import json
import subprocess
import os
class MYTCPServer:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
allow_reuse_address = False
max_packet_size = 8192
coding='utf-8'
request_queue_size = 5
server_dir='file_upload'
def __init__(self, server_address, bind_and_activate=True):
"""Constructor. May be extended, do not override."""
self.server_address=server_address
self.socket = socket.socket(self.address_family,
self.socket_type)
if bind_and_activate:
try:
self.server_bind()
self.server_activate()
except:
self.server_close()
raise
def server_bind(self):
"""Called by constructor to bind the socket.
"""
if self.allow_reuse_address:
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(self.server_address)
self.server_address = self.socket.getsockname()
def server_activate(self):
"""Called by constructor to activate the server.
"""
self.socket.listen(self.request_queue_size)
def server_close(self):
"""Called to clean-up the server.
"""
self.socket.close()
def get_request(self):
"""Get the request and client address from the socket.
"""
return self.socket.accept()
def close_request(self, request):
"""Called to clean up an individual request."""
request.close()
def run(self):
while True:
self.conn,self.client_addr=self.get_request()
print('from client ',self.client_addr)
while True:
try:
head_struct = self.conn.recv(4)
if not head_struct:break
head_len = struct.unpack('i', head_struct)[0]
head_json = self.conn.recv(head_len).decode(self.coding)
head_dic = json.loads(head_json)
print(head_dic)
#head_dic={'cmd':'put','filename':'a.txt','filesize':123123}
cmd=head_dic['cmd']
if hasattr(self,cmd):
func=getattr(self,cmd)
func(head_dic)
except Exception:
break
def put(self,args):
file_path=os.path.normpath(os.path.join(
self.server_dir,
args['filename']
))
filesize=args['filesize']
recv_size=0
print('----->',file_path)
with open(file_path,'wb') as f:
while recv_size < filesize:
recv_data=self.conn.recv(self.max_packet_size)
f.write(recv_data)
recv_size+=len(recv_data)
print('recvsize:%s filesize:%s' %(recv_size,filesize))
tcpserver1=MYTCPServer(('127.0.0.1',8080))
tcpserver1.run()
server.py
import struct
import json
import os
class MYTCPClient:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
allow_reuse_address = False
max_packet_size = 8192
coding='utf-8'
request_queue_size = 5
def __init__(self, server_address, connect=True):
self.server_address=server_address
self.socket = socket.socket(self.address_family,
self.socket_type)
if connect:
try:
self.client_connect()
except:
self.client_close()
raise
def client_connect(self):
self.socket.connect(self.server_address)
def client_close(self):
self.socket.close()
def run(self):
while True:
inp=input(">>: ").strip()
if not inp:continue
l=inp.split()
cmd=l[0]
if hasattr(self,cmd):
func=getattr(self,cmd)
func(l)
def put(self,args):
cmd=args[0]
filename=args[1]
if not os.path.isfile(filename):
print('file:%s is not exists' %filename)
return
else:
filesize=os.path.getsize(filename)
head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize}
print(head_dic)
head_json=json.dumps(head_dic)
head_json_bytes=bytes(head_json,encoding=self.coding)
head_struct=struct.pack('i',len(head_json_bytes))
self.socket.send(head_struct)
self.socket.send(head_json_bytes)
send_size=0
with open(filename,'rb') as f:
for line in f:
self.socket.send(line)
send_size+=len(line)
print(send_size)
else:
print('upload successful')
client=MYTCPClient(('127.0.0.1',8080))
client.run()
client.py
#进度条的效果
[# ]
[## ]
[### ]
[#### ]
#指定宽度
print('[%-15s]' %'#')
print('[%-15s]' %'##')
print('[%-15s]' %'###')
print('[%-15s]' %'####')
#打印%
print('%s%%' %(100)) #第二个%号代表取消第一个%的特殊意义
#可传参来控制宽度
print('[%%-%ds]' %50) #[%-50s]
print(('[%%-%ds]' %50) %'#')
print(('[%%-%ds]' %50) %'##')
print(('[%%-%ds]' %50) %'###')
#=========实现打印进度条函数==========
import sys
import time
def progress(percent,width=50):
if percent >= 1:
percent=1
show_str = ('%%-%ds' % width) % (int(width*percent)*'|')
print(' %s %d%%' %(show_str, int(100*percent)), end='')
#=========应用==========
data_size=1025
recv_size=0
while recv_size < data_size:
time.sleep(0.1) #模拟数据的传输延迟
recv_size+=1024 #每次收1024
percent=recv_size/data_size #接收的比例
progress(percent,width=70) #进度条的宽度70