036_python的大文件下载以及进度条展示

时间:2021-06-17 16:27:31

复习

1.黏包现象

粘包现象的成因:  

  • tcp协议的特点,面向流的,为了保证可靠传输,所以有很多优化的机制。 
  • 无边界 所有在连接建立的基础上传递的数据之间没有界限。
  • 收发消息很有可能不完全相等。
  • 缓存机制,导致没发过去的消息会在发送端缓存,没接收完的消息会在接收端缓存。

解决

  • 给应用层定制协议

    解决方案一:只发送一条信息
  • 先发送一个定长表示待发送数据长度的bytes,先接收一个固定长度  
  • 再发送要发送的数据,再按照长度接收数据。

    解决方案二 :发送的多条信息
  • 先发送一个定长表示待发送字典长度的bytes,先接收一个固定长度。
  • 再发送要发送字典(字典中存储的是文件信息),再按照长度接收字典。  
  • 再发送文件,再根据字典中的信息接收相应的内容。

那么这2种方案,如何选择呢?

如果发送数据之前,发送的定制协议超过1个变量时,就应该使用字典

否则,就需要发送多次,比如:文件名的长度,文件名,文件大小的长度,文件的内容...

发送的次数越多,在网络上,就比较浪费时间。

如果将上面的5个变量,放到一个字典里面,只需要发送一次,就可以了。

如果只有1个变量,就使用第一种方案。

大文件下载以及进度条展示

大文件传输:

server.py


import os
import json
import socket
import struct
filepath = r'E:\BaiduYunDownload\[电影天堂www.dy2018.com]移动迷宫3:死亡解药BD国英双语中英双字.mp4'
 
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()
 
conn,addr = sk.accept()
filename = os.path.basename(filepath)
filesize = os.path.getsize(filepath)
dic = {'filename':filename,'filesize':filesize}
str_dic = json.dumps(dic).encode('utf-8')
len_dic = len(str_dic)
length = struct.pack('i',len_dic)
conn.send(length)   # dic的长度
conn.send(str_dic)  # dic
with open(filepath,'rb') as f:  # 文件
    while filesize:
        content = f.read(4096)
        conn.send(content)
        filesize -= len(content)
        '''
        这里不能减等4096,因为文件,最后可能只有3字节。
        要根据读取的长度len(content),来计算才是合理的。
        '''
conn.close()
sk.close()

client.py


import json
import struct
import socket
 
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
 
dic_len = sk.recv(4)
dic_len = struct.unpack('i',dic_len)[0]
dic = sk.recv(dic_len)
str_dic = dic.decode('utf-8')
dic = json.loads(str_dic)
with open(dic['filename'],'wb') as f:  # 使用wb更严谨一些,虽然可以使用ab
    while dic['filesize']:
        content = sk.recv(4096)  #这边的4096,可以不用和server对应,改成1024,也可以
        dic['filesize'] -= len(content)
        f.write(content)
sk.close()

先执行server.py,再执行client.py等待30多秒,当前目录就会出现一个视频文件,打开确认,是否可以播放。

客户体验太差了,用户不知道啥时候能接收完,程序到底有没有卡住?下载花了多长时间?都不知道下面来一个进阶版的,增加进度条和下载时间

主要是修改client.py,代码如下:


import json
import struct
import socket
import sys
import time
 
def processBar(num, total):  # 进度条
    rate = num / total
    rate_num = int(rate * 100)
    if rate_num == 100:
        r = '\r%s>%d%%\n' % ('=' * rate_num, rate_num,)
    else:
        r = '\r%s>%d%%' % ('=' * rate_num, rate_num,)
    sys.stdout.write(r)
    sys.stdout.flush
 
start_time = time.time()  # 开始时间
 
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
 
dic_len = sk.recv(4)
dic_len = struct.unpack('i',dic_len)[0]
dic = sk.recv(dic_len)
str_dic = dic.decode('utf-8')
dic = json.loads(str_dic)
with open(dic['filename'],'wb') as f:  # 使用wb更严谨一些,虽然可以使用ab
    content_size = 0
    while True:
        content = sk.recv(4096)<br>        f.write(content) # 写入文件
        content_size += len(content)  # 接收大小
        processBar(content_size,dic['filesize'])  # 执行进度条函数
        if content_size == dic['filesize']:break  # 当接收的总大小等于文件大小时,终止循环
             
sk.close()  # 关闭连接
 
end_time = time.time()  # 结束时间
print('本次下载花费了{}秒'.format(end_time - start_time))

执行效果如下:

036_python的大文件下载以及进度条展示

上面效果展示了100个等号,太长了,那么要缩减到1/3呢?

修改进度条函数


