【发布时间】: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