如何通过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
相关产品推荐
相关产品推荐

