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ı
相关产品推荐
相关产品推荐