def processBar(num, total):  # 进度条
    rate = num / total
    rate_num = int(rate * 100)
    if rate_num == 100:
        r = '\r%s>%d%%\n' % ('=' * int(rate_num / 3), rate_num,) # 控制等号输出数量,除以3,表示显示1/3
    else:
        r = '\r%s>%d%%' % ('=' * int(rate_num / 3), rate_num,)
    sys.stdout.write(r)
    sys.stdout.flush

再次执行:

036_python的大文件下载以及进度条展示

再来一个高级版,显示绿色的飞机代码如下

代码如下:


def processBar(num, total):  # 进度条
    rate = num / total
    rate_num = int(rate * 100)
    pretty = '✈'
    if rate_num == 100:
        r = '\r\033[32m{}\033[0m{}%\n'.format(pretty * int(rate_num / 5), rate_num,)
    else:
        r = '\r\033[32m{}\033[0m{}%'.format(pretty * int(rate_num / 5), rate_num,)
    sys.stdout.write(r)
    sys.stdout.flush

效果如下:

036_python的大文件下载以及进度条展示

再来一个每秒换色,导入一个随机换色类。


import random
 
 
class Prompt(object):  # 提示信息显示
    colour_dic = {
        'red': 31,
        'green': 32,
        'yellow': 33,
        'blue': 34,
        'purple_red': 35,
        'bluish_blue': 36,
        'white': 37,
    }
 
    def __init__(self):
        pass
 
    @staticmethod
    def display(msg, colour='white'):
        choice = Prompt.colour_dic.get(colour)
        # print(choice)
        if choice:
            info = "\033[1;{};1m{}\033[1;0m".format(choice, msg)
            return info
        else:
            return False
 
    def random_color(msg):  # 随机换色
        colour_list = []
        for i in Prompt.colour_dic:
            colour_list.append(i)
 
        length = len(colour_list) - 1  # 最大索引值
        index = random.randint(0, length)  # 随机数
 
        ret = Prompt.display(msg, colour_list[index])  # 随机颜色
        return ret

修改client.py


from Prompt import Prompt
 
def processBar(num, total):  # 进度条
    rate = num / total
    rate_num = int(rate * 100)
    pretty = Prompt.random_color('✈')  # 随机换色
    if rate_num == 100:
        r = '\r{}{}%\n'.format(pretty * int(rate_num / 5), rate_num,)
    else:
        r = '\r{}{}%'.format(pretty * int(rate_num / 5), rate_num,)
    sys.stdout.write(r)
    sys.stdout.flush

执行效果如下:

036_python的大文件下载以及进度条展示

1.增加MD5校验

server.py


import os
import json
import socket
import struct
import hashlib
 
sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()
 
conn, addr = sk.accept()
filename =r '[电影天堂www.dy2018.com]移动迷宫3:死亡解药BD国英双语中英双字.mp4'  # 文件名
absolute_path = os.path.join(r'E:\BaiduYunDownload',filename)  # 文件绝对路径
buffer_size = 1024*1024  # 缓冲大小,这里表示1MB
 
md5obj = hashlib.md5()
with open(absolute_path, 'rb') as f:
    while True:
        content = f.read(buffer_size)  # 每次读取指定字节
        if content:
            md5obj.update(content)
        else:
            break  # 当内容为空时,终止循环
 
md5 = md5obj.hexdigest()
print(md5)  # 打印md5值
 
dic = {'filename':filename,
       'filename_md5':str(md5),'buffer_size':buffer_size,
       'filesize':os.path.getsize(absolute_path)}
str_dic = json.dumps(dic).encode('utf-8')
len_dic = len(str_dic)
length = struct.pack('i', len_dic)
conn.send(length)  # dic的长度
conn.send(str_dic)  # dic
with open(absolute_path, 'rb') as f:  # 文件
    while dic['filesize']:
        content = f.read(dic['buffer_size'])
        conn.send(content)
        dic['filesize'] -= len(content)
        '''
        这里不能减等4096,因为文件,最后可能只有3字节。
        要根据读取的长度len(content),来计算才是合理的。
        '''
conn.close()

client.py


import json
import struct
import socket
import sys
import time
import hashlib
import os
from Prompt import Prompt
 
def processBar(num, total):  # 进度条
    rate = num / total
    rate_num = int(rate * 100)
    pretty = Prompt.random_color('✈')
    if rate_num == 100:
        r = '\r{}{}%\n'.format(pretty * int(rate_num / 5), rate_num,)
    else:
        r = '\r{}{}%'.format(pretty * int(rate_num / 5), rate_num,)
    sys.stdout.write(r)
    sys.stdout.flush
 
start_time = time.time()  # 开始时间
 
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
 
