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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 12:12:10