C#如何结合开源库实现.NET Core百万文件上传的优化方案?
.NET程序员的血泪奋斗史:从0到1搞定大文件上传(含IE8兼容)
咱福建.NET仔最近接了个外包活,客户是做政府资料管理的,需求就一句话:“搞个大文件上传功能,20G文件随便传,文件夹要留层级,IE8也能用,加密传输存储,断点续传不能丢进度!”
客户还补刀:“网上那些开源组件都是垃圾,WebUploader停更了,H5方案IE8跑不动,我要原生JS!预算100块以内,源码要全,文档要细,不然换人!”
得,硬着头皮上吧!以下是我踩坑一周后总结的全栈解决方案,含前后端代码、IE8兼容技巧、加密和断点续传实现,直接复制就能用!
一、需求拆解:客户要的到底是个啥?
用大白话翻译客户需求:
- 大文件上传:20G文件分片传,断点续传(关浏览器/重启电脑不丢进度)。
- 文件夹保留层级:上传
/部门/2024项目/数据.xlsx,下载时也得是这个结构(别给我打包成zip!)。 - 加密:传输用SM4/AES,存储也加密(客户说“数据比命重要”)。
- 兼容IE8:Win7+IE8的老机器必须能用(甲方爸爸的机器不能碰)。
- 后端ASP.NET WebForm:客户不让换技术栈(只能硬啃WebForm)。
二、前端:原生JS + IE8兼容的“土味”方案
IE8不支持File API,不能用H5的分片上传,只能用Flash或iframe表单模拟异步。但客户要原生JS,所以选iframe+表单方案(Flash已被淘汰,且客户可能禁用)。
1. 前端核心代码(Vue3兼容IE8?不存在的,IE8只认ES5)
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="UploadPage.aspx.cs" Inherits="BigFileUpload.UploadPage" %>
大文件上传(IE8兼容版)
// 全局变量:存储上传任务、进度、加密密钥
var uploadTasks = {}; // 格式:{ fileId: { progress: 0, chunks: [], totalChunks: 0 } }
var sm4Key = '客户给的SM4密钥(实际从后端动态获取)'; // 加密密钥(硬编码仅示例,实际要安全存储)
// 初始化:绑定文件选择按钮
window.onload = function() {
document.getElementById('btnSelect').onclick = function() {
document.getElementById('fileInput').click(); // 触发文件选择
};
document.getElementById('fileInput').onchange = handleFileSelect;
};
// 处理文件选择(支持文件夹?IE8不支持webkitdirectory,只能手动输入路径)
function handleFileSelect(e) {
var file = e.target.files[0];
if (!file) return;
// 生成唯一fileId(防重复)
var fileId = 'file_' + Date.now();
uploadTasks[fileId] = {
progress: 0,
chunks: [],
totalChunks: Math.ceil(file.size / 2 * 1024 * 1024), // 分片大小2MB
fileName: file.name,
fileSize: file.size
};
// 计算文件MD5(用于断点校验,IE8用SparkMD5的兼容版)
calculateFileMD5(file, fileId);
}
// 计算文件MD5(IE8兼容版,用SparkMD5的旧版)
function calculateFileMD5(file, fileId) {
var chunkSize = 2 * 1024 * 1024; // 2MB/片
var chunks = Math.ceil(file.size / chunkSize);
var spark = new SparkMD5.ArrayBuffer();
var reader = new FileReader();
// 分片读取文件(IE8用FileReader的同步模式)
function loadNextChunk(currentChunk) {
var start = currentChunk * chunkSize;
var end = Math.min(start + chunkSize, file.size);
reader.readAsArrayBuffer(file.slice(start, end)); // IE8可能不支持slice,改用substring?
}
reader.onload = function(e) {
spark.append(e.target.result);
var currentChunk = Math.floor(e.target.result.byteLength / (2 * 1024 * 1024));
if (currentChunk < chunks - 1) {
loadNextChunk(currentChunk);
} else {
var md5 = spark.end();
uploadTasks[fileId].md5 = md5; // 记录MD5(断点校验用)
startUpload(fileId); // 开始上传
}
};
loadNextChunk(0);
}
// 开始上传(分片+断点续传)
function startUpload(fileId) {
var task = uploadTasks[fileId];
var chunkIndex = task.chunks.length; // 下一个要传的分片序号
// 检查是否已上传过该分片(从localStorage读进度)
var savedProgress = localStorage.getItem('upload_' + fileId);
if (savedProgress) {
task.chunks = JSON.parse(savedProgress).chunks;
chunkIndex = task.chunks.length;
}
// 上传当前分片(IE8用iframe模拟异步)
var formData = new FormData();
formData.append('fileId', fileId);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', task.totalChunks);
formData.append('fileName', task.fileName);
formData.append('chunk', task.file.slice(chunkIndex * 2 * 1024 * 1024, (chunkIndex + 1) * 2 * 1024 * 1024));
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'UploadHandler.ashx?action=upload&fileId=' + fileId + '&chunkIndex=' + chunkIndex;
document.body.appendChild(iframe);
// 监听iframe加载完成(上传结果)
iframe.onload = function() {
var response = JSON.parse(iframe.contentWindow.document.body.innerText);
if (response.code === 200) {
task.chunks.push(chunkIndex);
task.progress = (task.chunks.length / task.totalChunks) * 100;
localStorage.setItem('upload_' + fileId, JSON.stringify(task)); // 保存进度到localStorage
if (task.chunks.length === task.totalChunks) {
mergeChunks(fileId); // 合并所有分片
}
} else {
alert('上传失败:' + response.msg);
}
document.body.removeChild(iframe);
};
}
// 合并分片(调用后端接口)
function mergeChunks(fileId) {
var xhr = new XMLHttpRequest();
xhr.open('POST', 'UploadHandler.ashx?action=merge&fileId=' + fileId);
xhr.onload = function() {
var response = JSON.parse(xhr.responseText);
if (response.code === 200) {
alert('上传完成!文件路径:' + response.filePath);
localStorage.removeItem('upload_' + fileId); // 清除进度
} else {
alert('合并失败:' + response.msg);
}
};
xhr.send();
}
选择文件(支持20G)
2. 前端关键技巧(IE8兼容)
- 分片上传:用
File.slice()(IE8不支持,改用file.substring()?实际测试IE8的File对象不完整,可能需要用ActiveXObject读取文件流,这里简化处理)。 - 进度保存:用
localStorage存上传进度(IE8支持),但容量有限(建议分块存储,比如每5%存一次)。 - 加密预处理:前端用SM4加密分片(示例未写,需引入
sm-crypto库的ES5兼容版)。
三、后端:ASP.NET WebForm(C#)实现分片合并+加密存储
后端需要处理分片接收、断点校验、合并文件、加密存储,还要兼容SQL Server。
1. 后端核心代码(UploadHandler.ashx)
// UploadHandler.ashx(处理上传请求)
public class UploadHandler : IHttpHandler, IRequiresSessionState
{
private string _uploadDir = HttpContext.Current.Server.MapPath("~/Uploads/Temp/"); // 临时分片存储路径
private string _encryptKey = "客户给的SM4密钥(从配置或数据库读取)"; // 加密密钥
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "application/json";
string action = context.Request["action"];
switch (action)
{
case "upload": // 接收分片
UploadChunk(context);
break;
case "merge": // 合并分片
MergeChunks(context);
break;
default:
context.Response.Write(JsonConvert.SerializeObject(new { code = 400, msg = "无效操作" }));
break;
}
}
// 接收分片并存储(带SM4加密)
private void UploadChunk(HttpContext context)
{
string fileId = context.Request["fileId"];
int chunkIndex = int.Parse(context.Request["chunkIndex"]);
int totalChunks = int.Parse(context.Request["totalChunks"]);
string fileName = context.Request["fileName"];
// 校验分片是否已上传(防重复)
if (IsChunkUploaded(fileId, chunkIndex))
{
context.Response.Write(JsonConvert.SerializeObject(new { code = 200, msg = "分片已上传" }));
return;
}
// 读取分片内容(加密传输)
HttpPostedFile chunkFile = context.Request.Files["chunk"];
byte[] encryptedChunk = new byte[chunkFile.ContentLength];
chunkFile.InputStream.Read(encryptedChunk, 0, chunkFile.ContentLength);
// SM4解密分片(与前端加密对应)
byte[] decryptedChunk = SM4Decrypt(encryptedChunk, _encryptKey);
// 保存分片到临时目录
string tempDir = Path.Combine(_uploadDir, fileId);
if (!Directory.Exists(tempDir)) Directory.CreateDirectory(tempDir);
string chunkPath = Path.Combine(tempDir, $"chunk_{chunkIndex}");
File.WriteAllBytes(chunkPath, decryptedChunk);
// 记录分片元数据到数据库(SQL Server)
SaveChunkMetadata(fileId, chunkIndex, totalChunks, fileName);
context.Response.Write(JsonConvert.SerializeObject(new { code = 200, msg = "分片上传成功" }));
}
// 合并分片并存储到目标路径
private void MergeChunks(HttpContext context)
{
string fileId = context.Request["fileId"];
string tempDir = Path.Combine(_uploadDir, fileId);
string fileName = GetFileNameFromMetadata(fileId); // 从数据库获取文件名
string targetPath = Path.Combine(HttpContext.Current.Server.MapPath("~/Uploads/"), fileName);
// 合并所有分片(流式写入,避免内存溢出)
using (FileStream fs = new FileStream(targetPath, FileMode.Create))
{
for (int i = 0; i < GetTotalChunksFromMetadata(fileId); i++)
{
string chunkPath = Path.Combine(tempDir, $"chunk_{i}");
byte[] decryptedChunk = SM4Decrypt(File.ReadAllBytes(chunkPath), _encryptKey);
fs.Write(decryptedChunk, 0, decryptedChunk.Length);
File.Delete(chunkPath); // 删除临时分片
}
}
// 清理临时目录
Directory.Delete(tempDir);
// 保存文件元数据到数据库(SQL Server)
SaveFileMetadata(fileId, fileName, targetPath);
context.Response.Write(JsonConvert.SerializeObject(new { code = 200, msg = "合并成功", filePath = targetPath }));
}
// SM4加密工具方法(示例,需引入SM4库)
private byte[] SM4Encrypt(byte[] data, string key)
{
// 实际使用BouncyCastle或专用SM4库实现
return data; // 简化示例
}
private byte[] SM4Decrypt(byte[] data, string key)
{
// 实际使用BouncyCastle或专用SM4库实现
return data; // 简化示例
}
// 数据库操作(示例,使用SqlClient)
private void SaveChunkMetadata(string fileId, int chunkIndex, int totalChunks, string fileName)
{
using (SqlConnection conn = new SqlConnection("Server=.;Database=FileDB;User Id=sa;Password=123456;"))
{
conn.Open();
string sql = @"INSERT INTO FileChunks (FileId, ChunkIndex, TotalChunks, FileName)
VALUES (@FileId, @ChunkIndex, @TotalChunks, @FileName)";
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@FileId", fileId);
cmd.Parameters.AddWithValue("@ChunkIndex", chunkIndex);
cmd.Parameters.AddWithValue("@TotalChunks", totalChunks);
cmd.Parameters.AddWithValue("@FileName", fileName);
cmd.ExecuteNonQuery();
}
}
}
2. 后端关键逻辑
- 分片校验:通过数据库记录已上传的分片,避免重复上传。
- 加密存储:分片传输时用SM4加密,后端解密后存储(密钥需安全管理,建议从配置中心获取)。
- 文件夹结构保留:前端上传时传递文件夹路径(如
/部门/2024项目/),后端根据路径创建目录(示例未写,需在前端fileName中包含路径,后端解析后Directory.CreateDirectory)。
四、部署与调试(IE8必看)
-
环境准备:
- 服务器装IIS,启用ASP.NET WebForm支持(.NET Framework 4.8)。
- SQL Server创建数据库
FileDB,执行以下脚本建表:CREATE TABLE FileChunks ( Id INT PRIMARY KEY IDENTITY, FileId NVARCHAR(50) NOT NULL, ChunkIndex INT NOT NULL, TotalChunks INT NOT NULL, FileName NVARCHAR(255) NOT NULL, UNIQUE (FileId, ChunkIndex) ); CREATE TABLE FileMetadata ( FileId NVARCHAR(50) PRIMARY KEY, FileName NVARCHAR(255) NOT NULL, FilePath NVARCHAR(500) NOT NULL, UploadTime DATETIME DEFAULT GETDATE() );
-
IE8兼容调试:
- 用虚拟机装Win7+IE8,测试上传功能(重点测试分片续传、文件夹路径)。
- 前端
fileInput的webkitdirectory属性在IE8无效,需手动输入文件夹路径(如/部门/2024项目/),后端解析后创建目录。
五、源代码与支持
以上代码是简化版,实际需要:
- 引入SM4加密库(如
BouncyCastle的C#版本)。 - 处理IE8的
File对象兼容问题(可能需要用ActiveXObject读取文件流)。 - 优化分片上传的并发控制(IE8不支持
Promise,用setTimeout模拟异步)。
需要完整源码(含加密、文件夹路径处理、SQL Server脚本),可以加群374992201,我免费发你(群里还有红包和接单资源)。
(PS:客户说“能跑起来就行”,但我争取做到“稳如老狗”——毕竟这是吃饭的本事,冲就完事了!)
设置框架
安装.NET Framework 4.7.2
https://dotnet.microsoft.com/en-us/download/dotnet-framework/net472
框架选择4.7.2
添加3rd引用

编译项目

NOSQL
NOSQL无需任何配置可直接访问页面进行测试
SQL
使用IIS
大文件上传测试推荐使用IIS以获取更高性能。
使用IIS Express
小文件上传测试可以使用IIS Express
创建数据库

配置数据库连接信息

检查数据库配置

访问页面进行测试

相关参考:
文件保存位置,
效果预览
文件上传

文件刷新续传
支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件夹上传
支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
下载完整示例
更多推荐

所有评论(0)