0

我正在使用HttpClient将文件上传到需要Content-MD5标头的 API。如何HttpClient在发送之前获取完整的请求正文,以便我可以MD5在内容上运行并在请求标头中使用它?请注意,我还需要在多部分表单数据之间包含标题,即Content-Disposition每个部分中的所有其他标题。

我正在使用下面的代码,取自这个答案

private System.IO.Stream Upload(string url, string param1, Stream fileStream, byte [] fileBytes)
{
    HttpContent stringContent = new StringContent(param1);
    HttpContent fileStreamContent = new StreamContent(fileStream);
    HttpContent bytesContent = new ByteArrayContent(fileBytes);
    using (var client = new HttpClient())
    using (var formData = new MultipartFormDataContent())
    {
        formData.Add(stringContent, "param1", "param1");
        formData.Add(fileStreamContent, "file1", "file1");
        formData.Add(bytesContent, "file2", "file2");
        var response = client.PostAsync(url, formData).Result;
        if (!response.IsSuccessStatusCode)
        {
            return null;
        }
        return response.Content.ReadAsStreamAsync().Result;
    }
}
4

3 回答 3

2

你的文件有多大?亚当的答案的问题在于它将文件的全部内容缓冲在内存中。这可能会导致您的程序因大文件而内存不足,或者由于磁盘交换过多而导致性能不佳。

事实上,我发现即使是 MultipartFormDataContent.ReadAsStreamAsync() 也会将整个内容缓冲到内存中(可能通过调用 MultipartFormDataContent.LoadIntoBufferAsync())。使用 StreamContent.ReadAsStreamAsync() 时似乎不存在该问题,因此,如果您遇到内存问题,似乎唯一的解决方案是编写自己的 MultipartFormDataContent 实现,该实现不缓冲整个内容但利用StreamContent.ReadAsStreamAsync()。

请注意,如果接收流没有,我发现 MultipartFormDataContent.CopyToAsync() 不会缓冲内存中的全部内容。编写一个用作管道的 Stream 实现可能值得一试,其中写入的任何字节都会立即被 md5Hash.ComputeHash(Stream) 消耗。

编辑:这些是我在 .NET 4.0 上的经验。我听说 .NET 4.5 的行为在客户端缓冲方面有所不同,所以我不确定 MultipartFormDataContent.ReadAsStreamAsync() 如何对其执行。

于 2014-01-14T20:38:56.383 回答
2

现在,我提前承认我没有测试过这段代码,因为我没有测试站点设置可以点击。但是,我已经测试到在 LINQPad 中发布数据并且代码没有错误并且设置了 MD5 哈希值。以下应该适用于您想要做的事情:

private System.IO.Stream Upload(string url, string param1, Stream fileStream, byte[] fileBytes)
{
    HttpContent stringContent = new StringContent(param1);
    HttpContent fileStreamContent = new StreamContent(fileStream);
    HttpContent bytesContent = new ByteArrayContent(fileBytes);

    using (HttpClient client = new HttpClient())
    {
        using (MultipartFormDataContent formData = new MultipartFormDataContent())
        {
            formData.Add(stringContent, "param1", "param1");
            formData.Add(fileStreamContent, "file1", "file1");
            formData.Add(bytesContent, "file2", "file2");

            using (MD5 md5Hash = MD5.Create())
            {
                formData.Headers.ContentMD5 = md5Hash.ComputeHash(formData.ReadAsByteArrayAsync().Result);
            }

            var response = client.PostAsync(url, formData).Result;
            if (!response.IsSuccessStatusCode)
            {
                return null;
            }

            return response.Content.ReadAsStreamAsync().Result;
        }
    }
}
于 2013-11-04T16:35:36.313 回答
2

使用 HttpClient 处理这种类型的通用方法是使用 HttpMessageHandler ,就像您可能要做的其他工作一样,例如添加授权标头、签署消息等。

我还使用基于 Task 的语法重写了它,因为它更符合 HttpClient 的理念——调用者可以根据需要调用 .Result 。

private await Task<System.IO.Stream> Upload(string url, string param1, Stream fileStream, byte[] fileBytes)
{
    HttpContent stringContent = new StringContent(param1);
    HttpContent fileStreamContent = new StreamContent(fileStream);
    HttpContent bytesContent = new ByteArrayContent(fileBytes);

    var handler = new HttpClientHandler();
    var md5Handler = new RequestContentMd5Handler();
    md5Handler.InnerHandler = handler;

    using (HttpClient client = new HttpClient(md5Handler))
    {
        using (MultipartFormDataContent formData = new MultipartFormDataContent())
        {
            formData.Add(stringContent, "param1", "param1");
            formData.Add(fileStreamContent, "file1", "file1");
            formData.Add(bytesContent, "file2", "file2");

            using (var response = await client.PostAsync(url, formData))
            {
                if (!response.IsSuccessStatusCode)
                {
                    return null;
                }

                return await response.Content.ReadAsStreamAsync();
            }
        }
    }
}

此外,在每个请求上重新创建 HttpClient 通常是不好的做法(请参阅在 WebAPI 客户端中每次调用创建一个新的 HttpClient 的开销是多少?等),但我将其保留在此处以符合问题。

这是使用的处理程序...

/// <summary>
/// Handler to assign the MD5 hash value if content is present
/// </summary>
public class RequestContentMd5Handler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.Content == null)
        {
            return await base.SendAsync(request, cancellationToken);
        }

        await request.Content.AssignMd5Hash();

        var response = await base.SendAsync(request, cancellationToken);

        return response;
    }
}

以及扩展方法...

    /// <summary>
    /// Compute and assign the MD5 hash of the content.
    /// </summary>
    /// <param name="httpContent"></param>
    /// <returns></returns>
    public static async Task AssignMd5Hash(this HttpContent httpContent)
    {
        var hash = await httpContent.ComputeMd5Hash();

        httpContent.Headers.ContentMD5 = hash;
    }

    /// <summary>
    /// Compute the MD5 hash of the content.
    /// </summary>
    /// <param name="httpContent"></param>
    /// <returns></returns>
    public static async Task<byte[]> ComputeMd5Hash(this HttpContent httpContent)
    {
        using (var md5 = MD5.Create())
        {
            var content = await httpContent.ReadAsStreamAsync();
            var hash = md5.ComputeHash(content);
            return hash;
        }
    }

使对各个部分进行单元测试变得容易。

于 2017-07-07T17:34:05.713 回答