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

如何通过HttpClient实现大文件(约200MB)的流式HTTP上传?

用HttpClient实现大文件流式上传(避免内存加载)

嘿,我来帮你解决这个HttpClient流式上传大文件的问题!你之前的代码误区在于搞反了流的方向——你试图往响应流里写内容,但实际上我们需要构建的是请求的内容流,把文件数据流式发送给服务器。下面给你两种可行的方案,都是避免把整个200MB文件加载到内存的:

方案一:用MultipartFormDataContent(推荐,简洁省心)

这个方式利用HttpClient内置的MultipartFormDataContent来自动处理multipart表单的边界和头信息,配合StreamContent包装文件流,实现真正的流式上传:

using System.Net.Http;
using System.Net.Http.Headers;
using System.IO;

// 初始化HttpClient,设置Connection: close(和你之前的KeepAlive=false对应)
using (var httpClient = new HttpClient())
{
    httpClient.DefaultRequestHeaders.ConnectionClose = true;

    // 创建multipart表单内容容器
    using (var multipartContent = new MultipartFormDataContent())
    {
        // 打开要上传的大文件流(注意不要用File.ReadAllBytes,那会加载到内存)
        using (var fileStream = new FileStream("path/to/your/large/file", FileMode.Open, FileAccess.Read))
        {
            // 用StreamContent包装文件流,这是实现流式上传的核心
            var fileContent = new StreamContent(fileStream);
            
            // 设置文件对应的Content-Disposition头(指定表单字段名和文件名)
            fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
            {
                Name = "\"file\"", // 注意引号,符合multipart规范
                FileName = "\"large_file.bin\""
            };
            // 设置文件的Content-Type,根据实际文件类型调整,比如image/jpeg等
            fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

            // 将文件内容添加到multipart容器
            multipartContent.Add(fileContent);

            // 如果需要添加其他文件或文本字段,用同样的方式添加
            // 示例:添加一个文本描述字段
            var textContent = new StringContent("这是一个200MB的大文件");
            textContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
            {
                Name = "\"description\""
            };
            multipartContent.Add(textContent);

            // 发送请求,用HttpCompletionOption.ResponseHeadersRead避免等待整个响应下载完
            using (var response = await httpClient.PostAsync(
                "your-server-upload-url", 
                multipartContent, 
                HttpCompletionOption.ResponseHeadersRead))
            {
                // 确保请求成功
                response.EnsureSuccessStatusCode();
                
                // 处理服务器响应,这里也可以流式读取响应内容
                var responseText = await response.Content.ReadAsStringAsync();
                // 或者用流读取:using (var responseStream = await response.Content.ReadAsStreamAsync()) { ... }
            }
        }
    }
}

关键说明:

  • StreamContent会在发送请求时逐步从FileStream读取数据,不会把整个文件加载到内存,完美符合你的需求。
  • MultipartFormDataContent会自动生成和管理boundary,不需要你手动拼接,避免格式错误。
  • HttpCompletionOption.ResponseHeadersRead让HttpClient在收到响应头后就返回,不用等整个响应体下载完成,优化性能。

方案二:手动构建流式Multipart内容(精细控制场景)

如果你需要像原来HttpWebRequest那样手动控制boundary和头的拼接,可以自定义一个流来逐步输出multipart内容,同样实现流式上传:

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;

using (var httpClient = new HttpClient())
{
    httpClient.DefaultRequestHeaders.ConnectionClose = true;
    var boundaryString = $"----CustomBoundary{DateTime.Now.Ticks.ToString("x")}";

    // 用自定义的MultipartRequestStream来构建请求内容
    var requestContent = new StreamContent(new MultipartRequestStream(boundaryString, "path/to/your/large/file"));
    requestContent.Headers.ContentType = MediaTypeHeaderValue.Parse($"multipart/form-data; boundary={boundaryString}");

    // 发送请求
    using (var response = await httpClient.PostAsync(
        "your-server-upload-url", 
        requestContent, 
        HttpCompletionOption.ResponseHeadersRead))
    {
        response.EnsureSuccessStatusCode();
        // 处理响应
    }
}

// 自定义流,负责逐步输出multipart的头、文件内容和结尾
public class MultipartRequestStream : Stream
{
    private readonly string _boundary;
    private readonly string _filePath;
    private Stream _currentStream;
    private bool _headersWritten;
    private bool _fileSent;
    private bool _disposed;

    public MultipartRequestStream(string boundary, string filePath)
    {
        _boundary = boundary;
        _filePath = filePath;
        _currentStream = new MemoryStream(); // 用来临时存少量的头信息,不会占用大量内存
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        if (!_headersWritten)
        {
            // 先写入multipart的文件头
            var header = $"--{_boundary}\r\n" +
                         $"Content-Disposition: form-data; name=\"file\"; filename=\"{Path.GetFileName(_filePath)}\"\r\n" +
                         "Content-Type: application/octet-stream\r\n\r\n";
            var headerBytes = Encoding.UTF8.GetBytes(header);
            _currentStream.Write(headerBytes, 0, headerBytes.Length);
            _currentStream.Position = 0;
            _headersWritten = true;
        }

        int bytesRead = _currentStream.Read(buffer, offset, count);
        if (bytesRead == 0 && !_fileSent)
        {
            // 头写完了,现在开始读取文件流
            using (var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read))
            {
                bytesRead = fileStream.Read(buffer, offset, count);
                if (bytesRead == 0)
                {
                    // 文件读完了,写入multipart的结尾边界
                    var footerBytes = Encoding.UTF8.GetBytes($"\r\n--{_boundary}--\r\n");
                    _currentStream = new MemoryStream(footerBytes);
                    _currentStream.Position = 0;
                    _fileSent = true;
                    bytesRead = _currentStream.Read(buffer, offset, count);
                }
            }
        }
        return bytesRead;
    }

    // 以下是Stream抽象类的必填实现,我们只需要Read功能,其他抛出不支持异常
    public override bool CanRead => true;
    public override bool CanSeek => false;
    public override bool CanWrite => false;
    public override long Length => throw new NotSupportedException();
    public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
    public override void Flush() => _currentStream.Flush();
    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();

    protected override void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                _currentStream?.Dispose();
            }
            _disposed = true;
        }
        base.Dispose(disposing);
    }
}

关键说明:

  • 自定义流MultipartRequestStream会在StreamContent读取数据时,先输出multipart头,再逐步读取文件内容,最后输出结尾边界,全程流式处理。
  • 只有少量的头和尾会临时存在内存里,文件内容完全是从磁盘流式读取发送的。

总结

你之前的核心问题是混淆了请求流和响应流的方向——我们需要构建的是请求的内容,而不是操作响应流。用StreamContent包装文件流是实现流式上传的核心,配合MultipartFormDataContent可以省去手动处理multipart格式的麻烦。

内容的提问来源于stack exchange,提问作者Jakub Krampl

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 03:52:15