【问题标题】:Unable to get response using REST WebApi PutAsync when performing a chunked file upload执行分块文件上传时无法使用 REST WebApi PutAsync 获得响应
【发布时间】:2012-03-23 05:13:27
【问题描述】:

我正在编写我的第一个 REST Web 服务(使用 MVC 4 Beta 和时髦的新 Web Api),但在使用 HttpClient.PutAsync 方法时遇到了问题

概述是我正在尝试编写文件上传过程,在其中我将文件分块并使用 MVC 4 WebApi 的 HttpClient.PutAsync 方法。如果我只是遍历我的块并调用 PutAsync,那么一切都很好。但是,有时服务器无法写入特定的块,我需要将该失败报告回客户端,以便可以重试块上传。我还需要知道上传何时完成,以便上传步骤中的下一个阶段可以执行(例如:一旦文件上传,则需要进行数据库写入,加上一些额外的处理需要在服务器上启动以创建预览图像等)。

问题是,当我尝试从 PutAsync 方法访问响应时,一切都陷入了尖叫堆 - 第一个块由客户端发送,但从未被服务器接收,并且整个上传过程研磨到停下来。

这里是处理 PutAsset 方法的服务器代码(为了清楚起见,我删除了所有不相关的代码)。它似乎工作正常 - 文件可以愉快地上传和重新组装:

<HttpPut()>
Public Function PutArtwork(assetID As String, data As AssetBlock) As HttpResponseMessage(Of AssetBlockResponse)

    'Get the info from the tracker file
    Dim lContents As String = IO.File.ReadAllText(IO.Path.Combine(My.Settings.ServiceTempDirectory, "Asset/", (data.Tracker & ".tracker")))
    Dim lAssetInfo As AssetUpload = JsonConvert.DeserializeObject(Of AssetUpload)(lContents)

    If WriteChunkToFile(assetID, lAssetInfo, data) Then
        Dim lTracker As AssetBlockResponse = New AssetBlockResponse
        lTracker.TrackerGID = data.Tracker
        Return New HttpResponseMessage(Of AssetBlockResponse)(lTracker, HttpStatusCode.Accepted)
    Else
        tracker.TrackerGID = Guid.NewGuid.ToString
        tracker.ErrorCode = ApiErrorCode.FileChunkWriteFailed
        tracker.Message = String.Format("Write error for chunk {0-{1}", data.Offset, data.Length - 1)
        Dim lResponse As HttpResponseMessage(Of AssetBlockResponse) = New HttpResponseMessage(Of AssetBlockResponse)(tracker, HttpStatusCode.InternalServerError)
        lResponse.ReasonPhrase = String.Format("Write error for chunk {0-{1}", data.Offset, data.Length - 1)
        Return lResponse
    End If

End Function

Private Function WriteChunkToFile(assetID As String, info As AssetUpload, data As AssetBlock) As Boolean
    Dim lFile As String = GetAssetPath(assetID, String.Empty, data.Tracker & ".upload", 1, False, data.Length)
    If IO.File.Exists(lFile) Then
        Dim lBytes As Byte() = System.Convert.FromBase64String(data.FileData)
        Try
          WriteChunk(lFile, lBytes, data.Offset, data.Length)
          Return True
        Catch ex as Exception
          Return False
       End Try
End Function

Private Sub WriteChunk(filePath As String, data() As Byte, offset As Integer, length As Integer)
    Using lStream = IO.File.Open(filePath, FileMode.Open, FileAccess.Write, FileShare.Write)
        lStream.Position = offset
        lStream.Write(data, 0, length)
        lStream.Close()
    End Using
End Sub

以及调用 REST 服务的客户端代码:

Dim lClient As HttpClient = New HttpClient
Dim lBlock As AssetBlock = New AssetBlock

lBlock.Tracker = tracker.TrackerGID

lClient.BaseAddress = CreateUploadAddressUri(_BaseAddress, assetInfo)

Dim lLocalChunk As UploadChunkInfo
For Each chunkIterator As UploadChunkInfo In chunks
    lLocalChunk = chunkIterator
    lBlock.BlockHash = AssetMethods.SHA1HashBytes(lLocalChunk.Chunk)
    lBlock.Offset = lLocalChunk.Offset
    lBlock.Length = lLocalChunk.Chunk.Length
    lBlock.FileData = Convert.ToBase64String(lLocalChunk.Chunk)
    Dim lTask = lClient.PutAsync(lClient.BaseAddress, New StringContent(JsonConvert.SerializeObject(lBlock), Text.Encoding.UTF8, "application/json"))