dic_len = sk.recv(4)
dic_len = struct.unpack('i',dic_len)[0]
dic = sk.recv(dic_len)
str_dic = dic.decode('utf-8')
dic = json.loads(str_dic)
 
md5 = hashlib.md5()
with open(dic['filename'],'wb') as f:  # 使用wb更严谨一些,虽然可以使用ab
    content_size = 0
    while True:
        content = sk.recv(dic['buffer_size'])  # 接收指定大小
        f.write(content)  # 写入文件
        content_size += len(content)  # 接收大小
        md5.update(content)  # 摘要
 
        processBar(content_size,dic['filesize'])  # 执行进度条函数
        if content_size == dic['filesize']:break  # 当接收的总大小等于文件大小时,终止循环
 
    md5 = md5.hexdigest()
    print(md5)  # 打印md5值
    if dic['filename_md5'] == str(md5):
        print(Prompt.display('md5校验正确--下载成功','green'))
    else:
        print(Prompt.display('文件验证失败', 'red'))
        os.remove(dic['filename'])  # 删除文件
 
sk.close()  # 关闭连接
 
end_time = time.time()  # 结束时间
print('本次下载花费了{}秒'.format(end_time - start_time))

执行输出:

036_python的大文件下载以及进度条展示

socket的更多方法介绍

1.更多方法

服务端套接字函数

s.bind() 绑定(主机,端口号)到套接字

s.listen() 开始TCP监听

s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数

s.connect() 主动初始化TCP服务器连接

s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数

s.recv() 接收TCP数据

s.send() 发送TCP数据

s.sendall() 发送TCP数据

s.recvfrom() 接收UDP数据

s.sendto() 发送UDP数据

s.getpeername() 连接到当前套接字的远端的地址

s.getsockname() 当前套接字的地址

s.getsockopt() 返回指定套接字的参数

s.setsockopt() 设置指定套接字的参数

s.close() 关闭套接字

面向锁的套接字方法

s.setblocking() 设置套接字的阻塞与非阻塞模式

s.settimeout() 设置阻塞套接字操作的超时时间

s.gettimeout() 得到阻塞套接字操作的超时时间

面向文件的套接字的函数

s.fileno() 套接字的文件描述符

s.makefile() 创建一个与该套接字相关的文件

2.send和sendall方法

官方文档对socket模块下的socket.send()和socket.sendall()解释如下:

socket.send(string[, flags])

Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Returns the number of bytes sent. Applications are responsible for checking that all data has been sent; if only some of the data was transmitted, the application needs to attempt delivery of the remaining data.

send()的返回值是发送的字节数量,这个数量值可能小于要发送的string的字节数,也就是说可能无法发送string中所有的数据。如果有错误则会抛出异常。

socket.sendall(string[, flags])

Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Unlike send(), this method continues to send data from string until either all data has been sent or an error occurs. None is returned on success. On error, an exception is raised, and there is no way to determine how much data, if any, was successfully sent.

尝试发送string的所有数据,成功则返回None,失败则抛出异常。

故,下面两段代码是等价的:

#sock.sendall('Hello world\n')

#buffer = 'Hello world\n'
#while buffer:
#    bytes = sock.send(buffer)
#    buffer = buffer[bytes:]

验证客户端链接的合法性

使用hashlib.md5 加密

036_python的大文件下载以及进度条展示

为什么要随机字符串,是为了防止客户端的数据被窃取。生成随机的bytes类型数据,它是解不出来的。


import os
print(os.urandom(32))

执行输出:

b'PO\xca8\xc8\xf3\xa0\xb5,\xdd\xb8K \xa8D\x9cN"\x82\x03\x86g\x18e\xa7\x97\xa77\xb9\xa5VA'

server.py


import os
import socket
import hashlib
 
secret_key = '老衲洗头用飘柔'  # 加密key
 
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()
while True:
    try:
        conn,addr = sk.accept()
        random_bytes = os.urandom(32)  # 随即产生32个字节的字符串,返回bytes
        conn.send(random_bytes)  # 发送随机加密key
        md5 = hashlib.md5(secret_key.encode('utf-8'))  # 使用secret_key作为加密盐
        md5.update(random_bytes)  #得到MD5消息摘要
        ret = md5.hexdigest()  #以16进制返回消息摘要,它是一个32位长度的字符串
        msg = conn.recv(1024).decode('utf-8')  # 接收的信息解码
        if msg == ret:print('是合法的客户端')  # 如果接收的摘要和本机计算的摘要一致,就说明是合法的
        else:conn.close()  # 关闭连接
    finally:  # 无论如何,都执行下面的代码
        sk.close()  # 关闭连接
        break

client.py


import socket
import hashlib
secret_key = '老衲洗头用飘柔'  # 加密key
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
 
