1. 项目背景与挑战
作为一名常年混迹于咖啡馆的.NET开发者,最近接到了一个极具挑战性的外包项目——开发一个支持20GB大文件上传下载的系统。这个项目有几个让人头秃的硬性要求:
- 必须兼容从IE8到现代浏览器的全平台支持
- 预算仅有100元(包含我的咖啡续命钱)
- 技术栈限定在ASP.NET WebForm+Vue3的诡异组合
- 需要实现文件夹层级结构保持、断点续传等高级功能
面对这种史诗级挑战,我不得不祭出多年积累的野路子编程技巧。下面分享这套在极端条件下依然能稳定运行的大文件上传方案,特别适合预算有限但需求复杂的场景。
2. 前端架构设计与实现
2.1 跨浏览器兼容方案
在兼容IE8这个上古浏览器时,最大的障碍是它根本不支持现代的文件API。我的解决方案是构建一个双模式上传器:
javascript复制class IE8FolderUploader {
constructor() {
this.fileTree = {};
this.chunkSize = 10 * 1024 * 1024; // 10MB分片
this.initFileInput();
}
initFileInput() {
const input = document.createElement('input');
input.type = 'file';
// 浏览器特性检测
if (document.all && !window.atob) {
// IE8专用方案
input.setAttribute('multiple', 'multiple');
input.onchange = (e) => this.handleIE8Files(e);
} else {
// 现代浏览器方案
input.setAttribute('webkitdirectory', '');
input.onchange = (e) => this.handleModernFiles(e);
}
document.body.appendChild(input);
}
}
关键点:通过
document.all && !window.atob精准识别IE8,因为IE8不支持atob方法且保留document.all特性
2.2 文件树结构构建
现代浏览器可以通过webkitRelativePath获取完整路径,但IE8需要特殊处理:
javascript复制// IE8文件处理(模拟文件夹结构)
handleIE8Files(event) {
const files = event.target.files;
const fakeTree = {};
for (let i = 0; i < files.length; i++) {
const pathParts = files[i].name.split('/');
let currentNode = fakeTree;
// 用文件名中的/模拟目录层级
for (let j = 0; j < pathParts.length - 1; j++) {
const dir = pathParts[j];
if (!currentNode[dir]) currentNode[dir] = {};
currentNode = currentNode[dir];
}
currentNode[pathParts.pop()] = files[i];
}
this.fileTree = fakeTree;
}
2.3 分片上传实现
大文件上传必须采用分片策略,这里实现了带断点续传的分片逻辑:
javascript复制// 分片上传核心逻辑
uploadChunk(file, fileId, chunkIndex, relativePath) {
const blob = file.slice(chunkIndex * this.chunkSize,
(chunkIndex + 1) * this.chunkSize);
const formData = new FormData();
// 构建分片数据包
formData.append('file', blob);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', Math.ceil(file.size / this.chunkSize));
formData.append('fileId', fileId);
formData.append('relativePath', relativePath);
// 兼容IE8的XHR
const xhr = window.XMLHttpRequest ? new XMLHttpRequest() :
new ActiveXObject("Microsoft.XMLHTTP");
xhr.open('POST', '/api/upload', true);
xhr.onload = () => {
if (xhr.status === 200) {
this.saveProgress(fileId, chunkIndex + 1);
}
};
xhr.send(formData);
}
3. 后端处理架构
3.1 分片接收与临时存储
ASP.NET WebForm接收分片的处理逻辑:
csharp复制protected void Page_Load(object sender, EventArgs e)
{
if (Request.HttpMethod == "POST")
{
var fileId = Request.Form["fileId"];
var chunkIndex = int.Parse(Request.Form["chunkIndex"]);
var file = Request.Files["file"];
// 临时分片存储路径
var tempPath = Server.MapPath($"~/App_Data/Temp/{fileId}_{chunkIndex}");
file.SaveAs(tempPath);
// 如果是最后一片则触发合并
if (chunkIndex == int.Parse(Request.Form["totalChunks"]) - 1)
{
MergeChunks(fileId, tempPath);
}
}
}
3.2 分片合并与加密
合并分片时的关键注意事项:
csharp复制private void MergeChunks(string fileId, string finalPath)
{
var tempDir = Server.MapPath("~/App_Data/Temp/");
// 确保目标目录存在
var dir = Path.GetDirectoryName(finalPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
// 按顺序合并所有分片
using (var fs = new FileStream(finalPath, FileMode.Create))
{
for (int i = 0; ; i++)
{
var chunkPath = Path.Combine(tempDir, $"{fileId}_{i}");
if (!File.Exists(chunkPath)) break;
var bytes = File.ReadAllBytes(chunkPath);
fs.Write(bytes, 0, bytes.Length);
File.Delete(chunkPath); // 及时清理临时文件
}
}
// 简单加密示例(实际项目应使用正规加密库)
EncryptFile(finalPath);
}
安全提示:实际项目中应使用AES或SM4等标准加密算法,示例中的加密仅作演示
4. 性能优化技巧
4.1 IIS配置调优
对于大文件上传,必须调整IIS的以下参数(在web.config中):
xml复制<system.web>
<httpRuntime maxRequestLength="2147483647" executionTimeout="3600" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="2147483647" />
</requestFiltering>
</security>
</system.webServer>
参数说明:
maxRequestLength:单个请求最大尺寸(单位KB)maxAllowedContentLength:请求内容最大长度(单位字节)executionTimeout:请求超时时间(单位秒)
4.2 SQL Server存储优化
文件元数据存储建议采用以下表结构:
sql复制CREATE TABLE FileRecords (
FileId UNIQUEIDENTIFIER PRIMARY KEY,
FileName NVARCHAR(255),
RelativePath NVARCHAR(MAX),
FileSize BIGINT,
ChunkSize INT,
TotalChunks INT,
UploadedChunks INT,
Status TINYINT, -- 0=上传中,1=已完成,2=已加密
CreateTime DATETIME DEFAULT GETDATE()
);
索引优化建议:
sql复制CREATE INDEX IX_FileRecords_Status ON FileRecords(Status)
INCLUDE (UploadedChunks, TotalChunks);
5. 疑难问题解决方案
5.1 IE8加密传输问题
由于IE8的ActiveXObject限制,建议采用以下方案:
- 前端使用BASE64编码分片数据
- 后端解码后验证数据完整性
- 使用C#的
System.Security.Cryptography进行AES解密
示例解密代码:
csharp复制public static string DecryptString(string cipherText, string key)
{
byte[] iv = new byte[16];
byte[] buffer = Convert.FromBase64String(cipherText);
using (Aes aes = Aes.Create())
{
aes.Key = Encoding.UTF8.GetBytes(key);
aes.IV = iv;
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
using (MemoryStream ms = new MemoryStream(buffer))
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
using (StreamReader sr = new StreamReader(cs))
{
return sr.ReadToEnd();
}
}
}
5.2 万级文件下载优化
针对浏览器并发连接数限制,实现分批下载策略:
csharp复制public ActionResult DownloadFolder(string folderId)
{
// 1. 获取文件夹下所有文件列表
var files = GetFileListFromDB(folderId);
// 2. 生成下载清单文件
var manifest = GenerateDownloadManifest(files);
// 3. 返回清单文件供前端分批下载
return File(Encoding.UTF8.GetBytes(manifest),
"text/plain", "download_manifest.txt");
}
// 前端根据清单分批下载
function batchDownload(manifest) {
const files = manifest.split('\n');
const BATCH_SIZE = 5; // 每批5个文件
for(let i=0; i<files.length; i+=BATCH_SIZE) {
setTimeout(() => {
const batch = files.slice(i, i+BATCH_SIZE);
batch.forEach(file => downloadFile(file));
}, i/BATCH_SIZE * 3000); // 每批间隔3秒
}
}
5.3 断点续传可靠性提升
将上传进度从IE8的userData迁移到SQL Server:
csharp复制public class UploadProgressService
{
public void SaveProgress(string fileId, int chunkIndex)
{
using (var conn = new SqlConnection(Config.DbConn))
{
var cmd = new SqlCommand(@"
IF EXISTS (SELECT 1 FROM FileRecords WHERE FileId=@fileId)
UPDATE FileRecords SET UploadedChunks=@chunkIndex
WHERE FileId=@fileId
ELSE
INSERT INTO FileRecords(FileId, UploadedChunks)
VALUES(@fileId, @chunkIndex)", conn);
cmd.Parameters.AddWithValue("@fileId", fileId);
cmd.Parameters.AddWithValue("@chunkIndex", chunkIndex);
conn.Open();
cmd.ExecuteNonQuery();
}
}
}
6. 部署与测试建议
6.1 环境配置要点
-
IIS应用程序池配置:
- 启用32位应用程序(某些老式加密组件需要)
- 设置闲置超时为0(防止长时间上传被回收)
- 内存限制调整为0(无限制)
-
临时文件清理:
创建定时任务清理超过24小时的临时分片:powershell复制# 每日凌晨清理临时文件 Get-ChildItem "D:\Web\App_Data\Temp\" -Recurse | Where LastWriteTime -lt (Get-Date).AddDays(-1) | Remove-Item -Force
6.2 压力测试方案
使用JMeter进行分阶段测试:
-
基础测试:
- 10个并发,每个上传100MB文件
- 监测IIS工作进程内存占用
-
极限测试:
- 50个并发,每个上传1GB文件
- 观察SQL Server的连接池状态
-
持久性测试:
- 随机中断上传过程
- 验证断点续传的准确性
测试指标重点关注:
- 平均上传速度
- 内存泄漏情况
- 断点续传成功率
7. 成本控制经验
在100元预算下完成项目的关键技巧:
-
开发环境:
- 使用VS Community版(免费)
- SQL Server Express版(免费)
- 开发机用老旧笔记本(0成本)
-
第三方组件:
- 文件加密:BouncyCastle(开源)
- 前端上传:改造WebUploader(MIT协议)
- 进度条:自制CSS动画(0成本)
-
云服务:
- 测试使用Azure免费额度(12个月免费)
- 生产环境用最便宜的共享主机(年费约300元,说服客户追加预算)
-
咖啡因摄入优化:
- 选择便利店咖啡(7-11美式8元/杯)
- 使用保温杯延长热咖啡寿命
- 凌晨2点后改喝速溶咖啡(成本降至1元/杯)