Next

上述版本运行良好,但它是一种“一劳永逸”的方法。我想要的是从 PutAsync 获取响应,检查以确保一切正常,然后继续下一次尝试,或者根据需要重试块。所以上面的 .PutAsync 代码真的需要看起来像:

Dim lTask = lClient.PutAsync(lClient.BaseAddress, New StringContent(JsonConvert.SerializeObject(lBlock), Text.Encoding.UTF8, "application/json"))
If lTask.Result.IsSuccessStatusCode Then '!! This line is never hit !!
    lTask.Result.Content.ReadAsStringAsync().ContinueWith(Sub(readtask)
                                                            lLocalChunk.Status = UploadChunkInfo.ChunkUploadStatus.Uploaded
                                                        End Sub, TaskScheduler.FromCurrentSynchronizationContext())
Else
    lLocalChunk.Status = UploadChunkInfo.ChunkUploadStatus.Failed
    lLocalChunk.RetryCount += 1
End If

如果块被标记为失败,那么我们将作为循环的一部分重试。一旦我们重试了 X 次块,我们将整个上传过程视为失败并将其报告给用户。但是执行带有结果检查的 PutAsync 只会停止循环。它甚至从未命中服务器的 Put 方法。 Fiddler 显示没有到服务器的流量,

我现在完全没有想法 - 有什么想法吗?

【问题讨论】:

  • 一些附加信息:如果文件足够小(小于块大小),那么 PutAsync 方法似乎工作正常,正确返回响应。
  • 哇。你真的不需要自己做分块。 HttpClient 会为你做到这一点。
  • 它是异步的。您不能立即要求 .Result。将检查放在 .ContinueWith 中,将所有任务放在一个数组中,调用 WaitAll 然后返回并重新发布任何失败的任务。起泡、冲洗、重复。
  • .ContinueWith 是我的问题 - 在处理任务时将其添加到混合中,使一切工作变得愉快(以及排队的地方,因此上传很容易被拒绝和回滚)。谢谢@DanF

标签: .net rest httpclient asp.net-web-api


【解决方案1】:

这是我测试文件上传到 Web API 的一些代码

   [Fact]
        public void UploadAFile() {

            var httpClient = new HttpClient();
            httpClient.BaseAddress = new Uri(_HostUrl);
            httpClient.DefaultRequestHeaders.TransferEncodingChunked = true;
            var fileContent = new StreamContent(typeof(SimpleApiController).Assembly.GetManifestResourceStream(typeof(SimpleApiController),"bigfile.pdf"));
            fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

            var responseMessage = httpClient.PostAsync("SimpleApi/SendBigFile", fileContent).Result;


            Assert.Equal(HttpStatusCode.OK, responseMessage.StatusCode);
            Assert.Equal("102061", responseMessage.Content.ReadAsStringAsync().Result);
        }

如果您是自托管,则需要在服务器上设置这样的配置,

var config = new HttpSelfHostConfiguration(_HostUrl) {
                      TransferMode = System.ServiceModel.TransferMode.Streamed,
                      // Bypass 64K buffer in request body handler
                      MaxReceivedMessageSize = 1024 * 500,
                      MaxBufferSize = 1024 * 500
                  }; // Increase DOS protection limit

服务器端处理程序看起来像

 public HttpResponseMessage SendBigFile() {

            var stream = Request.Content.ReadAsStreamAsync().Result;
            var memoryStream = new MemoryStream();
            stream.CopyTo(memoryStream);
            var response = new HttpResponseMessage(HttpStatusCode.OK); ;
            response.Content = new StringContent(memoryStream.Length.ToString());
            return response;

        }

【讨论】:

  • 在消费端(在 MVC4 webapi 内部)是什么样的?您是在控制器中的一个大块中获取文件,还是在单个块中获取文件?上面的代码看起来很整洁。
  • 我真的很喜欢这种技术的外观,但尽我所能,它无法让它发挥作用。它给人的印象是工作,但最终似乎总是在流媒体上窒息。我已经把它归档了,等我有时间再仔细看看(在那之前,我原来的问题已经像上面那样解决了)
猜你喜欢
  • 2018-02-20
  • 2014-06-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-05
  • 1970-01-01
  • 2017-01-11
相关资源
最近更新 更多