在汽车制造企业的日常运营中,设计图纸、工艺文档和质检报告等大型文件的传输是刚需。我们最近接到一个特殊需求:某整车厂需要在其供应商协同平台上实现20GB以上工程文件的可靠传输,且必须兼容IE8浏览器(因部分供应商仍在使用老旧系统)。这个看似简单的"文件上传"需求,在实际开发中演变成了一场与浏览器限制、网络环境和安全要求的全面较量。
核心痛点:
- 浏览器内存限制:现代前端技术处理大文件时容易触发内存溢出
- 古董浏览器兼容:IE8不支持现代File API,更别提文件夹操作
- 企业级安全要求:传输过程需加密,且要保留完整的文件夹层级结构
经过多轮技术验证,最终确定的架构方案如下:
code复制[浏览器端]
├─ 现代浏览器方案:HTML5 File API + 分片上传
└─ IE8降级方案:ActiveXObject + 多文件选择
[服务端]
├─ 分片接收:ASP.NET WebForm + 临时文件存储
├─ 文件合并:磁盘IO流式操作
└─ 加密存储:SM4算法 + 阿里云OSS直传
[持久层]
├─ SQL Server:记录上传状态
└─ 本地存储:localStorage保存断点信息
选择ASP.NET WebForm而非MVC的原因在于:
通过测试不同网络环境下的传输稳定性,最终确定5MB为最优分片大小:
分片大小 = 网络带宽(Mbps) × 平均RTT(ms) / 8 × 0.8| 算法 | 密钥长度 | 浏览器兼容性 | 国密支持 | 最终选择 |
|---|---|---|---|---|
| AES | 128/256 | 现代浏览器 | 否 | 备选 |
| SM4 | 128 | 需polyfill | 是 | 采用 |
| 3DES | 168 | 全支持 | 否 | 淘汰 |
选择SM4的原因:
现代浏览器通过webkitRelativePath获取完整路径的方案存在兼容性问题。我们采用深度优先遍历算法处理文件夹结构:
javascript复制async function scanDirectory(directory, basePath = '') {
const fileList = [];
const dirReader = directory.createReader();
const entries = await new Promise(resolve =>
dirReader.readEntries(resolve)
);
for (const entry of entries) {
const fullPath = `${basePath}/${entry.name}`;
if (entry.isFile) {
fileList.push({
file: await getFile(entry),
path: fullPath
});
} else if (entry.isDirectory) {
fileList.push(...await scanDirectory(entry, fullPath));
}
}
return fileList;
}
避坑指南:
- Chrome对同一目录的readEntries调用可能返回空数组,需要循环调用直到返回空
- Safari中文件路径包含中文时会出现乱码,需额外进行URI编解码
对于IE8环境,我们开发了两套降级方案:
javascript复制// 方案1:多文件选择
function initIE8Upload() {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.onchange = function() {
Array.from(this.files).forEach(file => {
uploadFile(file, file.name); // 丢失文件夹结构
});
};
input.click();
}
// 方案2:ActiveX控件(需用户安装)
function initActiveXUpload() {
try {
const ax = new ActiveXObject('Scripting.FileSystemObject');
const folder = ax.BrowseForFolder(0, '选择文件夹', 0);
// 递归获取文件逻辑...
} catch (e) {
fallbackToMultiSelect();
}
}
ASP.NET处理大文件上传时需要调整web.config配置:
xml复制<system.web>
<httpRuntime maxRequestLength="2147483647" executionTimeout="3600" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="4294967295" />
</requestFiltering>
</security>
</system.webServer>
分片合并的流式处理代码:
csharp复制public static void MergeFiles(string tempDir, string outputPath)
{
using (var output = File.Create(outputPath))
{
foreach (var chunkFile in Directory.GetFiles(tempDir)
.OrderBy(f => int.Parse(Path.GetFileName(f))))
{
using (var input = File.OpenRead(chunkFile))
{
input.CopyTo(output);
}
File.Delete(chunkFile);
}
}
Directory.Delete(tempDir);
}
采用BouncyCastle库实现SM4加密流:
csharp复制public static void EncryptStream(Stream input, Stream output)
{
var engine = new SM4Engine();
var keyParam = new KeyParameter(Encoding.UTF8.GetBytes("16-byte-secret-key"));
var iv = new byte[16]; // 初始化向量
engine.Init(true, new ParametersWithIV(keyParam, iv));
var buffer = new byte[4096];
int bytesRead;
while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
{
var outBytes = new byte[bytesRead];
engine.ProcessBytes(buffer, 0, bytesRead, outBytes, 0);
output.Write(outBytes, 0, bytesRead);
}
}
处理大文件时最容易出现内存溢出,我们采用以下策略:
分片流式处理:避免一次性加载整个分片到内存
csharp复制public static void ProcessChunk(Stream chunkStream)
{
using (var memoryStream = new MemoryStream())
{
chunkStream.CopyTo(memoryStream, 81920); // 缓冲区设为80KB
// 处理逻辑...
}
}
GC手动控制:在适当位置强制垃圾回收
csharp复制GC.Collect();
GC.WaitForPendingFinalizers();
数组池技术:重用字节数组减少分配开销
csharp复制var buffer = ArrayPool<byte>.Shared.Rent(81920);
try {
// 使用buffer...
} finally {
ArrayPool<byte>.Shared.Return(buffer);
}
针对文件传输记录表做了以下优化:
sql复制-- 原始表结构优化
CREATE TABLE FileTransfers (
FileId VARCHAR(64) PRIMARY KEY,
OriginalName NVARCHAR(255) INDEX,
OSSPath VARCHAR(255),
ChunkCount INT,
CompletedChunks VARCHAR(MAX),
CreatedTime DATETIME DEFAULT GETDATE() INDEX
)
-- 添加分片状态位图索引
CREATE SPATIAL INDEX IX_ChunkStatus ON FileTransfers
(
CompletedChunks
) USING GEOMETRY_GRID
WITH (
BOUNDING_BOX = (0, 0, 10000, 10000)
);
针对大文件上传的IIS配置要点:
调整上传限制:
powershell复制Set-WebConfigurationProperty -Filter /system.webServer/security/requestFiltering -Name requestLimits.maxAllowedContentLength -Value 4294967295
启用动态内容压缩:
xml复制<httpCompression>
<dynamicTypes>
<add mimeType="application/octet-stream" enabled="true" />
</dynamicTypes>
</httpCompression>
连接数优化:
bash复制appcmd set config /section:serverRuntime /appConcurrentRequestLimit:10000
为满足汽车制造企业24小时生产需求,设计双活架构:
code复制[区域A]
├─ 负载均衡:F5 BIG-IP
├─ Web服务器:IIS ARR集群
└─ 存储:阿里云OSS+本地NAS
[区域B]
├─ 负载均衡:Nginx
├─ Web服务器:Tomcat集群
└─ 存储:MinIO分布式存储
[同步机制]
├─ 元数据同步:SQL Server AlwaysOn
└─ 文件同步:rsync+inotify实时同步
在以下环境进行压力测试:
| 文件大小 | 分片大小 | 上传时间 | 内存占用 | CPU负载 |
|---|---|---|---|---|
| 1GB | 1MB | 2m38s | 120MB | 45% |
| 1GB | 5MB | 1m52s | 350MB | 60% |
| 20GB | 5MB | 38m12s | 400MB | 70% |
| 20GB | 10MB | 35m47s | 800MB | 85% |
| 浏览器 | 文件夹上传 | 断点续传 | 加密传输 | 备注 |
|---|---|---|---|---|
| Chrome 120 | ✓ | ✓ | ✓ | 完整功能 |
| Firefox 115 | ✓ | ✓ | ✓ | 性能最优 |
| Edge 120 | ✓ | ✓ | ✓ | 同Chrome |
| Safari 16 | ✓ | ✓ | ✓ | 路径中文需编码 |
| IE11 | △ | ✓ | ✓ | 需启用兼容模式 |
| IE8 | × | △ | △ | 需ActiveX支持,功能受限 |
在实施这个汽车制造业文件传输系统的过程中,有几个关键经验值得分享:
浏览器特性检测比版本检测更可靠
不要简单判断navigator.userAgent,而应该用实际API检测:
javascript复制const supportsFileSystem = 'webkitEntries' in HTMLInputElement.prototype;
const supportsDirectory = 'FileSystemDirectoryEntry' in window;
分片上传的哈希校验必不可少
我们采用分段MD5校验方案:
csharp复制public static string CalculateChunkHash(Stream stream)
{
using (var md5 = MD5.Create())
{
var hashBytes = md5.ComputeHash(stream);
stream.Position = 0;
return BitConverter.ToString(hashBytes).Replace("-", "");
}
}
企业级项目必须考虑离线恢复
除了localStorage,我们还实现了IndexedDB的降级方案:
javascript复制function saveProgress(fileId, chunks) {
if ('indexedDB' in window) {
// 使用IndexedDB存储
} else {
// 降级到cookie存储
}
}
这个项目让我深刻体会到:在工业级软件开发中,没有"完美"的技术方案,只有针对特定场景的"最适"解决方案。面对汽车制造企业复杂的IT环境和严格的安全要求,我们需要在技术先进性和系统稳定性之间找到精准的平衡点。