news 2026/10/7 2:04:43

C# HTTP POST JSON实战:从HttpClient到连接复用与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# HTTP POST JSON实战:从HttpClient到连接复用与避坑指南

简介:面向C#开发人员在.NET环境下通过HTTP POST协议以JSON格式进行数据交互的参考资源。内容围绕HttpClient请求构建、HttpRequestMessage与StringContent的组装、Json.NET序列化与反序列化、异步发送与响应读取、异常排查与错误处理等常见场景展开,覆盖从基础请求封装到完整数据交互流程的关键环节,并涉及跨域配置与HTTPS安全连接等进阶话题,适合需要快速上手网络通信开发的初中级工程师。压缩包共27个文件,包含9个dll、9个xml和9个pdb文件,均为Newtonsoft.Json库在不同目标框架(如net20、net35、net40、net45以及netstandard系列)下的多版本封装,按目标框架分目录存放,便于按需选取;xml注释可用于IDE智能提示,pdb便于调试定位。包体仅6.41MB,轻量易部署。已有1397人学习该资源。通过本包可获取开箱即用的JSON处理库与对应文档,免去逐一适配目标运行时的麻烦,在实际项目中可有效提升POST+JSON交互的开发调试效率。

1. C# http post协议,数据交互形式为json:这行字背后的真实需求

做上位机对接MES、把Power Focus 6000读到的扭矩值推到产线系统,很多C#工程师第一次被“http post协议”卡住,不是卡在POST动作本身,而是卡在“数据交互形式为json”这半句上。接口文档写着“POST JSON”,本地测的时候一切正常,一放到客户现场就超时、中文变问号、服务端回415,甚至请求发多了整个程序卡死。C# http post协议,数据交互形式为json,真正要做的是三件事:选对发送方式、把对象序列化成后端认得的JSON、把响应解析回你能操作的数据。这篇笔记按我实际调接口的顺序来写:先讲选型,再讲序列化,然后封装成能复用和重试的请求器,最后整理五个我踩过的坑。新手可以照着一步步跑通,熟手可以重点看参数边界和连接复用部分。

2. 用HttpClient还是WebClient:C#发HTTP POST JSON的选型与最小实现

2.1 三套方案对比:HttpClient、WebClient、HttpWebRequest

C#里发HTTP POST请求,常见做法有三套:最老的HttpWebRequest、后来封装过的WebClient,以及现在官方推荐的HttpClient。很多老项目里能看到WebClient,代码短,但它内部把很多细节藏起来,遇到需要自定义Content-Type、超时控制、连接复用的时候,反而不好操作。HttpWebRequest功能最全,但写法啰嗦,要自己处理流、编码、异常,还容易漏掉释放。HttpClient是.NET 4.5之后的标准方案,支持异步、默认复用连接,和System.Text.Json配合得很顺。

我做选型时基本遵循一个原则:新项目一律用HttpClient,老项目的维护性改动也优先换到HttpClient。除非项目还没升级到.NET Framework 4.5以上,否则没有必要再用WebClient写新代码。

方案优点缺点适用场景
HttpWebRequest控制粒度最细代码量大、易漏处理老系统维护、自定义协议
WebClientAPI简单、上手快连接管理弱、超时控制差偶尔一次的小脚本
HttpClient异步友好、连接复用、官方推荐生命周期需要管理上位机、后端服务、日常接口对接

2.2 最小可用代码:HttpClient POST一个JSON对象

先跑通一个最简例子,再谈封装。下面这段代码用HttpClient向本地一个测试接口发送JSON,并读取响应:

using System; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; var payload = new { deviceId = "PF-6000-01", torque = 12.5, pass = true }; var json = JsonSerializer.Serialize(payload); using var content = new StringContent(json, Encoding.UTF8, "application/json"); using var client = new HttpClient(); client.Timeout = TimeSpan.FromSeconds(10); var resp = await client.PostAsync("http://192.168.0.110:8080/api/torque", content); var body = await resp.Content.ReadAsStringAsync(); Console.WriteLine($"{(int)resp.StatusCode} {body}");

