恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
GetAsync 和 SendAsync 到底差在哪?
首页
资讯中心
/
GetAsync 和 SendAsync 到底差在哪?
GetAsync 和 SendAsync 到底差在哪?
发布时间:2026/10/11 1:21:45
摘要很多人把这两个层面的东西混在一起答GET 和 POST 有什么区别。一边是 HTTP协议层的语义契约GET/POST/PUT/DELETE一边是 .NET客户端 API 的调用方式GetAsync/PostAsync/PutAsync/DeleteAsync/SendAsync。前者是交通法规后者是方向盘和挡位。本文把两层彻底切开并给出一张五个便捷方法到底差在哪的对称表——你会发现它们其实并不对称。一、先分清你在说哪一层层次主角回答的问题谁在乎协议层GET/POST/PUT/DELETE这个请求想对资源做什么服务端、网关、CDN、浏览器、代理API 层GetAsync/PostAsync/PutAsync/DeleteAsync/SendAsync我在 C# 里怎么写这段代码只有你和编译器一句话点破GetAsync之所以叫 Get只是因为它往请求行里写了GET这个单词。 它不负责保证幂等保证可缓存——那些是协议层的事由服务端和中间设备决定。打个比方协议层 寄快递时选的寄件 / 退货 / 代收货款。快递公司看到类型才知道能不能保价、能不能撤回、要不要记账。API 层 你在 App 上点哪个按钮。按钮长得不一样但填完信息最终都走同一个下单接口。在 .NET 里那个同一个下单接口就是SendAsync。二、第一层HTTP 协议层的 GET / POST / PUT / DELETE2.1 四个动词的语义契约方法语义安全 Safe幂等有 Body可缓存常用成功码GET获取资源表示✅✅规范上无✅200POST创建资源 / 执行动作❌❌✅❌201 / 200PUT整体替换资源❌✅✅❌200 / 204DELETE删除资源❌✅一般无❌204 / 200PATCH局部更新❌视实现✅❌200 / 204两个定义值得咬文嚼字安全Safe不产生服务端副作用只读。不代表没日志、没计费指的是不改变资源状态。幂等Idempotent发 1 次和发 N 次资源最终状态一致。PUT /products/1提交同一对象 3 次结果和第 1 次一样 → 幂等DELETE /products/1第一次返回 204第二次返回 404 ——这也是幂等的因为商品不存在这个状态没变。幂等 ≠ 返回码相同POST /orders发两次就是两张订单 → 非幂等2.2 为什么这套约定真的重要因为中间设备会当真。你的请求要穿过浏览器、企业代理、CDN、API 网关、负载均衡只有 GET 会被浏览器 / CDN缓存只有幂等方法GET / PUT / DELETE在网络抖动后能被自动重试网关的 WAF、限流、审计规则通常按方法区分策略只有非安全方法才会触发 CORS预检Preflight所以用 POST 干查询能跑通代价是丢掉缓存能力、丢掉自动重试、语义只能靠文档解释。三、第二层HttpClient API 层3.1 真相所有便捷方法最后都汇聚到 SendAsyncGetAsync / GetStringAsync / GetByteArrayAsync / GetStreamAsync PostAsync / PutAsync / PatchAsync / DeleteAsync │ ▼ 全部是语法糖内部构造 HttpRequestMessage HttpClient.SendAsync(HttpRequestMessage, ...) │ ▼ HttpMessageHandler 管道DelegatingHandler 链 → SocketsHttpHandler │ ▼ 网络GetAsync(url)在语义上几乎等价于using var req new HttpRequestMessage(HttpMethod.Get, url); using var resp await http.SendAsync(req);微软官方文档也是这么归类的——SendAsync那一栏写的是USER SPECIFIED任意 HttpMethod其他方法各自对应一个固定动词。3.2 逐个看五个方法各自的签名与限制①GetAsync—— 只有它支持HttpCompletionOptionGetAsync(string requestUri) GetAsync(Uri requestUri) GetAsync(string requestUri, HttpCompletionOption completionOption) // ✅ 独有 GetAsync(string requestUri, CancellationToken ct) GetAsync(Uri requestUri, HttpCompletionOption completionOption, CancellationToken ct) // ... 共 8 个重载覆盖 string/Uri × 有无 completionOption × 有无 ct注意GetAsync没有接受HttpContent的重载——这是故意的协议层就说 GET 不该有 body。GET 专属的快捷通道System.Net.Http内置var text await http.GetStringAsync(api/products/1); // → string var bytes await http.GetByteArrayAsync(api/products/1/img); // → byte[] await using var s await http.GetStreamAsync(api/report.csv);// → Stream var dto await http.GetFromJsonAsyncProductDto(api/products/1); // ← System.Net.Http.Json 扩展②PostAsync/ ③PutAsync—— 签名几乎一模一样PostAsync(string requestUri, HttpContent content) PostAsync(string requestUri, HttpContent content, CancellationToken ct) PutAsync (string requestUri, HttpContent content) PutAsync (string requestUri, HttpContent content, CancellationToken ct)它们都只有 4 个重载string/Uri × 有无 ct没有HttpCompletionOption版本。 这意味着POST/PUT 想要流式读取响应只能走SendAsync。HttpContent是抽象类常见实现var json new StringContent(payload, Encoding.UTF8, application/json); var json2 JsonContent.Create(new { name 显示器, price 1299 }); // 推荐自动带 Content-Type var form new FormUrlEncodedContent(new[] { new KeyValuePairstring,string(k,v) }); var file new StreamContent(fileStream); var multi new MultipartFormDataContent(); // 上传不想手动包装用System.Net.Http.Json的扩展.NET 7 补齐了 PATCHawait http.PostAsJsonAsync(api/products, dto); await http.PutAsJsonAsync(api/products/1, dto); await http.PatchAsJsonAsync(api/products/1, new { price 459 });⚠️PostAsJsonAsync这类方法不在HttpClient类上是System.Net.Http.Json命名空间下的扩展方法。找不到方法时先检查using。④DeleteAsync—— 最残缺的那个DeleteAsync(string requestUri) DeleteAsync(Uri requestUri) DeleteAsync(string requestUri, CancellationToken ct) DeleteAsync(Uri requestUri, CancellationToken ct)没有任何接受HttpContent的重载。 微软文档的官方说法是因为 HTTP DELETE 请求通常不含请求体DeleteAsync方法不提供接受HttpContent实例的重载。想给 DELETE 带 body比如批量删除只能绕道SendAsyncusing var req new HttpRequestMessage(HttpMethod.Delete, api/products/bulk) { Content JsonContent.Create(new { ids new[] { 1, 2, 3 }, reason 过期清理 }) }; using var resp await http.SendAsync(req);顺带提醒部分服务器Nginx、IIS 某些版本会直接丢弃 DELETE 的 body这是服务器行为不是 C# 的锅。批量删除更稳妥的设计是POST /api/products/bulk-delete。⑤PatchAsync—— 后来才补上的PatchAsync(string requestUri, HttpContent content) PatchAsync(string requestUri, HttpContent content, CancellationToken ct)从.NET Core 2.1 / .NET Standard 2.1 开始才有。在此之前大家只能手写new HttpRequestMessage(HttpMethod.Patch, url)。它的PatchAsJsonAsync扩展更要等到.NET 7。⑥SendAsync—— 唯一的完整出口SendAsync(HttpRequestMessage request) SendAsync(HttpRequestMessage request, CancellationToken ct) SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption) SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken ct)3.3 关键五个方法其实不对称这张表是全文最值钱的部分——很多人以为五个方法只是名字不同其实能力差别很大方法能带 Body能指定HttpCompletionOption能设逐请求 Header能自定义方法/版本/OptionsGetAsync❌ 无重载✅唯一有❌❌PostAsync✅❌❌❌PutAsync✅❌❌❌PatchAsync✅❌❌❌DeleteAsync❌无重载❌❌❌SendAsync✅✅✅✅由此得出三条实战结论想让 POST/PUT/PATCH 的响应流式读取 → 必须用SendAsync它们没有HttpCompletionOption重载想给 DELETE 带 body → 必须用SendAsync想给任何请求加逐请求 HeaderAuthorization、TraceId、签名→ 必须用SendAsync3.4SendAsync独占的六种能力能力 1任意 HTTP 方法含自定义using var req new HttpRequestMessage(new HttpMethod(REPORT), url); // WebDAV.NET 10 起还多了一等公民HttpMethod.QueryRFC 10008 定义的带 body 的安全查询方法using var q new HttpRequestMessage(HttpMethod.Query, api/products/search) { Content JsonContent.Create(new ProductFilter(Skus, MaxPrice)) }; using var r await http.SendAsync(q);能力 2逐请求 Header而非全局 Header// ❌ 并发下互相污染且 token 无法中途刷新 http.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, token); // ✅ using var req new HttpRequestMessage(HttpMethod.Get, api/me); req.Headers.Authorization new AuthenticationHeaderValue(Bearer, token); req.Headers.Add(X-Request-Id, requestId);规则所有请求都一样的User-Agent、Accept-Language放DefaultRequestHeaders会变的Authorization、TraceId、Tenant-Id必须放request.Headers。能力 3HttpCompletionOption—— 决定什么时候返回// 默认 ResponseContentRead整个响应体下载进内存后 await 才返回 using var resp await http.GetAsync(url); // ResponseHeadersRead收到响应头就返回响应体还在网络上流着 using var resp2 await http.SendAsync( new HttpRequestMessage(HttpMethod.Get, url), HttpCompletionOption.ResponseHeadersRead); if (resp2.Content.Headers.ContentLength 100 * 1024 * 1024) throw new InvalidOperationException(响应过大拒绝下载); await using var stream await resp2.Content.ReadAsStreamAsync(); await stream.CopyToAsync(fileStream); // 大文件直接落盘不进内存差异的本质默认的ResponseContentRead会先把整个响应缓冲进一个MemoryStream好处是连接能立刻归还连接池坏处是大 payload 时内存陡增ResponseHeadersRead跳过这层缓冲直接拿到 socket 上的流。⚠️ .NET 10 起浏览器环境WASM / Blazor默认开启响应流式需要HttpCompletionOption.ResponseHeadersRead语义的行为成了默认ReadAsStreamAsync()返回的不再是MemoryStream—— Blazor 代码里的同步流操作升级后会报错。能力 4HttpRequestMessage.Options—— 给管道传私有参数static readonly HttpRequestOptionsKeybool SkipAudit new(SkipAuditLog); using var req new HttpRequestMessage(HttpMethod.Post, api/users/export); req.Options.Set(SkipAudit, true); // DelegatingHandler 里能读到能力 5控制 HTTP 版本与策略using var req new HttpRequestMessage(HttpMethod.Get, url) { Version new Version(2, 0), VersionPolicy HttpVersionPolicy.RequestVersionOrLower, };能力 6让DelegatingHandler管道统一拦截public class TimingHandler : DelegatingHandler { protected override async TaskHttpResponseMessage SendAsync( HttpRequestMessage request, CancellationToken ct) { var sw Stopwatch.StartNew(); try { return await base.SendAsync(request, ct); } finally { Log(${request.Method} {request.RequestUri} {sw.ElapsedMilliseconds}ms); } } }DelegatingHandler只能重写SendAsync——你的自定义中间件认证刷新、重试、审计、签名、Mock天然围绕HttpRequestMessage工作。业务代码统一走SendAsync中间件才能一视同仁地拦截所有请求。四、两层对照速查表维度协议层 GET/POST/PUT/DELETEAPI 层 GetAsync/PostAsync/PutAsync/DeleteAsync/SendAsync本质请求行里的动词字符串C# 的方法调用约束对象服务端 所有中间设备只有你的编译期幂等性由协议定义API不关心也不检查谁能看到抓包、网关日志都能看到只有源码里有能否自定义可以任意合法 token只有SendAsync可以性能差异——没有便捷方法内部就是SendAsync五、六个高发误解❌ 误解 1用SendAsync就能绕开 GET 的语义不能。你写HttpMethod.Get线上报文里就是GET服务端和 CDN 该怎么对待还是怎么对待。换 API 不改变协议层语义就像换个牌子的方向盘不改变交通法规。❌ 误解 2GetAsync不能带 Body那就SendAsync GET Body技术上SendAsync确实能往 GET 里塞Content但 RFC 9110 明确说GET 的 body 没有通用语义很多代理会静默丢弃还可能被当成请求走私攻击拦截。正确做法是POST /search或.NET 10 起QUERY而不是给 GET 塞 body。❌ 误解 3PostAsync非幂等是 HttpClient 决定的恰恰相反PostAsync只是往报文里写了POST。非幂等是HTTP 规范对 POST 的定义。你完全可以用PostAsync打一个幂等接口服务端做了去重也可以用SendAsync(HttpMethod.Get, ...)打一个改数据的接口那是你的锅。❌ 误解 4HttpRequestMessage可以复用不能。 第二次发送会抛InvalidOperationException: The request message was already sent. Cannot send the same request message multiple times.源码层面是HttpClient.CheckRequestMessage调用了request.MarkAsSent()已发送过的会被拦下。有趣的反直觉点为什么 Polly /Microsoft.Extensions.Http.Resilience的重试能重发同一个消息因为校验只发生在HttpClient.SendAsync这一层管道内部的DelegatingHandler调base.SendAsync不再校验。所以你自己写重试循环必须新建HttpRequestMessage// ❌ 会炸 var req BuildRequest(); for (int i 0; i 3; i) await http.SendAsync(req); // ✅ 每次新建 for (int i 0; i 3; i) { using var req BuildRequest(); // 工厂方法每次全新 var resp await http.SendAsync(req); if (resp.IsSuccessStatusCode) return resp; }同理配了AddStandardResilienceHandler()的重试策略时请求体优先用StringContent/ByteArrayContent/JsonContent不要用StreamContent —— 流只能读一次重试第二次拿不到内容。❌ 误解 5SendAsync比GetAsync快完全一样。GetAsync内部就是构造HttpRequestMessage再调SendAsync多出来的只有一次对象分配可忽略。SendAsync的价值是控制粒度不是性能。真正影响性能的是复用HttpClient或IHttpClientFactory、ResponseHeadersRead流式读取、SocketsHttpHandler连接池配置。❌ 误解 6便捷方法会自动抛异常不会。GetAsync/PostAsync等在收到 4xx / 5xx 时返回正常的HttpResponseMessage不会抛异常。HTTP 状态码不是错误是结果。要抛异常得显式调EnsureSuccessStatusCode()using var resp await http.GetAsync(url); resp.EnsureSuccessStatusCode(); // 非 2xx 时抛 HttpRequestException var dto await resp.Content.ReadFromJsonAsyncProductDto();六、选型决策到底用哪个要发请求 │ ├─ 标准动词 响应体不大 无需特殊 header │ └─► 便捷方法GetFromJsonAsync / PostAsJsonAsync / PutAsJsonAsync / DeleteAsync │ 代码最短意图最清晰 │ ├─ 满足以下任一 → 必须 SendAsync │ · 方法非标准QUERY / REPORT / 自定义 │ · 逐请求 headerAuthorization、TraceId、签名 │ · POST/PUT/PATCH 也要 ResponseHeadersRead 流式读取 │ · DELETE 要带 body │ · 指定 HTTP 版本、设置 Options │ · 需要统一出口让 DelegatingHandler 拦截所有请求 │ └─ 无论哪个 HttpClient 必须复用单例 或 IHttpClientFactory HttpRequestMessage 每次新建绝不复用 记得 EnsureSuccessStatusCode()它不会自动抛七、一页纸总结协议层GET/POST/PUT/DELETE是给机器看的契约查用 GET安全幂等可缓存增用 POST非幂等全改用 PUT幂等部改用 PATCH删用 DELETE幂等动作型用 POST用对动词你免费获得缓存、自动重试、可发现性三样能力API 层GetAsync/PostAsync/PutAsync/DeleteAsync/SendAsync是给人和编译器看的语法糖五个便捷方法全部是SendAsync的包装方法名只决定报文里那个动词字符串它们并不对称只有GetAsync有HttpCompletionOption重载DeleteAsync没有任何带 body 的重载五个方法都无法设置逐请求 headerSendAsync是唯一完整出口任意方法、逐请求 header、流式读取、HTTP 版本、Options、与DelegatingHandler管道无缝协作HttpRequestMessage一次性、不可复用HttpClient必须复用记住这两句就不会再混协议层决定这个请求该不该被缓存、被重试——你说了不算。API 层决定这段代码好不好读、好不好扩展——这个你说了算。