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

C#大文件下载哈希值损坏问题排查及代码求助

大文件分块下载哈希损坏问题的排查与解决

问题描述

下载200MB以下文件正常,但大文件下载后哈希值损坏。尝试分块下载仍未解决,已确认服务器文件完好,调整下载速度也无效。

现有实现代码

void getdownload(string url, string fileName, string destinationPath, long totalSize)
{
    List<Tuple<string, string, long>> parts = new List<Tuple<string, string, long>>();
    long partSize = 100 * 1024 * 1024; // 100MB
    int partCount = (int)(totalSize / partSize);
    if (totalSize % partSize > 0)
        partCount++;

    for (int i = 0; i < partCount; i++)
    {
        long start = i * partSize;
        long end = (i + 1) * partSize - 1;
        if (end > totalSize)
            end = totalSize;

        string partFileName = string.Format("{0}.part{1}", fileName, i);
        string partFilePath = Path.Combine(destinationPath, partFileName);
        string urlx = string.Format("{0}?start={1}&end={2}", url, start, end);
        parts.Add(new Tuple<string, string, long>(urlx, partFilePath, end));
    }

    using (WebClient client = new WebClient())
    {
        int currentPart = 0;
        client.DownloadProgressChanged += (s, e) =>
        {
            var totalSize = e.TotalBytesToReceive;
            DownloadChanged(e, parts[0].Item3);
        };
        client.DownloadFileCompleted += (s, e) =>
        {
            if (e.Error != null)
            {
                Console.WriteLine("Error while downloading part {0}: {1}", currentPart, e.Error.Message);
            }
            else if (e.Cancelled)
            {
                Console.WriteLine("Download of part {0} was cancelled", currentPart);
            }
            else
            {
                Console.WriteLine("Part {0} was downloaded successfully", currentPart);
                MessageBox.Show(currentPart.ToString());
                currentPart++;
                if (currentPart < parts.Count)
                {
                    MessageBox.Show(currentPart.ToString());
                    client.DownloadFileAsync(new Uri(parts[currentPart].Item1), parts[0].Item2);
                    client.DownloadFileCompleted += DownloadFileCompleted;
                }
            }
        };
        client.DownloadFileAsync(new Uri(parts[0].Item1), parts[0].Item2);
        client.DownloadFileCompleted += DownloadFileCompleted;
        MessageBox.Show(parts[0].Item1.ToString());
    }
}

代码中的关键错误

  • 分块文件被重复覆盖:下载后续分块时,始终使用parts[0].Item2作为保存路径,所有分块都写入同一个文件,后面的内容直接覆盖前面的,最终文件仅保留最后一个分块的数据,哈希必然错误。
  • 重复注册事件:每次下载新分块都执行client.DownloadFileCompleted += DownloadFileCompleted;,导致事件被多次绑定,回调会重复触发,引发逻辑混乱。
  • Range请求范围错误:HTTP的Range头中,end是包含在内的字节索引(从0开始),最后一个分块的end应该是totalSize-1,而不是totalSize,否则会请求超出文件范围的字节。
  • 缺少分块合并逻辑:下载完成后没有将多个分块文件合并成完整文件的步骤,直接使用分块文件的话自然不完整。

修正后的代码

void getdownload(string url, string fileName, string destinationPath, long totalSize)
{
    List<Tuple<string, string, long>> parts = new List<Tuple<string, string, long>>();
    long partSize = 100 * 1024 * 1024; // 100MB
    int partCount = (int)(totalSize / partSize);
    if (totalSize % partSize > 0)
        partCount++;

    // 生成分块信息,改用Range头而非URL参数
    for (int i = 0; i < partCount; i++)
    {
        long start = i * partSize;
        // 最后一个分块的end不需要计算,直接用start-即可,服务器会返回剩余所有字节
        string partFileName = $"{fileName}.part{i}";
        string partFilePath = Path.Combine(destinationPath, partFileName);
        parts.Add(new Tuple<string, string, long>(url, partFilePath, start));
    }

    using (WebClient client = new WebClient())
    {
        int currentPart = 0;

        // 定义回调方法,避免重复注册
        AsyncCompletedEventHandler downloadCallback = null;
        downloadCallback = (s, e) =>
        {
            if (e.Error != null)
            {
                Console.WriteLine("分块{0}下载失败:{1}", currentPart, e.Error.Message);
                client.DownloadFileCompleted -= downloadCallback;
                return;
            }
            if (e.Cancelled)
            {
                Console.WriteLine("分块{0}下载被取消", currentPart);
                client.DownloadFileCompleted -= downloadCallback;
                return;
            }

            Console.WriteLine("分块{0}下载完成", currentPart);
            currentPart++;

            if (currentPart < parts.Count)
            {
                // 设置Range头,请求当前分块的字节范围
                var currentPartInfo = parts[currentPart];
                client.Headers.Remove("Range");
                client.Headers.Add("Range", $"bytes={currentPartInfo.Item3}-");
                // 下载到当前分块的对应文件,不再覆盖
                client.DownloadFileAsync(new Uri(currentPartInfo.Item1), currentPartInfo.Item2);
            }
            else
            {
                // 所有分块下载完成,合并成完整文件
                MergeDownloadedParts(destinationPath, fileName, partCount);
                client.DownloadFileCompleted -= downloadCallback;
                Console.WriteLine("所有分块合并完成,文件下载成功");
            }
        };

        // 仅注册一次事件
        client.DownloadFileCompleted += downloadCallback;

        // 启动第一个分块的下载
        var firstPart = parts[0];
        client.Headers.Add("Range", $"bytes={firstPart.Item3}-");
        client.DownloadFileAsync(new Uri(firstPart.Item1), firstPart.Item2);
    }
}

// 合并分块文件的方法
void MergeDownloadedParts(string destinationPath, string fileName, int partCount)
{
    string finalFilePath = Path.Combine(destinationPath, fileName);
    using (FileStream finalStream = new FileStream(finalFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
    {
        for (int i = 0; i < partCount; i++)
        {
            string partPath = Path.Combine(destinationPath, $"{fileName}.part{i}");
            using (FileStream partStream = new FileStream(partPath, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                // 将分块内容复制到最终文件
                partStream.CopyTo(finalStream);
            }
            // 可选:删除临时分块文件
            File.Delete(partPath);
        }
    }
}

额外优化建议

  • 验证服务器Range支持:先发送HEAD请求,检查响应头是否包含Accept-Ranges: bytes,如果没有,服务器不支持分块下载,需改用普通下载。
  • 添加哈希校验:下载完成后计算完整文件的哈希值,与服务器提供的哈希对比,确保文件完整性。
  • 改用HttpClient:WebClient属于旧API,推荐使用HttpClient,它的异步模型更清晰,支持超时、取消令牌等更完善的功能。
  • 分块容错机制:如果某个分块下载失败,可单独重试该分块,无需重新下载整个文件。

内容的提问来源于stack exchange,提问作者Cehennem Aslanı

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 17:25:19