这段代码的关键点有三个。第一,StringContent的第三个参数写成了"application/json",这是告诉服务端我发的是JSON,不是普通表单;如果不写这个参数,默认是text/plain,很多后端接口会直接拒绝。第二,Encoding.UTF8必须显式写出来,尤其当JSON里包含中文时,编码不一致会乱码。第三,Timeout这里设成了10秒,上位机请求远程接口时不能一直等,超时时间要根据接口实际耗时来调,后面会有专门说明。

还要提醒一句:这段代码里new HttpClient()后面跟着using,只是图省事。实际项目里不要每次请求都new一个HttpClient,这会造成连接资源浪费和端口耗尽,第4章会详细讲。

2.3 必调的三个参数:Timeout、Content-Type与Accept

参数看起来不起眼,但大多数对接失败都出在这几处。Content-Type我们已经说过了,它必须与请求体格式一致。发送JSON时,服务端对Content-Type的检查可能是完全匹配,也可能只检查application/json前缀,所以最稳妥的写法是application/json; charset=utf-8,把字符集也带上。

Accept是很多C#工程师容易忽略的。它表示客户端希望接收什么格式的响应。有的后端会同时返回JSON和HTML两种格式,如果没有Accept头,服务端默认可能返回HTML,然后你的反序列化代码就炸了。我一般会加上Accept: application/json。

User-Agent也值得设置。有些Web服务有反爬或安全策略,不接受默认的HttpClient标识;还有一些网关会按UA做路由。在生产环境里,我建议把这些头信息放到一个静态的HttpClient.DefaultRequestHeaders里统一设置,而不是每次请求都手动赋值。

Timeout的设置需要一点血泪经验。如果接口本身要处理大数据量,10秒可能不够。反过来,如果接口在内网,3秒就够了。我习惯先设5秒做一次测试,看响应时间分布再调整。超时太短会误判为失败,超时太长会让上位机界面卡住。对于可能慢的接口,应该用下一页要讲的异步和CancellationToken,而不是一味拉长Timeout。

3. JSON的组装与解析:别让序列化拖后腿

3.1 系统库JsonSerializer与Newtonsoft.Json的选择

把C#对象变成JSON,首选是.NET自带的System.Text.Json。它在.NET Core 3.0之后内置,性能和内存占用都比Newtonsoft.Json好,而且不用引入第三方包。但有一个坑:System.Text.Json默认对属性名是大小写敏感的,反序列化时如果后端给的是DeviceID而你的C#属性叫deviceId,就匹配不上。

我遇到这种情况时,通常给属性加JsonPropertyName特性,或者在构造JsonSerializerOptions时设置:

var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, WriteIndented = false }; var obj = JsonSerializer.Deserialize<MyClass>(json, options);

PropertyNameCaseInsensitive解决大小写不一致的问题,但如果你要对接的是老系统,比如某些金蝶云接口,返回字段风格不统一,又或者项目里已经大量使用Newtonsoft.Json的JObject、JArray,那没必要强行迁移。Newtonsoft在灵活性和生态上仍然有优势,比如它处理日期格式“/Date(1546300800000)/”这种老式格式比系统库方便。我的习惯是:新写的数据接口用System.Text.Json;复杂字段转换多、老项目重构,继续用Newtonsoft,不要为了追新而踩坑。

3.2 发送嵌套对象与接收不定结构:JToken和JsonDocument的查询写法

业务数据很少是扁平的,比如上报扭矩值的请求里可能要带“工位信息”、“操作员信息”这种嵌套对象。C#里直接定义强类型类最清晰,但有些接口文档比较随意,或者结构是会变的,这时候可以动态拼。

发送嵌套对象可以先定义类:

