当前位置: 代码网 > it编程>编程语言>Asp.net > C#实现大文件分片上传完整指南

C#实现大文件分片上传完整指南

2026年04月15日 Asp.net 我要评论
大文件分片上传的核心思路是:前端将大文件切割成多个小分片,逐个发送到服务端暂存,全部接收完成后服务端按顺序合并还原。下面从前后端实现、数据库设计、断点续传、合并逻辑、并发优化和避坑指南六个维度来介绍。

大文件分片上传的核心思路是:前端将大文件切割成多个小分片,逐个发送到服务端暂存,全部接收完成后服务端按顺序合并还原。下面从前后端实现、数据库设计、断点续传、合并逻辑、并发优化和避坑指南六个维度来介绍。

一、核心原理

分片上传不是http协议的内置特性,需要业务层自行实现。前端使用file.slice()(浏览器)或filestream.read()(桌面端)将文件按固定大小切片,单片大小建议2~5 mb——太小增加http请求开销,太大降低失败重传效率。每次请求携带三个关键字段:fileid(全文件唯一标识)、chunkindex(从0开始的片序号)、totalchunks(总片数),服务端按fileid + chunkindex幂等写入,不能依赖请求顺序。

二、前端实现(c# 桌面端 / winforms / wpf)

/// <summary>
/// 大文件分片上传客户端(使用 httpclient)
/// </summary>
public class chunkuploader
{
    private static readonly httpclient _httpclient = new httpclient();
    private const int chunk_size = 5 * 1024 * 1024; // 5mb 每片
    private const string upload_url = "https://localhost:5001/api/upload/chunk";
    private const string merge_url = "https://localhost:5001/api/upload/merge";

    public async task<bool> uploadlargefileasync(string filepath, string fileid)
    {
        using var filestream = new filestream(filepath, filemode.open, fileaccess.read, 
                                                fileshare.read, 81920, fileoptions.asynchronous);
        long filesize = filestream.length;
        int totalchunks = (int)math.ceiling((double)filesize / chunk_size);
        
        // 1. 查询服务端已上传的分片(断点续传)
        var uploadedchunks = await getuploadedchunksasync(fileid);
        
        for (int chunkindex = 0; chunkindex < totalchunks; chunkindex++)
        {
            if (uploadedchunks.contains(chunkindex)) continue; // 跳过已上传的分片
            
            // 2. 读取分片数据
            int offset = chunkindex * chunk_size;
            int currentchunksize = (int)math.min(chunk_size, filesize - offset);
            byte[] chunkdata = new byte[currentchunksize];
            
            filestream.seek(offset, seekorigin.begin);
            await filestream.readasync(chunkdata, 0, currentchunksize);
            
            // 3. 计算当前分片的哈希值(用于完整性校验)
            string chunkhash = computesha256hash(chunkdata);
            
            // 4. 上传分片
            bool success = await uploadchunkasync(fileid, chunkindex, totalchunks, 
                                                    chunkdata, chunkhash);
            if (!success)
            {
                // 失败重试(带指数退避)
                success = await retryuploadasync(fileid, chunkindex, totalchunks, chunkdata, chunkhash);
                if (!success) return false;
            }
        }
        
        // 5. 所有分片上传完成,触发合并
        return await mergechunksasync(fileid, path.getfilename(filepath), filesize);
    }
    
    private async task<bool> uploadchunkasync(string fileid, int chunkindex, int totalchunks, 
                                                byte[] chunkdata, string chunkhash)
    {
        using var content = new multipartformdatacontent();
        content.add(new bytearraycontent(chunkdata), "file", $"chunk_{chunkindex}");
        content.add(new stringcontent(fileid), "fileid");
        content.add(new stringcontent(chunkindex.tostring()), "chunkindex");
        content.add(new stringcontent(totalchunks.tostring()), "totalchunks");
        content.add(new stringcontent(chunkhash), "chunkhash");
        
        var response = await _httpclient.postasync(upload_url, content);
        return response.issuccessstatuscode;
    }
    
    private async task<hashset<int>> getuploadedchunksasync(string fileid)
    {
        var response = await _httpclient.getasync($"{upload_url}/status?fileid={fileid}");
        if (!response.issuccessstatuscode) return new hashset<int>();
        
        var json = await response.content.readasstringasync();
        var uploaded = jsonserializer.deserialize<list<int>>(json);
        return new hashset<int>(uploaded ?? new list<int>());
    }
    
    private async task<bool> mergechunksasync(string fileid, string filename, long filesize)
    {
        var mergedata = new { fileid, filename, filesize };
        var content = new stringcontent(jsonserializer.serialize(mergedata), 
                                        encoding.utf8, "application/json");
        var response = await _httpclient.postasync(merge_url, content);
        return response.issuccessstatuscode;
    }
    
    private static string computesha256hash(byte[] data)
    {
        using var sha256 = sha256.create();
        byte[] hash = sha256.computehash(data);
        return convert.tohexstring(hash).tolowerinvariant();
    }
}

关键要点

  • httpclient 必须复用单例实例或用 ihttpclientfactory,否则会导致 socket 耗尽;
  • 超时时间需要显式配置为较大值(如 30 分钟),默认 100 秒不足以完成大文件上传;
  • .net 5+ 中 streamcontent 默认不会自动 dispose 底层流,建议改用 bytearraycontent 以确保安全。

三、服务端实现(asp.net core)

3.1 服务配置(program.cs)

var builder = webapplication.createbuilder(args);

// 禁用默认请求体大小限制(两层都要配置)
builder.webhost.configurekestrel(options =>
{
    options.limits.maxrequestbodysize = long.maxvalue; // 禁用 kestrel 层限制
});

builder.services.configure<formoptions>(options =>
{
    options.multipartbodylengthlimit = long.maxvalue; // 禁用 mvc 层限制
});

var app = builder.build();

asp.net core 中有两层请求体限制:kestrel 自身的 maxrequestbodysize(默认 30mb)和 mvc 层的 multipartbodylengthlimit两层必须同时调整才能生效。

3.2 分片上传 api(uploadcontroller)

[apicontroller]
[route("api/[controller]")]
[disablerequestsizelimit] // 禁用请求大小限制
public class uploadcontroller : controllerbase
{
    private readonly iuploadservice _uploadservice;
    
    public uploadcontroller(iuploadservice uploadservice)
    {
        _uploadservice = uploadservice;
    }
    
    /// <summary>
    /// 上传单个分片(绕过 iformfile,避免 oom)
    /// </summary>
    [httppost("chunk")]
    public async task<iactionresult> uploadchunk([fromform] chunkuploadrequest request)
    {
        // 验证参数
        if (string.isnullorempty(request.fileid) || request.chunkindex < 0)
            return badrequest("invalid parameters");
        
        // 验证分片哈希
        using var ms = new memorystream();
        await request.file.copytoasync(ms);
        byte[] chunkdata = ms.toarray();
        string computedhash = computesha256hash(chunkdata);
        
        if (!computedhash.equals(request.chunkhash, stringcomparison.ordinalignorecase))
            return badrequest("chunk hash mismatch");
        
        // 幂等保存:如果已存在则直接返回成功
        bool saved = await _uploadservice.savechunkasync(request.fileid, request.chunkindex, 
                                                          chunkdata, request.chunkhash);
        if (!saved)
            return conflict(new { message = "chunk already exists", index = request.chunkindex });
        
        return ok(new { success = true, index = request.chunkindex });
    }
    
    /// <summary>
    /// 查询已上传的分片索引(断点续传核心)
    /// </summary>
    [httpget("chunk/status")]
    public async task<iactionresult> getuploadedchunks([fromquery] string fileid)
    {
        var uploadedchunks = await _uploadservice.getuploadedchunkindicesasync(fileid);
        return ok(uploadedchunks);
    }
    
    /// <summary>
    /// 合并所有分片
    /// </summary>
    [httppost("merge")]
    public async task<iactionresult> mergechunks([frombody] mergerequest request)
    {
        // 加锁防止并发合并
        bool merged = await _uploadservice.mergechunksasync(request.fileid, request.filename);
        if (!merged)
            return conflict(new { message = "merge failed or already in progress" });
        
        return ok(new { success = true, filepath = $"/uploads/{request.filename}" });
    }
}

public class chunkuploadrequest
{
    public string fileid { get; set; }
    public int chunkindex { get; set; }
    public int totalchunks { get; set; }
    public string chunkhash { get; set; }
    public iformfile file { get; set; }
}

public class mergerequest
{
    public string fileid { get; set; }
    public string filename { get; set; }
    public long filesize { get; set; }
}

关键要点

  • 不要使用 iformfile 直接处理 gb 级文件,它会触发完整文件读取和内存缓冲,导致 oom。但分片上传场景下单片只有 2-5 mb,用 iformfile 是可行的;
  • 每片保存后必须校验哈希,网络传输中单片出错很常见,仅靠文件大小无法判断内容正确性;
  • 接口必须支持幂等写入——重复上传同一片应直接返回成功,而非报错。

四、数据库设计(跟踪上传状态)

为支持断点续传和状态恢复,需要设计两张核心表:

上传会话表(uploadsession)

字段类型说明
sessionidguid pk文件上传会话唯一标识
filenamevarchar(255)原始文件名
filesizebigint文件总大小(字节)
filehashvarchar(128)整个文件的 sha256 值(秒传校验)
chunksizeint分片大小(字节)
totalchunksint总分片数
uploadedchunkscountint已上传分片数
statustinyint状态:0-上传中,1-合并中,2-已完成,3-失败
createdatdatetime2创建时间
updatedatdatetime2更新时间

分片记录表(uploadedchunk)

字段类型说明
chunkidbigint pk自增主键
sessionidguid fk关联到 uploadsession
chunkindexint分片序号(从 0 开始)
chunksizeint该分片大小(最后一片可能较小)
chunkhashvarchar(128)该分片的 sha256 值
storedpathvarchar(500)分片在磁盘上的存储路径
uploadedatdatetime2上传时间

状态持久化策略

内存维护活跃会话可以提升性能,但进程崩溃会丢失状态。生产环境应在关键节点落库:首次上传时插入记录,每个分片成功后更新 uploadedchunkscountlastchunkindex,合并完成后将 status 改为 completed 并清理临时文件。

五、分片合并实现

/// <summary>
/// 安全合并分片(使用 seek 定位写入,避免内存溢出)
/// </summary>
public async task<bool> mergechunksasync(string fileid, string finalfilename)
{
    var chunks = await getchunksorderedasync(fileid);
    if (chunks.count == 0) return false;
    
    // 检查是否所有分片都已到达
    int totalchunks = await gettotalchunkscountasync(fileid);
    if (chunks.count != totalchunks) return false;
    
    string tempdir = path.combine(_config["storage:chunkpath"], fileid);
    string finalpath = path.combine(_config["storage:finalpath"], finalfilename);
    
    // 使用 filestream 配合 seek 定位写入,而非全量加载
    using var finalstream = new filestream(finalpath, filemode.create, fileaccess.write, 
                                           fileshare.none, 81920, useasync: true);
    
    int chunksize = _config.getvalue<int>("chunksize", 5 * 1024 * 1024);
    
    foreach (var chunk in chunks)
    {
        long offset = chunk.chunkindex * (long)chunksize;
        finalstream.seek(offset, seekorigin.begin);
        
        string chunkpath = path.combine(tempdir, $"{fileid}_{chunk.chunkindex}.tmp");
        using var chunkstream = new filestream(chunkpath, filemode.open, fileaccess.read);
        await chunkstream.copytoasync(finalstream);
    }
    
    await finalstream.flushasync();
    
    // 合并完成后校验全文件哈希(可选)
    string finalhash = await computefilesha256async(finalpath);
    if (!finalhash.equals(await getexpectedfilehashasync(fileid), stringcomparison.ordinalignorecase))
    {
        file.delete(finalpath);
        return false;
    }
    
    // 清理临时分片文件和目录
    foreach (var chunk in chunks)
    {
        file.delete(path.combine(tempdir, $"{fileid}_{chunk.chunkindex}.tmp"));
    }
    directory.delete(tempdir);
    
    return true;
}

合并要点

  • 不要用 file.appendallbytes()file.readallbytes() + file.writeallbytes(),大文件会内存溢出;
  • 必须使用 filestream.seek() 按分片编号计算偏移量后写入,确保写入位置精确;
  • 合并前必须校验三个条件:分片哈希完整、全部分片已到达、加锁防止并发合并;
  • 合并成功后立即清理临时文件,失败时也要清理并标记任务为失败状态;
  • 建议设置后台定时任务(如每 30 分钟执行一次),扫描并清理超过 2 小时未完成上传的临时分片。

六、断点续传实现

断点续传的核心是 客户端在开始上传前先向服务端查询已接收的分片索引,跳过这些索引再上传剩余分片

流程如下:

  1. 客户端计算 fileid(通常为 文件名_文件大小_最后修改时间 或文件内容的 md5);
  2. 客户端发送 head/get 请求 get /api/upload/chunk/status?fileid=xxx,获取服务端已接收的 chunkindex 列表;
  3. 客户端比对本地分片列表,跳过已上传的分片,仅上传缺失部分;
  4. 每上传成功一个分片,服务端立即持久化状态到数据库;
  5. 所有分片上传完成后,调用 /merge 接口触发合并。

注意事项

  • 不要用本地文件修改时间或 md5 做续传依据,服务端可能清理过临时文件;
  • 每个分片上传后必须检查 http 状态码和响应体中的明确确认信息,遇到 409 conflict(分片已存在)可直接跳过,遇到 500 错误则采用指数退避重试策略(最多 3 次);
  • 断点续传需要服务端持久化状态,仅依赖磁盘临时文件是不够的——iis 或 kestrel 重启后已上传的分片会丢失。

七、并发上传优化

多个分片可以并发上传以提升效率,但需控制并发数避免带宽抢占:

// 使用 semaphoreslim 控制最大并发数
private static readonly semaphoreslim _semaphore = new semaphoreslim(3); // 最多 3 个并发

public async task uploadwithconcurrencyasync(string filepath, string fileid, int totalchunks)
{
    var tasks = new list<task>();
    
    for (int chunkindex = 0; chunkindex < totalchunks; chunkindex++)
    {
        await _semaphore.waitasync();
        int index = chunkindex; // 捕获变量
        
        tasks.add(task.run(async () =>
        {
            try
            {
                await uploadsinglechunkasync(filepath, fileid, index, totalchunks);
            }
            finally
            {
                _semaphore.release();
            }
        }));
    }
    
    await task.whenall(tasks);
}

八、避坑指南

1. 服务端默认限制问题

asp.net core 有两层请求体限制,必须同时调整才生效。kestrel 默认 maxrequestbodysize 为 30mb,mvc 层也有自己的限制,两层都要配置为 long.maxvalue

2. stream 行为差异

.net framework 中 streamcontent 会自动 dispose 底层流,而 .net 5+ 默认不会。建议统一使用 bytearraycontent 避免兼容性问题。

3. http 顺序不可靠

http 请求不保证顺序到达,服务端必须以 fileid + chunkindex 为准进行幂等写入,不能依赖请求到达顺序进行合并。

4. 大文件哈希计算

计算整个文件的 sha256 时,不要用 sha256.create().computehash(filestream) 一次性读入内存,而应使用 transformblock / transformfinalblock 增量分块计算,避免 oom。

5. 合并时的并发控制

合并操作必须加锁防止并发多次触发。可使用文件锁(filestream.lock())或分布式锁(如 redis setnx)实现。

6. 临时文件清理

必须设置自动清理机制:用后台定时任务扫描 lastmodified 超过设定时间(如 2 小时)的临时分片并删除,避免磁盘被残留文件占满。

九、方案选择建议

方案适用场景优点缺点
自建分片上传需要完全掌控、自定义业务逻辑灵活可控、无外部依赖开发成本高、需要处理所有边界情况
webuploader + asp.net mvcweb 端大文件上传,历史项目成熟稳定、社区资源多前端依赖外部组件
阿里云 oss / 腾讯云 cos直接对接云存储分片上传已内置、高可靠、支持断点续传需要云服务账号、有流量费用
azure blob storage微软生态项目与 .net 集成好、原生支持块上传仅限 azure 环境

建议:如果项目已经使用云存储,优先使用云厂商的 sdk(如阿里云 oss、azure blob、腾讯云 cos),它们内置了分片上传、断点续传和错误重试机制。如果需要完全自建,请务必关注上述的数据库设计、幂等性、并发控制和临时文件清理等生产环境要点。

以上就是c#实现大文件分片上传完整指南的详细内容,更多关于c#大文件分片上传的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com