话前
一开始在做文件上传的时候,没考虑过文件上传失败的问题,因为上传的多数都是几十兆几百兆的文件。但是也有会上传比较大的文件,此时传统的上传直接没了反应,也不知道上传了多少就很头疼。后来网上搜索的关于大文件上传的思路,网上的思路还是蛮多的,都挺成熟的,然后就想把大文件分片下载这个技术引到项目中。最后也是成功引入分片上传和断点续传,还有大文件分片下载。本篇就来聊一聊大文件分片上传。
一、为什么要使用分片上传
要用分片上传,首先要知道为什么要使用分片上传,或者说那些场景下要使用分片上传。
1.文件过大:一个文件过大,几个G或者几十G的大文件就非常适合分片上传
2.网络不稳定:网络时好时坏或者比较慢的情况
3.断点续传:传了一部分后暂停想继续传的时候,分片上传也是个不错的选择(断点续传需要考虑其他因素,这里不做具体详情的讲说)
二、分片上传原理
了解了为什么要使用分片上传后,就要引入分片上传了。
分片上传的大致流程:前端对文件进行切片,然后分片发送到服务端。服务端接收到分片文件后保存相应的文件,并对每片做好排序(我是采用文件名前序号的方式),最后合并文件,这是总体流程。具体的流程如下:
1.客户端对将要上传的文件定义唯一标识,并对文件进行分片(每10兆为一片)
2.客户端调用服务端接口,进行上传文件片(客户端每次都要携带文件唯一标识,客户端可以每五个一组进行并行上传)
3.服务端用文件唯一标识建临时文件夹(用来存储文件片,最后方便在这个文件下删除文件片)
4.客户端每次上传完后根据服务端返回的结果判断该组的文件片是否都上传从成功,若有没成功的文件片,可以单独对该文件片进行重新上传
5.服务端根据上传文件夹中文件片的总和跟客户端分片的总和是否相等判断文件片是否都上传完成
6.分片文件上传完成后,服务端合并文件片,并删除临时文件夹中分片文件
三、具体的流程图
四、程序
分片上传要是做好,里面细节还是很多的,我是基于c#实现的。
/// <summary>
/// 文件上传返回结果的模型
/// </summary>
public class FileUploadModel
{
/// <summary>
/// 当前片上传的状态,true表示当前片上传成功
/// </summary>
public bool IndexState { get; set; }
/// <summary>
/// 所有片是否上传完,true表示都已上上传完
/// </summary>
public bool TotalState { get; set; }
/// <summary>
/// 最后服务端文件存放的位置
/// </summary>
public string FilePath { get; set; }
/// <summary>
/// 提示信息
/// </summary>
public string Message { get; set; }
}
/// <summary>
/// 上传文件
/// </summary>
/// <param name="folderId">文件唯一ID</param>
/// <param name="fileName">文件名</param>
/// <param name="total">总片数</param>
/// <param name="index">当前片数索引</param>
/// <returns></returns>
[DisableRequestSizeLimit]
[HttpPost]
public ActionResult<string> UploadBigFile([FromForm] string folderId, [FromForm] string fileName, [FromForm] int total, [FromForm] int index)
{
var filePath = Path.Combine(dirPath, folderId);
if (System.IO.Directory.Exists(filePath))
System.IO.Directory.CreateDirectory(filePath);
//filePath=Path.Combine(filePath, fileName);
var resultModel=new FileUploadModel();
filePath = Path.Combine(dirPath, index + "_" + fileName);
//1.判断当前片是否存在,存在的话,可以直接跳过继续上传,有点断点续传那么点意思
if (System.IO.File.Exists(filePath))
{
resultModel = new FileUploadModel { IndexState = true, TotalState = true, Message="当前片已经存在,继续上传" };
return JsonHelper.SerializeObjectToUpper(new ResultModel(true, resultModel));
}
var data = Request.Form.Files;
if (!DataFileBlockExist(filePath))
{
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
foreach (var file in data)
{
file.CopyTo(fileStream); //保存文件
}
}
}
//2.判断是不是已经上传完
if (System.IO.File.Exists(filePath) && !DataTransComplete(dirPath, "_" + fileName, total))
{
resultModel = new FileUploadModel { IndexState = true, TotalState = false };
return JsonHelper.SerializeObjectToUpper(new ResultModel(true, resultModel));
}
//3.合并判断,
//如果已经是最后一个分片,组合
//当然你也可以用其它方法比如接收每个分片时直接写到最终文件的相应位置上,
//但要控制好并发防止文件锁冲突
if (DataTransComplete(dirPath, "_" + fileName, total))
{
var filepath = Path.Combine(dirPath, fileName);
try
{
//4.独占方式进行文件写入!防止在合并文件的过程中又发生文件合并操作
using (FileStream fs = new FileStream(filepath, FileMode.Create, FileAccess.Write, FileShare.None))
{
for (int i = 1; i <= total; ++i)
{
string part = Path.Combine(dirPath, i + "_" + fileName);
var bytes = System.IO.File.ReadAllBytes(part);
fs.Write(bytes, 0, bytes.Length);
bytes = null;
//5.删除对应的分片
System.IO.File.Delete(part);
}
var fileid = Guid.NewGuid();
var extName = fs.Name.GetExtensioName();
//var fileName = Path.GetFileName(filepath);
fs.Close();
//6.上传完成;
var resultPath = Path.Combine(webAttPath, folderId,fileName);
resultModel = new FileUploadModel { IndexState = true, TotalState = false ,FilePath= resultPath };
return JsonHelper.SerializeObjectToUpper(new ResultModel(true, resultModel));
}
}
catch (Exception ex)
{
NLoggerHelper.Logger.Error(ex.Message);
resultModel = new FileUploadModel { IndexState = false, TotalState = false,Message=ex.Message };
return JsonHelper.SerializeObjectToUpper(new ResultModel(false, resultModel));
}
}
resultModel = new FileUploadModel { IndexState = false, TotalState = false };
return JsonHelper.SerializeObjectToUpper(new ResultModel(false, resultModel));
}
/// <summary>
/// 当前索引的文件块 是否存在;
/// </summary>
/// <param name="path">文件存储位置</param>
/// <param name="name">文件名</param>
/// <param name="index">块索引</param>
/// <returns></returns>
private bool DataBlockExist(string path, string name, int index)
{
string file = Path.Combine(path, index + "_" + name);
if (System.IO.File.Exists(file))
return true;
else
return false;
}
/// <summary>
/// 当前索引的文件块 是否存在;
/// </summary>
/// <param name="filePath">文件路径</param>
/// <returns></returns>
private bool DataFileBlockExist(string filePath)
{
if (System.IO.File.Exists(filePath))
return true;
else
return false;
}