public class TorqueReport { public string DeviceId { get; set; } public double Torque { get; set; } public StationInfo Station { get; set; } } public class StationInfo { public string StationNo { get; set; } public string LineName { get; set; } } var report = new TorqueReport { DeviceId = "PF-6000-01", Torque = 12.5, Station = new StationInfo { StationNo = "S01", LineName = "总装一线" } }; var json = JsonSerializer.Serialize(report);

序列化后得到的JSON就是嵌套结构。接收响应时,如果后端返回的JSON不固定,或者你想先试探结构再决定怎么绑定,推荐用JsonDocument做一次“侦查”。不少人一上来就反序列化成强类型,结果接口稍微变一下字段就异常。先用JsonDocument查一下,是更稳的姿势。

using var doc = JsonDocument.Parse(body); var root = doc.RootElement; bool success = root.GetProperty("success").GetBoolean(); double torque = root.GetProperty("data").GetProperty("torque").GetDouble(); if (root.TryGetProperty("list", out var list) && list.ValueKind == JsonValueKind.Array) { foreach (var item in list.EnumerateArray()) { Console.WriteLine(item.GetProperty("id").GetString()); } }

TryGetProperty配合ValueKind判断,是非常实用的“json查询函数”。它能避免接口缺字段时抛异常,也能处理“这个字段可能是对象也可能是数组”的情况。遇到不确定结构时,我绝不直接写死索引,而是先判断再取值。

3.3 时间格式、空值与大小写的三个处理习惯

JSON里最容易闹鬼的是时间。System.Text.Json默认序列化DateTime为ISO 8601格式,比如2026-05-01T08:30:00,这很好。但如果你遇到的后端要的是yyyy-MM-dd HH:mm:ss,就需要自定义转换器。别偷懒用ToString("yyyy-MM-dd HH:mm:ss")先把时间转成字符串,因为DateTime类型序列化时不会读你原来的字符串。

字符串拼JSON时,空值处理也要小心。JsonSerializer默认会保留值为null的属性,有些后端对此敏感,希望忽略空值。可以通过DefaultIgnoreCondition配置:

var options = new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull };

有一个容易被忽略的小点:System.Text.Json默认会把非ASCII字符转成\uxxxx。比如“扭矩”会变成\u626d\u77e9。这在网络传输上没问题,但如果你在服务端日志里看到一堆\u开头的东西,容易误判为乱码。如果你希望直接显示中文,可以通过Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping关闭转义。不过这会让输出体积变大,生产环境除非有调试需求,否则保持默认就好。

大小写问题建议约定俗成:C#属性用PascalCase,但是系统库序列化默认用了camelCase?其实默认是序列化成员原名PascalCase;反序列化时大小写敏感。为了和后端一致,我通常在JsonSerializerOptions里设置PropertyNamingPolicy = JsonNamingPolicy.CamelCase,让请求体变成小驼峰,这样跟大多数REST API的JSON风格一致,也减少后端联调时的争议。

4. 把POST JSON封装成可靠的上位机请求器:连接复用、超时与重试

4.1 不要把HttpClient放在using里:连接复用与Socket耗尽

第2章的最小代码里我用using var client = new HttpClient(),那是为了先跑通。真实的上位机程序,每秒钟可能上报一次数据,如果你每次上报都new一个HttpClient,用完之后释放,底层网络连接不会立刻关闭,而是会进入TIME_WAIT状态。持续跑一小时,你会看到本地端口被占满,报错“无法连接到远程服务器”。

这个问题的官方解法是:把HttpClient设计成单例,或者使用IHttpClientFactory。HttpClient内部的HttpClientHandler会帮你维护连接池,同一个HttpClient实例会自动复用连接。这也是热词里常说的“http连接复用”。注意,连接复用不代表你要手动去设置Connection: Keep-Alive,HttpClient默认就是keep-alive。你只要不要每次new,就自然复用。

