大文件分片上传就是一个契合分治算法的场景,现而今,视频文件的体积越来越大,高清视频体积大概2-4g不等,但4K视频的分辨率是标准高清的四倍,需要四倍的存储空间——只需两到三分钟的未压缩4K 电影,或者电影预告片的长度,就可以达到500GB。 8K视频文件更是大得难以想象,而现在12K正在出现,如此巨大的文件,该怎样设计一套合理的数据传输方案?这里我们以前后端分离项目为例,前端使用Vue.js3.0配合ui库Ant-desgin,后端采用并发异步框架Tornado实现大文件的分片无阻塞传输与异步IO写入服务
在前端对视频进行分片
首先要安装Ant-desgin:
随后,参照Ant-desgin官方文档:https://antdv.com/components/overview-cn 构建上传控件:
<a-upload @change="handleChange" :beforeUpload="beforeUpload">
<a-button>
<upload-outlined></upload-outlined>
上传资质文件
</a-button>
</a-upload>
注意这里需要将绑定的before-upload强制返回false,设置为手动上传:
beforeUpload:function() {
return false
},
声明分片方法:
// 分片上传方法
handleChange: function (file) {
console.log(file)
// 获取文件大小 文件最小是1kb 转换成字节码1024
var size = file.file.size
// console.log('size', size)
this.size = size
// 定义分片大小 200kb
var shardSize = 1024 * 200
// 总片数 向上取整
this.shardCount = Math.ceil(size / shardSize)
// console.log(this.shardCount);
// 切片操作
for (var i = 0; i < this.shardCount; i++) {
//开始位置 每一次循环的开始位置
var start = i * shardSize
// 结束位置 取最小值 如 378,0+200取200 378,200+200取378
var end = Math.min(size, start + shardSize)
// console.log(start, end)
//切片
var shardfile = file.file.slice(start, end)
// console.log('shardfile')
const axiosupload = this.axios.create({withCredentials:false});
let data = new FormData();
// console.log('1111111size',this.size)
data.append('file', file);
data.append('count', count);
data.append('filename', filename);
// 发起请求
axiosupload({
method: "POST",
url: this.weburl + '/upload/',
data: data
}).then(resp => {
console.log(resp)
if (resp.data.errcode == 0) {
this.finished += 1
console.log(this.finished, this.shardCount)
if (this.finished == this.shardCount) {
this.myaxios(this.weburl + '/upload/', "put", { "filename": filename, "size": this.size }).then(resp => {
if (resp.errcode == 0) {
this.websrc = this.upload_dir + filename
this.src = filename
this.finished = 0;
this.task(filename);
}
})
}
}
})
}
},
具体分片逻辑是,大文件总体积按照单片体积的大小做除法并向上取整,获取到文件的分片个数,这里为了测试方便,将单片体积设置为200kb,可以随时做修改。
随后,分片过程中使用Math.min方法计算每一片的起始和结束位置,再通过slice方法进行切片操作,最后将分片的下标、文件名、以及分片本体异步发送到后台。
当分片请求成功之后,执行合并分片的操作
//发起定时任务
task: function (filename) {
console.log("执行---------------------")
this.myaxios("http://127.0.0.1:8000/scheduler/", "post", { "filename": filename, "count": this.shardCount, "size": this.size, "job_id": "1" }).then(data => {
console.log(data);
});
},
前端的分片逻辑就执行完成了。
后端异步IO写入
为了避免同步写入引起的阻塞,安装aiofiles库:
pip install aiofiles
aiofiles用于处理asyncio应用程序中的本地磁盘文件,配合Tornado的异步非阻塞机制,可以有效的提升文件写入效率:
import aiofiles
# 支持协程 异步 io 非阻塞
# 超大型文件上传 分治算法 分片上传
class SliceUpload(BaseHandler):
# 上传分片
async def post(self):
# 获取分片实体
file = self.request.files["file"][0]
# 获取下标
count = self.get_argument("count",None)
# 文件名
filename = self.get_argument("filename",None)
# 获取文件内容
content = file["body"]
# 异步写入
async with aiofiles.open("./static/upload/{}_{}".format(filename,count),"wb") as file:
# 异步
await file.write(content)
self.finish({"errcode":0,"msg":"分片上传成功"})
这里后端获取到分片实体、文件名、以及分片标识后,将分片文件以文件名_分片标识的格式异步写入到系统目录中,以一张378kb大小的png图片为例,分片文件应该顺序为200kb和178kb,如图所示:
当分片文件都写入成功后,触发分片合并接口
class SliceUpload(BaseHandler):
# 合并分片逻辑
async def put(self):
filename = self.get_argument("filename",None)
count = 0
# 文件体积
size = self.get_argument("size",None)
try:
filesize = os.path.getsize("./static/upload/{}".format(filename))
except Exception as e:
print(str(e))
filesize = 0
print(filesize,size)
if int(size) != filesize:
# 异步打开文件句柄
async with aiofiles.open("./static/upload/{}".format(filename),"ab") as file:
while True:
try:
# 读取分片
shard_file = open("./static/upload/{}_{}".format(filename,count),'rb')
# 异步写入
await file.write(shard_file.read())
# 手动关闭句柄
shard_file.close()
except Exception as e:
print(str(e))
break
count = count + 1
self.finish({"errcode":0,"msg":"合并完毕"})
这里通过文件名进行寻址,随后遍历合并,注意句柄写入模式为增量字节码写入,否则会逐层将分片文件覆盖,同时也兼具了断点续写的功能。有些逻辑会将分片个数传入后端,让后端判断分片合并个数,其实并不需要,因为如果寻址失败,会自动抛出异常并且跳出循环,从而节约了一个参数的带宽占用。
轮询服务
在真实的超大文件传输场景中,由于网络或者其他因素,很可能导致分片任务中断,此时就需要通过降级快速响应,返回托底数据,避免用户的长时间等待,这里我们使用基于Tornado的Apscheduler库来调度分片任务
pip install apscheduler
随后编写轮询服务文件:
# 导包
import re
from tornado.web import url
from datetime import datetime
from distutils.log import debug
from email.mime import application
from itertools import count
from numpy import size
# 时间循环 回调事件
from tornado.ioloop import IOLoop,PeriodicCallback
from tornado.web import RequestHandler,Application
from apscheduler.schedulers.tornado import TornadoScheduler
import os
# 全局变量
scheduler = None
# 维护一个job列表 存在内存里
job_ids = []
# 初始化
def init_scheduler():
# 调用全局作用域下的变量
global scheduler
# 实例化对象
scheduler = TornadoScheduler()
# 调用内置方法
scheduler.start()
print("定时任务开启")
# 声明任务 数据分片传输是否正常进行 任务id,有几次上传就有几次job
# 文件名 大小 分片总数
def task(job_id,filename,size,count):
size=int(size)
count=int(count)
filelist = os.listdir('./static/upload')
print('文件',filelist)
temp = []
for i in range(count):
print(i)
for x in filelist:
if x == "{}_{}".format(filename,i) :
print('xxxx',x)
temp.append(x)
print('temp',temp)
print('count',count)
# 从目录中读取分片的个数
if count != len(temp):
print('分片未上传成功')
return False
try:
# 得到文件的大小
f_size = os.path.getsize('./static/upload/{}'.format(filename))
except Exception as e:
f_size=0
print(e)
if int(size) != f_size :
print('分片合并未成功')
return False
# 删除分片
for x in range(count):
os.remove("./static/upload/{}_{}".format(filename,x))
# 删除定时任务
scheduler.remove_job(str(job_id))
job_ids.remove(job_id)
print("分片执行完毕")
from base import BaseHandler
# 声明服务控制器
class SchedulerHandler(BaseHandler):
# 操作任务 添加到任务队列中
async def get(self):
job_id = self.get_argument("job_id",None)
filename = self.get_argument("filename",None)
size = self.get_argument("size",None)
count = self.get_argument("count",None)
print('----------',job_id,filename,size,count)
global job_ids
if job_id not in job_ids:
# 将此次任务的id放在任务列表中
job_ids.append(job_id)
print('job_id111',job_ids)
# 任务名称 内置的间息执行 想几秒后执行
scheduler.add_job(task,'interval',seconds=3,id=str(job_id),args=(job_id,filename,size,count))
print('job_id222',id)
print("定时任务入队")
res = {'errcode':0,'msg':'ok'}
else:
print('该任务已经存在')
res = {'errcode':0,'msg':'failed'}
self.finish(res)
if __name__ == '__main__':
# task(1,'2.mp4','1108',6)
# 声明路由
routes = [url(r'/scheduler/',SchedulerHandler)]
init_scheduler()
# 声明tornado对象
application = Application(routes,debug=True)
application.listen(8000)
IOLoop.current().start()
每一次分片接口被调用后,就建立定时任务对分片文件进行监测,如果分片成功就删除分片文件,同时删除任务。