C# 文件上传的协议选择 C#在HTTP/1.1, HTTP/2, gRPC中如何选择文件传输方案

5次阅读

http/1.1大文件上传卡在100-continue因expect头未获服务端及时响应,nginx/iis等代理常忽略该机制;应禁用expect100continue、调大各层请求体限制,并采用分片+幂等+断点续传保障健壮性。

C# 文件上传的协议选择 C#在HTTP/1.1, HTTP/2, gRPC中如何选择文件传输方案

HTTP/1.1 上传大文件时为什么卡在 100-continue?

因为默认启用 Expect: 100-continue,服务端没及时响应确认,客户端就挂起等待。尤其在 Nginx、IIS 或某些反向代理后面,这个行为常被忽略或拦截。

实操建议:

  • 客户端侧:设置 request.ServicePoint.Expect100Continue = falseHttpWebRequest)或用 HttpClient 时在 HttpClientHandler 中设 Expect100ContinueTimeout 为 0
  • 服务端侧:ASP.NET Core 默认不处理 100-continue,但 Kestrel 可通过 WebHostBuilder.UseKestrel(o => o.Limits.MaxRequestLineSize) 类似配置间接影响;更关键的是确保中间件(如 UseHttpsRedirection)不干扰初始请求头
  • 上传超 100MB 时,务必同步调大 maxAllowedContentLength(IIS)、client_max_body_size(Nginx)和 ASP.NET Core 的 FormOptions.ValueLengthLimit

HTTP/2 下 HttpClient 上传文件会复用连接,但别指望自动分块重试

HTTP/2 天然支持多路复用和头部压缩,上传多个小文件时延迟明显降低;但单个大文件上传仍是一次性流式发送,断连后不会自动续传 —— 这和协议无关,是 HttpClient 实现决定的。

实操建议:

  • 确认 .NET 版本 ≥ 5.0,且服务端明确支持 HTTP/2(TLS 1.2+、ALPN 协商成功),否则 HttpClient 会静默降级到 HTTP/1.1
  • 不要依赖 HttpClient 自带的“重试逻辑”处理上传中断;需自己实现分片(如按 5MB 切 Stream)+ 断点记录 + 幂等接口(例如带 X-Upload-IDX-Chunk-Index
  • 避免在 using var stream = File.OpenRead(...) 内直接传给 PostAsync —— 若上传中途失败,stream 已被释放,无法重试;应打开后缓存位置,或改用 FileStream 并控制 leaveOpen: true

gRPC 不适合直接传大文件,但可以封装成流式上传契约

gRPC 基于 HTTP/2,天生支持双向流,但它默认对单条消息有 4MB 限制(MaxReceiveMessageSize),且二进制 payload 直接序列化进 Protobuf,没有原生文件元数据(如 filename、content-type)支持。

实操建议:

  • stream UploadFile(stream UploadRequest) returns (UploadResponse) 定义,其中 UploadRequest 包含 bytes chunkint64 offsetString file_id 字段,而非整个文件塞进一个 message
  • 服务端必须显式配置 Kestrel 的 MaxRequestBodySize = NULL(不限制)和 gRPC 的 MaxReceiveMessageSize(比如 100_000_000)
  • 客户端不能用 ChannelCredentials.Insecure 走明文 HTTP/2(多数浏览器和工具不支持),必须配 TLS;本地开发可用自签名证书,但需 GrpcChannel 显式信任

.NET 6+ 的 IFormFile 在 HTTP/1.1 和 HTTP/2 下行为一致,但底层流不可 Seek

无论协议怎么变,ASP.NET Core 接收表单文件时都走 IFormFile 抽象,它背后是 ReadOnlyStream(内存或临时磁盘流),CanSeek 恒为 false —— 这意味着你不能反复读取、不能直接传给需要 Seek() 的库(比如某些 ZIP 解压器)。

实操建议:

  • 如果要校验哈希或多次解析内容,先用 file.CopyToAsync(memoryStream) 缓存到 MemoryStream,再重置位置(memoryStream.position = 0
  • 别在 Controller 里直接 await file.OpenReadStream().CopyToAsync(...) 后又想读一遍 —— 流已到底,下次读返回空
  • 上传并发高时,FormOptions.MultipartBodyLengthLimitMemoryBufferThreshold 需权衡:设太低导致频繁落盘,太高则内存压力大

真正麻烦的从来不是选哪个协议,而是上传过程中网络抖动、服务重启、客户端切后台、磁盘满这些事 —— 协议只管“怎么送”,而健壮性得靠分片、校验、幂等、状态持久化一起兜底。

text=ZqhQzanResources