一个简单的单例写法是:

public static class ApiClient { private static readonly HttpClient _client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; static ApiClient() { _client.DefaultRequestHeaders.Accept.Add( new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); } public static HttpClient Instance => _client; }

4.2 一个带超时、重试和日志的POST封装

单例解决了连接复用,但还没解决重试和定位问题。上位机请求MES系统时,网络抖动、服务端瞬间重启都会导致一次请求失败。常见做法是加一个简单的指数退避重试。注意,重试只适合幂等接口;如果你的POST会让服务端重复建单,重试必须谨慎,或者只在响应超时且明确知道服务端未处理时才重试。

下面这个封装支持设置重试次数,并把请求和响应摘要打进日志:

public static async Task<string> PostJsonAsync( string url, string json, int retryCount = 3, CancellationToken ct = default) { var content = new StringContent(json, Encoding.UTF8, "application/json"); for (int i = 0; i <= retryCount; i++) { try { using var resp = await ApiClient.Instance.PostAsync(url, content, ct); var body = await resp.Content.ReadAsStringAsync(ct); if (!resp.IsSuccessStatusCode) { Log.Warn($"HTTP {(int)resp.StatusCode} url={url} body={body}"); } return body; } catch (OperationCanceledException) when (!ct.IsCancellationRequested) { Log.Warn($"Timeout url={url} attempt={i + 1}"); await Task.Delay(TimeSpan.FromSeconds(2 * (i + 1)), ct); } catch (HttpRequestException ex) { Log.Warn($"Request failed url={url} ex={ex.Message}"); await Task.Delay(TimeSpan.FromSeconds(2 * (i + 1)), ct); } } throw new InvalidOperationException($"POST failed after {retryCount + 1} attempts: {url}"); }

这里有几个设计取舍。第一,StringContent不要在循环外面复用,因为发送后流位置可能不对,每次重试都应该重建一次。第二,捕获异常时分两类:OperationCanceledException多半是超时,但CancellationToken手动取消时也会抛这个异常,所以判断里加上!ct.IsCancellationRequested,别把用户的取消当成超时重试。第三,Task.Delay(TimeSpan.FromSeconds(2 * (i+1)))是两倍递增的退避,第一次等2秒,第二次等4秒,避免连续重试把服务端打得更死。

4.3 用postman做对照:先跑通再上代码

封装之前,我强烈建议你先在Postman里把接口调通。很多人直接上手写C#,出问题之后分不清是“网络不通”、“JSON格式不对”还是“后端逻辑报错”。用Postman发送POST请求,选POST方法,URL填上,请求体选raw并设置为JSON,然后直接Send。如果Postman能通,说明网络和后端基本没问题,问题大概率在C#的请求构造;如果Postman也不通,先别改代码,去查IP、端口、服务状态。

我见过一个假象:Postman里放一个中文JSON字符串,发送成功,C#里同样字符串发过去后端就乱码。原因是Postman自动把Charset设成了UTF-8,而C#代码里忘了指定编码。这种问题在对照Postman时最容易发现。所以保留Postman的请求页签,把Content-Type和Charset截图下来,跟C#的请求头逐项对比。

另外,公司内网接口经常有防火墙或网关限制,Postman能通是“你本机IP在白名单”,而程序跑在服务器上,服务器IP不在白名单,结果就是“本地没问题,一部署就超时”。用Postman对照时,最好在目标环境所在的机器上再测一次,而不是只在开发机测。

5. C# HTTP POST JSON常见翻车现场与避坑记录

5.1 现象:本地调试正常,部署到客户机器上就超时

这是最典型的上位机事故。在本机用Postman和程序都能通,打包发布到客户电脑后,POST请求十有八九超时。原因通常是目标服务器只允许特定IP访问,或者客户机器上开了系统代理。更隐蔽的是,Windows的WinHTTP代理设置可能继承了某些系统级配置,导致HttpClient走了代理。

