ASMX WebService分块传输字节数组致ZIP损坏问题排查
ASMX WebService大文件分块传输ZIP损坏问题排查与修复方案
问题背景
当前维护的跨服务器ZIP文件传输遗留ASMX WebService,前期小文件传输运行稳定,待传输文件体积增长至700MB以上后,控制台客户端一次性接收WebService返回的大字节数组时触发异常。改造为分块收发字节数组的逻辑后,客户端拼接生成的ZIP文件出现损坏、无法解压的问题。
以下为各版本原始代码:
控制台应用(原始版本)
string saveto = MdlMain.FileName; object file = (object)serviceSoapClient.GetFile( saveto); FileStream fileStream = new FileStream(GlobalVariable.FileDirectory + str2, FileMode.Create); fileStream.Write((byte[])file, 0, Conversions.ToInteger(NewLateBinding.LateGet(file, (Type)null, "Length", new object[0], (string[])null, (Type[])null, (bool[])null))); fileStream.Flush(); fileStream.Close();
WebService(原始版本)
public byte[] GetFile( string strFilename) { FileStream fileStream = new FileStream(this.DJDownloadPath + strFilename, FileMode.Open, FileAccess.Read); long length = fileStream.Length; byte[] array = new byte[checked((int)(length + 1L - 1L) + 1)]; fileStream.Read(array, 0, checked((int)length)); fileStream.Close(); HttpContext.Current.Response.BufferOutput = false; HttpContext.Current.Response.Buffer = false; return array; }
控制台应用(改造版本)
FileStream fileStream= new FileStream(@"D:\example.zip", FileMode.Create); int totalchunkNo = targetfilesize / 2000000; int remainder = targetfilesize % 2000000; if (remainder > 0 && remainder != totalchunkNo) totalchunkNo++; for (int i = 1; i <= totalchunkNo; i++) { byte[] bytetobeWritten = (byte[])serviceSoapClient.GetFileByChunk(MdlMain.FileName, i); fileStream.Write(bytetobeWritten, 0, Conversions.ToInteger(NewLateBinding.LateGet(bytetobeWritten, (Type)null, "Length", new object[0], (string[])null, (Type[])null, (bool[])null))); } fileStream.Flush(); fileStream.Close();
WebService(改造版本)
[WebMethod] public byte[] GetFileByChunk(string strFilename, int requestChunkNo) { FileStream fileStream = new FileStream(this.DJDownloadPath + strFilename, FileMode.Open, FileAccess.Read); long length = fileStream.Length; byte[] array = new byte[length]; int incomingOffset = 0; int chunkSize = 2000000; fileStream.Read(array, 0, (int)length); fileStream.Close(); int currentchunkNo = 0; byte[] outboundBuffer = new byte[chunkSize]; while (incomingOffset < array.Length) { int lengthh = Math.Min(outboundBuffer.Length, array.Length - incomingOffset); Buffer.BlockCopy(array, incomingOffset, outboundBuffer, 0, lengthh); incomingOffset += lengthh; currentchunkNo++; if (currentchunkNo == requestChunkNo) { return outboundBuffer; } } return null; }
现有代码核心问题
- ZIP损坏直接原因:改造后的WebService分块接口固定返回长度为2000000字节的数组,处理最后一个分块时,仅将剩余有效字节复制到数组头部,数组尾部未被覆盖的位置全是默认0值;客户端写入时未判断当前块的有效长度,将多余0字节全部写入最终文件,直接破坏ZIP文件结构导致无法解压。
- 内存开销无优化反而升高:分块接口每次收到请求都会先把整个目标文件全量读入内存,再循环遍历匹配请求的块号,完全没有解决大文件内存占用过高的问题,700MB文件每次分块请求都要占用700MB以上内存,高并发下极易触发服务端内存溢出。
- 总块数计算逻辑错误:客户端计算总块数时加了无意义的
remainder != totalchunkNo判断——remainder是最后一块的剩余字节数,totalchunkNo是块计数,二者单位不同没有可比性,极端场景下会出现少算块导致文件缺失、多算块写入空内容的问题。 - 流读取逻辑存在固有隐患:所有版本的读取逻辑都只调用一次
Stream.Read方法就默认读取完成,实际上该方法返回的实际读取字节数可能小于传入的请求长度,大文件、磁盘IO波动场景下很容易出现文件读不全的问题。 - 原始版本数组长度计算错误:原始版本创建返回字节数组时,冗余计算多申请了1字节空间,返回的数组比实际文件多1个空字节,小文件场景下解压软件的容错机制可以忽略该问题,大文件下会直接触发ZIP结构校验失败。
修复后的正确实现
WebService端修复代码
[WebMethod] public long GetFileLength(string strFilename) { string filePath = Path.Combine(this.DJDownloadPath, strFilename); return new FileInfo(filePath).Length; } [WebMethod] public byte[] GetFileByChunk(string strFilename, int requestChunkNo, int chunkSize = 2000000) { string filePath = Path.Combine(this.DJDownloadPath, strFilename); using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { long fileLength = fileStream.Length; long offset = (requestChunkNo - 1) * (long)chunkSize; // 偏移超出文件长度返回空数组 if (offset >= fileLength) { return new byte[0]; } // 计算当前块实际有效长度,最后一块取剩余文件长度 int actualReadLength = (int)Math.Min(chunkSize, fileLength - offset); byte[] result = new byte[actualReadLength]; // 直接移动流指针到目标偏移位置,不需要全量读文件 fileStream.Seek(offset, SeekOrigin.Begin); // 循环读取确保读满有效长度,避免单次Read返回不完整 int totalRead = 0; while (totalRead < actualReadLength) { int read = fileStream.Read(result, totalRead, actualReadLength - totalRead); if (read == 0) break; totalRead += read; } return result; } }
客户端修复代码
const int ChunkSize = 2000000; string savePath = @"D:\example.zip"; // 先从服务端获取准确的文件总长度 long targetFileSize = serviceSoapClient.GetFileLength(MdlMain.FileName); // 简化总块数计算逻辑 long totalChunkNo = (long)Math.Ceiling(targetFileSize * 1.0 / ChunkSize); using (FileStream fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write)) { for (int i = 1; i <= totalChunkNo; i++) { byte[] chunkBytes = serviceSoapClient.GetFileByChunk(MdlMain.FileName, i, ChunkSize); // 按返回数组的实际长度写入,不会写入多余空字节 fileStream.Write(chunkBytes, 0, chunkBytes.Length); } fileStream.Flush(); }
额外优化建议
- 框架配置调整:ASMX WebService默认请求/响应大小限制仅为4MB左右,大文件传输场景需要在服务端web.config、客户端app.config中调整
maxRequestLength、maxReceivedMessageSize、executionTimeout等配置参数,设置为大于预期最大文件体积的值,避免框架层面拦截请求。 - 传输一致性校验:可以在分块接口中额外返回当前块的MD5哈希值,客户端收到块后先做哈希校验,校验不通过则重传当前块,避免网络传输过程中字节丢失、篡改导致的文件损坏。
- 长期迭代方案:如果后续文件体积持续增长,建议逐步将遗留ASMX服务升级为ASP.NET WebAPI或gRPC服务,采用流式响应代替整字节数组返回,大幅降低内存占用,提升传输稳定性。
内容的提问来源于stack exchange,提问作者Syafi Mohamad
相关产品推荐
相关产品推荐

