使用C# ZipFile创建含音乐的ZIP文件时遇传输流EOF异常
解决ZIP打包时“Received an unexpected EOF or 0 bytes from the transport stream”异常
这个EOF异常我之前处理过好几次,大概率是网络流没读完整就传给Zip库导致的——毕竟网络流是一次性、不可回溯的,而且很容易因为网络波动中断,Zip库需要完整的数据流才能正确打包。给你几个靠谱的解决方案:
核心思路:先缓存完整数据流,再打包
不要直接把HTTP响应的网络流传给Zip库,先把流完整读取到内存或临时文件里,确保数据没有缺失后再加入ZIP包。
1. 缓存到内存流(适合小文件)
用内存流先把音乐文件完整下载下来,再写入ZIP。这里以你用到的Ionic DotNetZip库为例:
// 建议复用HttpClient,避免socket资源耗尽 private static readonly HttpClient _httpClient = new HttpClient(); public async Task CreateMusicZip(List<string> musicUrls, string outputZipPath) { using (var zip = new Ionic.Zip.ZipFile()) { foreach (var url in musicUrls) { try { using (var responseStream = await _httpClient.GetStreamAsync(url)) using (var memoryStream = new MemoryStream()) { // 把网络流完整复制到内存流 await responseStream.CopyToAsync(memoryStream); // 重置内存流位置到开头,不然Zip库读不到内容 memoryStream.Position = 0; // 从URL提取文件名作为ZIP内的文件名 var fileName = Path.GetFileName(new Uri(url).LocalPath); zip.AddEntry(fileName, memoryStream); } } catch (Exception ex) { // 可以在这里记录单个文件下载失败的日志,不影响其他文件 Console.WriteLine($"Failed to download {url}: {ex.Message}"); } } await zip.SaveAsync(outputZipPath); } }
2. 缓存到临时文件(适合大文件)
如果音乐文件体积较大,用内存流会占用过多内存,这时候可以先下载到临时文件,再加入ZIP,最后清理临时文件:
public async Task CreateMusicZipWithTempFiles(List<string> musicUrls, string outputZipPath) { using (var zip = new Ionic.Zip.ZipFile()) { foreach (var url in musicUrls) { string tempFilePath = null; try { tempFilePath = Path.GetTempFileName(); // .NET 6+ 自带DownloadFileAsync,低版本可以自己实现流复制 await _httpClient.DownloadFileAsync(url, tempFilePath); var fileName = Path.GetFileName(new Uri(url).LocalPath); zip.AddFile(tempFilePath, ""); // 第二个参数是ZIP内的目录,空表示根目录 } catch (Exception ex) { Console.WriteLine($"Failed to process {url}: {ex.Message}"); } finally { if (tempFilePath != null && File.Exists(tempFilePath)) { File.Delete(tempFilePath); } } } await zip.SaveAsync(outputZipPath); } }
3. 增加重试机制,应对网络波动
网络请求偶尔会因为波动失败,加上重试能大幅降低异常概率:
private async Task<MemoryStream> DownloadWithRetry(string url, int maxRetries = 3) { for (int retry = 0; retry < maxRetries; retry++) { try { using (var responseStream = await _httpClient.GetStreamAsync(url)) { var memoryStream = new MemoryStream(); await responseStream.CopyToAsync(memoryStream); memoryStream.Position = 0; return memoryStream; } } catch (HttpRequestException) { if (retry == maxRetries - 1) throw; // 最后一次重试失败,抛出异常 await Task.Delay(1000 * (retry + 1)); // 指数退避,间隔1s、2s、3s重试 } } throw new InvalidOperationException("Failed to download after max retries"); }
然后在打包方法里调用这个重试方法即可。
4. 验证下载完整性(可选)
如果HTTP响应返回了Content-Length头,可以验证下载的流长度是否和预期一致,提前发现不完整的下载:
using (var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); // 确保HTTP请求成功(状态码200-299) if (response.Content.Headers.ContentLength.HasValue) { long expectedLength = response.Content.Headers.ContentLength.Value; using (var responseStream = await response.Content.ReadAsStreamAsync()) using (var memoryStream = new MemoryStream()) { await responseStream.CopyToAsync(memoryStream); if (memoryStream.Length != expectedLength) { throw new IOException($"Incomplete download: expected {expectedLength} bytes, got {memoryStream.Length}"); } memoryStream.Position = 0; return memoryStream; } } // 没有Content-Length的情况,直接返回流 var stream = await response.Content.ReadAsStreamAsync(); var memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream); memoryStream.Position = 0; return memoryStream; }
额外注意点
- 不要每次创建新的
HttpClient实例,复用一个实例可以避免socket资源耗尽的问题。 - 如果用的是.NET自带的
System.IO.Compression.ZipArchive,核心逻辑一样:先缓存完整流,再写入ZIP条目。
内容的提问来源于stack exchange,提问作者Ankit Patel
相关产品推荐
相关产品推荐