解决方法是先确认网络路径:在客户机器上用telnet 目标IP 端口或Test-NetConnection验证TCP连通性。如果网络能通,再检查代码里的代理设置。对已知的内网接口,我一般显式关掉代理:

var handler = new HttpClientHandler { UseProxy = false }; var client = new HttpClient(handler);

注意:如果你用了HttpClient的单例且已经设置过UseProxy,要在创建时就传入HttpClientHandler,不能后面再改。这也是为什么我把单例封装放在前面的原因。

5.2 现象:服务端返回415或406,说Content-Type不对

POST发出去了,但服务端直接回415 Unsupported Media Type,或者406 Not Acceptable。原因几乎都出在请求头。StringContent虽然你写了application/json,但有时候编码会重复,比如application/json; charset=utf-8和application/json; charset=UTF-8,某些敏感的后端解析字符串时会严格比较,大小写不同也可能拒收。

解决方法是打开Fiddler或直接在C#里打印请求头。检查两个地方:Content-Type是否真的带上了application/json,Accept头是否写了后端支持的格式。还遇到过一种情况:服务端要求的是text/json而不是application/json,这种后端文档写得不清楚的时候,用Postman抓一下成功的请求头,照搬过来。

5.3 现象:中文变成问号或乱码

发送中文JSON后,服务端日志显示“???”或者“扭矩”这种奇怪的字符。原因一般是编码不一致。C#端用Encoding.UTF8发送,但服务端用GBK解码;或者反过来。再一个常见原因是服务端接收时读RequestBody的编码不是UTF-8,需要它调整。

C#这侧的排查比较固定:确认StringContent第一个参数是字符串,第二个参数是Encoding.UTF8,第三个参数是application/json。不要直接用ByteArrayContent然后手动把字符串转成byte[],除非你明确知道要什么编码。如果你在日志里看到\u626d\u77e9这种转义,不代表服务端乱码,那是JSON标准转义,后端解析后是正常中文。别把两件事搞混。

5.4 现象:请求量一大就出现“无法连接到远程服务器”

上位机每秒钟上报一次扭矩数据,跑不了几分钟就报错,错误信息类似“SocketException: 由于目标计算机积极拒绝”,或者“通常每个套接字地址只允许使用一次”。刚才讲过,这基本是每次都new HttpClient造成的。每次new都会新建TCP连接,连接释放后进入TIME_WAIT状态,Windows默认要等120秒才能回收端口。所以几百个请求后,本地可用端口耗尽。

解决方法是改成单例HttpClient。改完之后再看现象是否消失。如果还是出问题,打开netstat -ano | findstr TIME_WAIT,看看是不是还有大量TIME_WAIT堆积。如果单例还堆积,可能是服务端主动关闭连接,导致客户端没来得及复用。这种情况下,可以调整Keep-Alive的超时或考虑用IHttpClientFactory管理生命周期。对桌面程序和上位机来说,静态单例基本够用。

5.5 现象:反序列化抛异常,原来后端返回的是JSON数组

调好的接口,响应一直是{"success":true,"data":{...}},某天突然反序列化报错——JsonException,说请求的根元素是数组,不是对象。这种情况很常见,尤其是MES或者金蝶云这类系统的部分接口,列表查询返回的是[{...},{...}],而且接口文档没写清楚。

解决方式分两步。第一步,先用JsonDocument解析根节点,判断ValueKind是Array还是Object再决定处理方式:

using var doc = JsonDocument.Parse(body); if (doc.RootElement.ValueKind == JsonValueKind.Array) { var list = JsonSerializer.Deserialize<List<MyDto>>(body); } else { var single = JsonSerializer.Deserialize<MyDto>(body); }

