作为一名深耕.NET领域多年的开发者,我最近接手了一个极具挑战性的外包项目:客户需要一套能够处理20GB以上大文件上传的系统,同时必须兼容IE8浏览器,保留完整的文件夹层级结构,并且要求全程加密传输。更棘手的是,预算非常有限,客户希望用最低的成本实现最稳定的方案。
面对这样的需求,市面上现成的解决方案要么无法处理超大文件,要么对IE8的兼容性极差,更别提保留文件夹结构这种"高级"功能了。经过一个月的潜心研究和反复测试,我终于开发出了一套完整的原生JS+ASP.NET WebForm全栈解决方案,不仅完美满足了客户的所有需求,还实现了断点续传、加密存储等增值功能。
这套系统的核心设计理念是"分而治之"——将大文件分割成小块进行传输,同时在客户端和服务端分别记录上传进度,实现断点续传功能。整个架构分为三个主要部分:
选择原生JS而非现成的上传库主要基于以下考虑:
ASP.NET WebForm的选择则是考虑到:
前端处理大文件上传的核心是将文件分割为多个小块。经过反复测试,5MB的分片大小在IE8上表现最为稳定:
javascript复制data() {
return {
chunkSize: 5 * 1024 * 1024, // 5MB分片
// 其他数据...
};
}
分片过程使用File API的slice方法,对于IE8则通过Blob.js polyfill提供兼容支持:
javascript复制const chunk = task.file.slice(start, end); // IE8支持File.slice
为确保数据传输安全,前端使用AES-256算法对每个分片进行加密:
javascript复制const encryptedChunk = CryptoJS.AES.encrypt(
CryptoJS.lib.WordArray.create(chunkContent),
this.aesKey,
{ mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }
).toString();
加密密钥由前端动态生成,并通过安全通道传输给后端。实际项目中,建议使用更安全的密钥交换机制。
前端通过localStorage记录上传进度,即使关闭浏览器也能恢复上传:
javascript复制// 保存进度到localStorage
localStorage.setItem(`upload_${task.taskId}`, JSON.stringify({
chunkIndex: task.chunkIndex,
uploadedSize: task.uploadedSize
}));
// 恢复上传时读取进度
const savedProgress = JSON.parse(localStorage.getItem(`upload_${task.taskId}`));
if (savedProgress) {
task.chunkIndex = savedProgress.chunkIndex;
task.uploadedSize = savedProgress.uploadedSize;
}
考虑到IE8的localStorage容量限制(仅5MB),实现中对大文件的上传进度进行了分key存储。
后端接收分片的ASP.NET WebForm处理程序主要完成以下工作:
csharp复制protected void Page_Load(object sender, EventArgs e)
{
if (Request.HttpMethod == "POST")
{
// 获取上传的分片数据
HttpPostedFile chunkFile = Request.Files["chunk"];
// 解密分片
byte[] encryptedData = File.ReadAllBytes(chunkFile.TempFileName);
byte[] decryptedData = AesDecrypt(encryptedData, aesKey);
// 存储分片
string chunkPath = Path.Combine(uploadPath, $"{taskId}_{chunkIndex}");
File.WriteAllBytes(chunkPath, decryptedData);
// 记录进度到数据库
SaveProgressToDB(taskId, filePath, chunkIndex, totalChunks);
Response.Write("{\"code\":200,\"msg\":\"分片上传成功\"}");
}
}
为保留上传的文件夹结构,前端会传递完整的相对路径,后端据此创建对应的目录结构:
csharp复制string uploadPath = HttpContext.Current.Server.MapPath("~/uploader/files/");
string fileDir = Path.Combine(uploadPath, Path.GetDirectoryName(filePath));
Directory.CreateDirectory(fileDir); // 创建所需目录
对于IE8等不支持直接获取文件夹结构的浏览器,采用元数据记录的方式实现类似功能。
当所有分片上传完成后,前端会触发合并请求。后端会:
csharp复制// 检查分片完整性
bool isComplete = true;
for (int i = 0; i < totalChunks; i++)
{
if (!File.Exists(Path.Combine(uploadPath, $"{taskId}_{i}")))
{
isComplete = false;
break;
}
}
if (isComplete)
{
// 合并分片
using (var fs = new FileStream(finalPath, FileMode.Create))
{
for (int i = 0; i < totalChunks; i++)
{
byte[] chunkData = File.ReadAllBytes(Path.Combine(uploadPath, $"{taskId}_{i}"));
fs.Write(chunkData, 0, chunkData.Length);
File.Delete(Path.Combine(uploadPath, $"{taskId}_{i}")); // 删除临时分片
}
}
}
为支持断点续传功能,设计了专门的进度记录表:
sql复制CREATE TABLE upload_progress (
id INT IDENTITY(1,1) PRIMARY KEY,
task_id NVARCHAR(255) NOT NULL, -- 任务ID
file_path NVARCHAR(1000) NOT NULL, -- 文件存储路径
chunk_index INT NOT NULL, -- 当前分片索引
total_chunks INT NOT NULL, -- 总分片数
uploaded_size BIGINT NOT NULL, -- 已上传大小
status NVARCHAR(50) NOT NULL DEFAULT 'pending', -- 上传状态
create_time DATETIME DEFAULT GETDATE(), -- 创建时间
update_time DATETIME DEFAULT GETDATE() -- 更新时间
);
上传过程中,后端会频繁查询和更新进度记录:
csharp复制// 查询进度
string sql = "SELECT chunk_index FROM upload_progress WHERE task_id = @taskId AND file_path = @filePath";
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@taskId", taskId);
cmd.Parameters.AddWithValue("@filePath", filePath);
// 执行查询...
}
// 更新进度
sql = @"IF EXISTS (SELECT 1 FROM upload_progress WHERE task_id = @taskId AND file_path = @filePath AND chunk_index = @chunkIndex)
UPDATE upload_progress SET update_time = GETDATE() WHERE task_id = @taskId AND file_path = @filePath AND chunk_index = @chunkIndex
ELSE
INSERT INTO upload_progress (task_id, file_path, chunk_index, total_chunks, uploaded_size) VALUES (@taskId, @filePath, @chunkIndex, @totalChunks, @uploadedSize)";
针对IE8的特殊需求,实现了几项关键兼容措施:
javascript复制// IE8兼容代码示例
if (typeof FormData === 'undefined') {
// 使用iframe模拟表单提交
var iframe = document.createElement('iframe');
iframe.name = 'upload-iframe';
iframe.style.display = 'none';
document.body.appendChild(iframe);
// 构建传统表单
var form = document.createElement('form');
form.action = '/api/upload/chunk.aspx';
form.method = 'POST';
form.target = 'upload-iframe';
form.enctype = 'multipart/form-data';
// 添加表单字段...
document.body.appendChild(form);
form.submit();
}
针对20GB以上的大文件,实现了以下优化:
javascript复制// 上传进度处理
onUploadProgress: (e) => {
if (e.lengthComputable) {
// 计算实时上传速度
const timeDiff = e.timeStamp - (task.lastTime || Date.now());
const speed = (e.loaded - task.uploadedSize) / (timeDiff || 1) / 1024;
task.speed = speed.toFixed(2);
task.lastTime = e.timeStamp;
// 更新进度显示
task.uploadedSize = e.loaded;
task.progress = Math.round((task.uploadedSize / task.totalSize) * 100);
}
}
前端使用AES-256加密每个分片,确保传输过程中数据安全:
javascript复制// 前端加密
const encryptedChunk = CryptoJS.AES.encrypt(
CryptoJS.lib.WordArray.create(chunkContent),
this.aesKey,
{ mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }
).toString();
// 后端解密
public static byte[] AesDecrypt(byte[] encryptedData, string key)
{
using (Aes aes = Aes.Create())
{
aes.Key = Encoding.UTF8.GetBytes(key);
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
using (ICryptoTransform decryptor = aes.CreateDecryptor())
{
return decryptor.TransformFinalBlock(encryptedData, 0, encryptedData.Length);
}
}
}
根据客户要求,存储时使用国密SM4算法进行二次加密:
csharp复制// SM4加密存储
public static byte[] Sm4Encrypt(byte[] data, string key)
{
// 实现SM4加密逻辑
// ...
return encryptedData;
}
为确保系统正常运行,需要进行以下IIS配置:
xml复制<system.web>
<httpRuntime maxRequestLength="2147483647" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="2147483647" />
</requestFiltering>
</security>
</system.webServer>
修改Web.config中的数据库连接字符串:
xml复制<connectionStrings>
<add name="FileUploader" connectionString="Server=localhost;Database=file_uploader;User Id=sa;Password=your_password;" />
</connectionStrings>
在实际部署和使用过程中,总结了以下性能优化经验:
javascript复制// 并行上传实现示例
const parallelCount = 3; // 并行上传数
let uploadingChunks = 0;
function uploadNextChunk() {
if (uploadingChunks >= parallelCount) return;
// 查找下一个待上传分片
const nextChunk = findNextChunk();
if (!nextChunk) return;
uploadingChunks++;
doUpload(nextChunk).finally(() => {
uploadingChunks--;
uploadNextChunk();
});
// 继续检查是否有更多分片需要上传
if (uploadingChunks < parallelCount) {
uploadNextChunk();
}
}
在实际应用中,可能会遇到以下典型问题:
分片顺序错乱:
IE8内存不足:
上传进度丢失:
合并失败:
csharp复制// 分片校验示例
public bool VerifyChunk(string chunkPath, string expectedMd5)
{
using (var md5 = MD5.Create())
{
using (var stream = File.OpenRead(chunkPath))
{
byte[] hash = md5.ComputeHash(stream);
string actualMd5 = BitConverter.ToString(hash).Replace("-", "").ToLower();
return actualMd5 == expectedMd5.ToLower();
}
}
}
虽然当前方案已经满足基本需求,但还可以进一步扩展:
javascript复制// 动态分片大小调整示例
function adjustChunkSize(networkSpeed) {
if (networkSpeed > 1024 * 1024) { // 1MB/s以上
return 10 * 1024 * 1024; // 10MB
} else if (networkSpeed > 512 * 1024) { // 512KB/s以上
return 5 * 1024 * 1024; // 5MB
} else {
return 2 * 1024 * 1024; // 2MB
}
}
这套ASP.NET大文件上传解决方案经过多个项目的实际检验,在稳定性、兼容性和功能性方面都表现出色。特别是在处理超大文件和兼容老旧浏览器方面,相比市面上常见的方案有明显优势。开发过程中积累的经验和技巧,对于任何需要实现类似功能的.NET开发者都值得参考。