Web API 服务在读取流时挂起

Posted

技术标签:

【中文标题】Web API 服务在读取流时挂起【英文标题】:Web API service hangs on reading the stream 【发布时间】:2018-10-26 16:27:17 【问题描述】:

说明:我正在修改支持可恢复文件上传的 ASP.NET Core Web API 服务(托管在 Windows 服务中)。这可以正常工作,并在许多失败情况下恢复文件上传,除了下面描述的一种情况。

问题:当服务在另一台计算机上并且客户端在我的计算机上并且我拔下计算机上的电缆时,客户端检测到网络缺失,而服务在 fileSection.FileStream 上挂起。读()。有时服务会在 8 分钟内检测到故障,有时会在 20 分钟内检测到故障,有时永远不会。

我还注意到,在拔掉电缆并停止客户端后,服务卡在 Read() 函数并且文件大小为 x KB,但是当服务最终检测到异常时,它会额外写入 4 KB到文件。这很奇怪,因为我关闭了缓冲并且缓冲区大小为 2 KB。

问题:如何正确检测服务上没有网络,或者正确超时,或者取消请求

服务代码

public static async Task<List<(Guid, string)>> StreamFileAsync(
   this HttpRequest request, DeviceId deviceId, FileTransferInfo transferInfo)
    
        var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType), DefaultFormOptions.MultipartBoundaryLengthLimit);
        var reader = new MultipartReader(boundary, request.Body);
        var section = await reader.ReadNextSectionAsync(_cancellationToken);

        if (section != null)
        
            var fileSection = section.AsFileSection();
            var targetPath = transferInfo.FileTempPath;

            try
            
                using (var outfile = new FileStream(transferInfo.FileTempPath, FileMode.Append, FileAccess.Write, FileShare.None))
                
                    var buffer = new byte[DefaultCopyBufferSize];
                    int read;

                    while ((read = fileSection.FileStream.Read(buffer, 0, buffer.Length)) > 0) // HANGS HERE
                    
                        outfile.Write(buffer, 0, read);
                        transferInfo.BytesSaved = read + transferInfo.BytesSaved;
                    
                
            
            catch (Exception e)
            
                ...
            
        
    

客户端代码

var request = CreateRequest(fileTransferId, boundary, header, footer, filePath, offset, headers, null);

using (Stream formDataStream = request.GetRequestStream())
    
            formDataStream.ReadTimeout = 60000;

            formDataStream.Write(Encoding.UTF8.GetBytes(header), 0, header.Length);
            byte[] buffer = new byte[2048];

            using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
            
                fs.Seek(offset, SeekOrigin.Begin);

                for (int i = 0; i < fs.Length - offset;)
                
                    int k = await fs.ReadAsync(buffer, 0, buffer.Length);
                    if (k > 0)
                    
                        await Task.Delay(100);
                        await formDataStream.WriteAsync(buffer, 0, k);
                    

                    i = i + k;
                
            

            formDataStream.Write(footer, 0, footer.Length);
        

        var uploadingResult = request.GetResponse() as HttpWebResponse;



private static HttpWebRequest CreateRequest(
        Guid fileTransferId,
        string boundary,
        string header,
        byte[] footer,
        string filePath,
        long offset,
        NameValueCollection headers,
        Dictionary<string, string> postParameters)
    
        var url = $"_BaseAddressv1/ResumableUpload?fileTransferId=fileTransferId";
        HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
        request.Method = "POST";
        request.ContentType = "multipart/form-data; boundary=\"" + boundary + "\"";
        request.UserAgent = "Agent 1.0";
        request.Headers.Add(headers); // custom headers
        request.Timeout = 120000;
        request.KeepAlive = true;
        request.AllowReadStreamBuffering = false;
        request.ReadWriteTimeout = 120000;
        request.AllowWriteStreamBuffering = false;
        request.ContentLength = CalculateContentLength(filePath, offset, header, footer, postParameters, boundary);
        return request;
    

我尝试了什么

    我将这些添加到配置文件中:

    试图在服务器上设置超时

    var host = new WebHostBuilder().UseKestrel(o => o.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);)

    使用异步和非异步 Read()

    尝试过保持活动和不使用

    尝试在网络恢复时中止请求:request?.Abort();

    尝试设置 formDataStream.ReadTimeout = 60000;

【问题讨论】:

因为它没有为服务器说明;设置 outfile.ReadTimeout 不是一个选项吗? 这会产生异常:“System.InvalidOperationException:此流不支持超时。在 System.IO.Stream.set_ReadTimeout(Int32 value) at ...” 【参考方案1】:

由于我没有找到更好的方法,我决定在读取流中添加超时并将其保存到文件中。很好的例子贴在这里:https://blogs.msdn.microsoft.com/pfxteam/2012/10/05/how-do-i-cancel-non-cancelable-async-operations/

public static async Task<List<(Guid, string)>> StreamFileAsync(this HttpRequest request, DeviceId deviceId, FileTransferInfo transferInfo)

    var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType), DefaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, request.Body);
    var section = await reader.ReadNextSectionAsync(_cancellationToken);

    if (section != null)
    
        var fileSection = section.AsFileSection();
        var targetPath = transferInfo.FileTempPath;

        try
        
            await SaveMyFile(...);
        
        catch (OperationCanceledException)...
        catch (Exception)...
    


private static async Task SaveMyFile(...)

        var cts = CancellationTokenSource.CreateLinkedTokenSource(myOtherCancellationToken);
        cts.CancelAfter(streamReadTimeoutInMs);
        var myReadTask = StreamFile(transferInfo, fileSection, cts.Token);
        await ExecuteMyTaskWithCancellation(myReadTask, cts.Token);



private static async Task<T> ExecuteMyTaskWithCancellation<T>(Task<T> task, CancellationToken cancellationToken)

        var tcs = new TaskCompletionSource<bool>();

        using (cancellationToken.Register(s => ((TaskCompletionSource<bool>) s).TrySetResult(true), tcs))
        
            if (task != await Task.WhenAny(task, tcs.Task))
            
                throw new OperationCanceledException(cancellationToken);
            
        

        return await task;


private static async Task<bool> StreamFile(...)

        using (var outfile = new FileStream(transferInfo.FileTempPath, FileMode.Append, FileAccess.Write, FileShare.None))
        
            var buffer = new byte[DefaultCopyBufferSize];
            int read;

            while ((read = await fileSection.FileStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
            
                if (token.IsCancellationRequested)
                
                    break;
                

                await outfile.WriteAsync(buffer, 0, read);
                transferInfo.BytesSaved = read + transferInfo.BytesSaved;
            

            return true;
        

【讨论】:

以上是关于Web API 服务在读取流时挂起的主要内容,如果未能解决你的问题,请参考以下文章

托管在 Windows 服务中的 WCF 服务在停止时挂起

Apache/PHP 进程在与 MySQL 交互时挂起

SendMessage() WINAPI 在用于连接到 DDE 服务器时挂起

在读取 hbase 表时挂起 Mapreduce 作业

Spark 2.1 在读取大量数据集时挂起

[尝试在事件函数中分配值时挂起