You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#并行分块下载文件使用MemoryMappedFile写入出现损坏问题排查

问题原因
  • Range范围与视图长度不匹配:HTTP Range 头的区间是闭区间,例如Range: bytes=0-99 代表请求前100个字节(包含下标99)。原代码请求分块时指定的范围是[piece.Start, piece.End],返回的内容长度为 piece.End - piece.Start + 1,但创建内存映射视图流时用了piece.End-piece.Start - 1,直接截断了2字节的写入空间,每个分块都会丢失末尾2字节,最终文件自然损坏。
  • 异步方法同步阻塞导致死锁/数据丢失:在Parallel.ForEachAsync的异步委托中使用.Result、.Wait() 同步阻塞,极易引发线程池饥饿、请求死锁,部分分块的写入逻辑可能未执行完成就被释放,导致区块缺失。
  • HttpClient使用错误:原代码中httpPool.Get() 属于无效调用(HttpClient无此方法),且HttpClient本身是线程安全类,不需要重复获取实例,错误的实例获取逻辑会导致请求失败,对应分块无数据写入。
  • 资源未正确释放:内存映射文件、视图流未用using 做生命周期管理,缓冲区数据可能未完全刷入磁盘就被释放,导致文件尾部损坏。
  • 缺少服务端范围请求校验:未校验目标服务器是否支持Accept-Ranges 头,若服务端不支持分块下载,所有Range请求都会返回完整文件,直接导致写入内容重叠损坏。
  • 未定义并行配置:原代码中parallelOptions 变量未声明,编译无法通过,运行时默认并行度不受控,容易触发服务端限流导致部分分块下载失败。
修复方案

核心修改点

  1. 修正视图流长度与Range请求参数,保证请求长度与写入长度完全一致
  2. 移除所有同步阻塞调用,全程使用await执行异步操作
  3. 直接复用线程安全的HttpClient实例,删除无效的实例获取逻辑
  4. 为所有非托管资源添加using声明,自动完成资源释放与缓冲区刷写
  5. 新增服务端范围请求支持校验,避免不支持分块的场景下出现文件损坏
  6. 显式定义并行配置,控制最大并行度避免触发服务端限流
  7. 替换已过时的WebRequest,改用HttpClient发送Head请求获取文件长度

修复后完整代码

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using static System.Console;

namespace ZenTester
{
    internal class FileChunk
    {
        public long Start { get; set; }
        public long End { get; set; }
        public int Id { get; set; }

        public FileChunk(long startByte, long endByte)
        {
            Start = startByte;
            End = endByte;
        }
    }

    internal class RetryHandler : DelegatingHandler
    {
        private int _maxRetries = 3;

        public RetryHandler(HttpMessageHandler innerHandler) : base(innerHandler) { }

        public RetryHandler(HttpMessageHandler innerHandler, int maxRetries) : base(innerHandler)
        {
            _maxRetries = maxRetries;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            HttpResponseMessage response = null;
            for (var i = 0; i < _maxRetries; i++)
            {
                response = await base.SendAsync(request, cancellationToken);
                if (response.IsSuccessStatusCode)
                {
                    return response;
                }
                // 失败后释放响应资源避免泄漏
                response.Dispose();
            }
            return response;
        }
    }

    public static class ZenTester
    {
        private static async Task DownloadFile(string url, int parts, string outFile = null!)
        {
            using var httpClient = new HttpClient(new RetryHandler(new HttpClientHandler(), 10)) 
            { 
                MaxResponseContentBufferSize = 1000000000 
            };
            
            // 发Head请求获取文件信息
            using var headRequest = new HttpRequestMessage(HttpMethod.Head, url);
            using var headResponse = await httpClient.SendAsync(headRequest);
            headResponse.EnsureSuccessStatusCode();
            
            var responseLength = headResponse.Content.Headers.ContentLength 
                                ?? throw new InvalidOperationException("无法获取目标文件长度");
            if (!headResponse.Headers.AcceptRanges.Contains("bytes"))
            {
                throw new NotSupportedException("目标服务器不支持分块下载");
            }
            
            var partSize = (long)Math.Floor(responseLength / (parts + 0.0));
            var pieces = new List<FileChunk>();
            var uri = new Uri(url);
            
            WriteLine($"{responseLength.ToString(CultureInfo.InvariantCulture)} TOTAL SIZE");
            WriteLine($"{partSize.ToString(CultureInfo.InvariantCulture)} PART SIZE\n");
            
            string filename = outFile ?? Path.GetFileName(uri.LocalPath);

            // 用using管理内存映射文件生命周期
            using var mmf = MemoryMappedFile.CreateFromFile(filename, FileMode.OpenOrCreate, null, responseLength);
            
            // 生成分块列表
            for (long i = 0; i < responseLength; i += partSize)
            {
                pieces.Add(i + partSize < responseLength
                    ? new FileChunk(i, i + partSize)
                    : new FileChunk(i, responseLength));
            }

            // 显式定义并行配置
            var parallelOptions = new ParallelOptions
            {
                MaxDegreeOfParallelism = parts,
                CancellationToken = CancellationToken.None
            };
            
            await Parallel.ForEachAsync(pieces, parallelOptions, async (piece, cancellationToken) =>
            {
                using var request = new HttpRequestMessage { RequestUri = new Uri(url) };
                // Range头用闭区间,end要减1
                request.Headers.Range = new RangeHeaderValue(piece.Start, piece.End - 1);
                
                using var message = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
                // 校验是否是分块响应
                if (message.IsSuccessStatusCode && message.StatusCode == System.Net.HttpStatusCode.PartialContent)
                {
                    await using var streamToRead = await message.Content.ReadAsStreamAsync(cancellationToken);
                    // 视图长度和分块大小一致,不需要减1
                    await using var streams = mmf.CreateViewStream(piece.Start, piece.End - piece.Start);
                    await streamToRead.CopyToAsync(streams, cancellationToken);
                }
                else
                {
                    throw new InvalidOperationException($"分块{piece.Start}-{piece.End}下载失败,状态码:{message.StatusCode}");
                }
            });
        }
        
        public static async Task Main(string[] args)
        {
            var url = "https://wallpaperaccess.com/full/2159447.jpg";
            await DownloadFile(url, 8);
        }
    }
}

内容的提问来源于stack exchange,提问作者0xSingularity

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 14:45:00