第二步,如果是数组,但你的业务只需要第一个元素,要确认到底是“只取第一条”还是“应该是单条数据”:有的服务端为了统一格式,即使只有一条也返回数组。这种情况下,最好在封装层做一个标准化方法,把data字段里的数组和对象都转成同一个类型结构,避免上层业务去判断。定期检查接口返回的JSON结构,用Postman看一眼,别等代码崩了再处理。

6. 进阶:一个能应对慢接口的异步POST函数,顺手验证连接是否复用

把之前的封装改成完全异步并支持CancellationToken,在前台上位机里特别重要。因为PostAsync本身是异步的,但如果你在调用处用了.Result或.Wait(),依然会卡死UI线程。正确做法是从按钮事件开始就async void,一路await下来。

private async void btnSend_Click(object sender, EventArgs e) { btnSend.Enabled = false; try { var json = JsonSerializer.Serialize(report); var body = await PostJsonAsync(url, json, retryCount: 2, ct: cancellationToken); txtResult.Text = body; } catch (Exception ex) { MessageBox.Show(ex.Message); } finally { btnSend.Enabled = true; } }

async void只在UI事件里用,业务层不要这么写。另外,长时间运行的轮询上报,要给每个请求带一个独立的CancellationTokenSource,在窗体关闭时取消正在等待的请求,否则程序关不掉,进程一直在后台等超时。

验证连接是否有复用的一个土办法:在上位机里连续调用100次PostJsonAsync,然后看本地端口变化。Windows上执行netstat -ano | findstr "<目标端口>",如果建立和断开的连接数远小于请求数,说明在复用连接。如果每次请求都新建连接,你会看到大量TIME_WAIT堆积。这个方法我一般写进联调自测清单里。

还有一个容易忽略的习惯:对日志里出现的异常,不要只打印异常信息,我总会整体记录URL、请求体摘要、响应时间、HTTP状态码。这样凌晨被叫起来处理现场问题时,不用靠猜。这个习惯帮我避免了好几次半夜翻日志翻到天亮。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 2:04:40

GLM-4代码仓库源码zip包:从解压到跑通推理的完整避坑指南

简介&#xff1a;本资源为GLM-4代码仓库源码zip包&#xff0c;面向大模型应用开发者、算法工程师及希望研究GLM-4工程实现的技术人员&#xff0c;可用于本地部署、推理调用、微调实验与二次开发。压缩包共78个文件&#xff0c;约7.57MB&#xff0c;以Python脚本为主体&#xff…

作者头像 李华
网站建设 2026/10/7 2:04:30

基于Spring Boot+Spring Security+JWT的官方账号认证系统实战

之前在做账号系统的国际化改造时&#xff0c;碰到一个非常典型的账号注册场景&#xff1a;用户的显示名称是“谷口愛季”&#xff0c;登录用户名却是airi.taniguchi.official。刚开始我以为这只是普通的用户名&#xff0c;结果在开发环境里连续踩了不少坑——数据库唯一约束对大…

作者头像 李华
网站建设 2026/10/7 2:04:29

小智AI语音控制实战:MCP工具注册与系统音量调节全流程

前几天夜里我一直在折腾一件事&#xff1a;让小智AI在听懂“把音量调到百分之四十”之后&#xff0c;真的动手去改系统音量&#xff0c;而不是只回我一句“好的&#xff0c;已为你调低音量”。这个目标听起来很基础&#xff0c;但真走完才发现&#xff0c;背后其实是一条很长的…

作者头像 李华
网站建设 2026/10/7 2:02:32

Samba 4 域控运维脚本集:备份、巡检与信息采集实战

简介&#xff1a;这份资源汇集了在 Samba 4&#xff08;AD-DC&#xff09;环境中日常运维常用的 Shell 脚本集合&#xff0c;面向在 Debian Jessie 与 Debian Stretch 上搭建、维护 Samba 域控及成员服务器的系统管理员与运维人员。内容涵盖备份、权限检查、sysvol ACL 设置、域…

作者头像 李华