urandom = sk.recv(32)  # 接收32字节,也就是os.urandom的返回值
md5_obj = hashlib.md5(secret_key.encode('utf-8'))  # 使用加密盐加密
md5_obj.update(urandom)
sk.send(md5_obj.hexdigest().encode('utf-8'))  # 发送md5摘要
print('-----')
sk.close()  # 关闭连接

先执行server.py,再执行client.py

client输出:-----

server输出:是合法的客户端

如果100客户端,来连接呢?秘钥都是通用的。

一般情况下,用在哪些场景呢?

比如公司级别,比如1台机器,向100台服务器获取数据

假如黑客渗透到内网,得知到服务器IP地址。就可以做端口扫描,一台计算机的端口范围是0~65535扫描6万多次,就能知道了。

1.使用hmac加密

hmac是专门来做客户端合法性的

import hmac
obj = hmac.new(key=b'secret_key',msg=b'100212002155')
print(obj.hexdigest())

执行输出:

27111d37764a2fe5bc79d297e7b54c35

客户端也使用hmac,验证一下,就可以了。

改造server和client

server.py


import os
import socket
import hmac
 
secret_key = '老衲洗头用飘柔'.encode('utf-8')
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()
while True:
    try:
        conn,addr = sk.accept()
        random_bytes = os.urandom(32)
        conn.send(random_bytes)
        obj = hmac.new(key=secret_key,msg=random_bytes)
        ret = obj.hexdigest()
        msg = conn.recv(1024).decode('utf-8')
        if msg == ret:print('是合法的客户端')
        else:conn.close()
    finally:
        sk.close()
        break

client.py


import socket
import hmac
 
secret_key = '老衲洗头用飘柔'.encode('utf-8')
sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
 
urandom = sk.recv(32)
hmac_obj = hmac.new(key=secret_key, msg=urandom)
sk.send(hmac_obj.hexdigest().encode('utf-8'))
print('-----')
sk.close()

socketserver

SocketServer内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进 程” 专门负责处理当前客户端的所有请求。

036_python的大文件下载以及进度条展示

它能实现多个客户端,同时连接,它继承了socket

ThreadingTCPServer实现的Soket服务器内部会为每个client创建一个 “线程”,该线程用来和客户端进行交互。

使用ThreadingTCPServer:

创建一个继承自 SocketServer.BaseRequestHandler 的类,必须继承

类中必须定义一个名称为 handle 的方法,必须重写

看BaseRequestHandler 的源码,它的hendle方法,是空的


def handle(self):
    pass

需要自己去实现

server.py


import socketserver
class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        print(self.request)
 
server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),MyServer)
server.serve_forever()

client.py


import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
sk.close()

先执行server.py,再执行client.py

server输出

<socket.socket fd=540, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 62243)>

每连接一个client.就会触发handle,输出request监听,等待连接,接收,全由hendle完成了。

server.py


import socketserver
class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        print(self.request)
        self.request.send(b'hello')  # 跟所有的client打招呼
        print(self.request.recv(1024))  # 接收客户端的信息
 
server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),MyServer)
server.serve_forever()

client.py


import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
print(sk.recv(1024))
inp = input('>>>').encode('utf-8')
sk.send(inp)
sk.close()

先执行server.py,再执行client.py

client输出:b'hello'

>>>hiserver输出:<socket.socket fd=316, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 49176)> b'hi'

开多个客户端,也可以执行

036_python的大文件下载以及进度条展示

server能够和多个client通信

036_python的大文件下载以及进度条展示

1.连续发送

client连续发送:


import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
while True:
    print(sk.recv(1024))
    #inp = input('>>>').encode('utf-8')
    sk.send(b'hahaha')
sk.close()

server连续接收:


import socketserver
class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            print(self.request)
            self.request.send(b'hello')  # 跟所有的client打招呼
            print(self.request.recv(1024))  # 接收客户端的信息
 
server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),MyServer)
server.serve_forever()

执行效果如下:

036_python的大文件下载以及进度条展示

如果server端口重复,使用以下代码:


# 设置allow_reuse_address允许服务器重用地址
    socketserver.TCPServer.allow_reuse_address = True

完整代码如下:


import socketserver
class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            print(self.request)  # 这里不能使用input,否则卡住了
            self.request.send(b'hello')  # 跟所有的client打招呼
            print(self.request.recv(1024))  # 接收客户端的信息
if __name__ == '__main__':
    socketserver.TCPServer.allow_reuse_address = True
    server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),MyServer)
    server.serve_forever()

明日默写

import socketserver
class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        self.request.send(b'hello')
        msg = self.request.recv(1024)
        print(msg)

if __name__ == '__main__':
    socketserver.TCPServer.allow_reuse_address = True
    server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),MyServer)
    server.serve_forever()