C# HttpClient 发送 JSON POST 请求实战:序列化、编码与连接池避坑指南 简介这份资源面向使用C#进行网络通信开发的程序员聚焦HTTP POST协议下以JSON格式完成数据交互的完整实现思路。内容围绕HttpClient类的请求构建、Json.NETNewtonsoft.Json的序列化与反序列化、POST请求发送与响应接收、异常捕获及HTTPS安全连接等关键环节展开适合需要对接接口、调试数据交互的初中级开发者参考。压缩包共27个文件以9个dll动态库、9个xml文档和9个pdb调试符号文件为主涵盖Newtonsoft.Json在net20至netstandard2.0等多个框架版本下的程序集方便按目标平台直接引用整体约6.41MB。目前已有1399人学习下载。借助这些现成组件与配套说明读者可快速完成JSON对象的序列化发送、响应内容反序列化解析并掌握错误处理与跨域、安全连接等常见问题的应对方式减少自行配置依赖与排查兼容性的时间。1. 从一次接口联调翻车说起C# 里把 JSON 塞进 HTTP POST 到底难在哪前阵子帮一个做上位机的小团队看问题他们用 C# 写了个采集端要把设备状态定时推给服务端协议定的是 HTTP POSTbody 走 JSON。代码写出来不到三十行本地跑得好好的一上现场就出幺蛾子服务端偶尔收到空 body偶尔报 415偶尔中文变问号。几个人查了一下午最后发现是StringContent没指定Encoding.UTF8加上HttpClient被using包在循环里反复 new连接池被打爆。这类问题在 C# 网络协议交互里太典型了——不是不会发请求而是不知道 JSON 序列化、Content-Type、字符编码、连接复用这几件事怎么配合。这份资源围绕的就是这个场景用 C# 通过 HTTP POST 把 JSON 数据发给服务端覆盖请求构造、序列化、异常处理、响应解析的完整链路。适合正在做接口对接、上位机数据上报、微服务之间轻量通信的开发者尤其是那种“能跑但总出玄学问题”的项目。下面我按拆项目的思路把它从结构到代码再到改造讲一遍。2. 项目结构与技术选型一个 POST 请求由哪些零件拼起来2.1 目录与文件组织这类项目通常不会太大但结构清晰与否直接决定后面好不好改。我见过的合格组织方式大致是这样HttpJsonDemo/ ├── Program.cs // 入口演示调用 ├── Services/ │ └── ApiClient.cs // 封装 HttpClient统一发请求 ├── Models/ │ ├── DeviceStatus.cs // 请求体数据模型 │ └── ApiResponse.cs // 服务端返回模型 ├── Helpers/ │ └── JsonHelper.cs // 序列化/反序列化配置 └── appsettings.json // 基地址、超时等配置核心思路是把“发请求”这件事从业务代码里抽出来。很多新手会把HttpClient、JsonSerializer、try-catch全塞在一个方法里第一次能跑第二次要加个 header 就得满文件找。抽成ApiClient之后业务层只关心“我传一个对象拿回一个对象”中间的网络细节被隔离。2.2 技术选型为什么是 HttpClient System.Text.JsonC# 发 HTTP 请求有几套方案老一点的HttpWebRequest、WebClient新一点的HttpClient还有第三方库如 RestSharp、Flurl。这个项目场景下常见做法是用HttpClient配System.Text.Json理由很实在方案适用场景本项目取舍HttpWebRequest老项目兼容太底层API 繁琐不推荐新写WebClient简单下载已标记过时POST JSON 不顺手HttpClient通用 HTTP 通信首选支持异步、连接池、拦截器RestSharp复杂 REST 客户端功能全但引入额外依赖System.Text.Json.NET 内置序列化性能好、无第三方依赖Newtonsoft.Json老项目、复杂转换兼容性好但需额外包选System.Text.Json不是因为它完美而是 .NET Core 3.0 之后内置少一个 NuGet 依赖就少一份版本冲突。如果你的项目里已经有 Newtonsoft也不必强行换两者可以共存只是别在同一个模型上混用特性标注。2.3 数据模型定义请求体的模型决定了序列化出来的 JSON 长什么样。假设要上报设备状态// Models/DeviceStatus.cs using System.Text.Json.Serialization; public class DeviceStatus { // 设备唯一编号序列化后字段名为 deviceId [JsonPropertyName(deviceId)] public string DeviceId { get; set; } // 温度值double 类型 [JsonPropertyName(temperature)] public double Temperature { get; set; } // 采集时间用 DateTimeOffset 保留时区 [JsonPropertyName(collectedAt)] public DateTimeOffset CollectedAt { get; set; } // 设备描述可能为 null序列化时忽略 [JsonPropertyName(remark)] [JsonIgnore(Condition JsonIgnoreCondition.WhenWritingNull)] public string Remark { get; set; } }这里有两个参数值得说。JsonPropertyName显式指定 JSON 字段名避免 C# 的 PascalCase 和服务端约定的 camelCase 对不上。JsonIgnore的WhenWritingNull条件让Remark为空时不出现在 JSON 里服务端如果对多余字段敏感这个设置能省掉一轮扯皮。DateTimeOffset比DateTime更适合跨时区场景序列化出来带偏移量服务端解析不会差八小时。3. 核心代码拆解从对象到 JSON 再到 HTTP 请求3.1 序列化配置别用默认的JsonSerializer的默认配置有几个坑属性名原样输出PascalCase、中文会被转义成\uXXXX、枚举输出数字。所以第一步是定一套统一的JsonSerializerOptions// Helpers/JsonHelper.cs using System.Text.Encodings.Web; using System.Text.Json; public static class JsonHelper { // 全局共享的序列化配置避免每次 new 造成性能损耗 public static readonly JsonSerializerOptions Options new JsonSerializerOptions { // 属性名首字母小写匹配服务端 camelCase 约定 PropertyNamingPolicy JsonNamingPolicy.CamelCase, // 允许中文等非 ASCII 字符原样输出不转义 Encoder JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // 反序列化时忽略大小写容错服务端字段大小写不一致 PropertyNameCaseInsensitive true, // 枚举以字符串形式输出可读性更好 Converters { new System.Text.Json.Serialization.JsonStringEnumConverter() } }; }UnsafeRelaxedJsonEscaping这个名字看着吓人其实只是允许中文、符号原样输出不改变 JSON 语义。不加它日志里看到的中文全是\u6D4B\u8BD5排查问题时要多绕一圈。PropertyNamingPolicy设成 CamelCase 后模型上就不用每个字段都写JsonPropertyName但两者同时存在时以特性为准这点要记住。3.2 构造 POST 请求StringContent 的正确姿势把对象变成StringContent是整条链路最容易翻车的地方// Services/ApiClient.cs using System.Net.Http; using System.Text; using System.Text.Json; public class ApiClient { private readonly HttpClient _httpClient; public ApiClient(HttpClient httpClient) { _httpClient httpClient; } public async TaskApiResponse PostStatusAsync(DeviceStatus status) { // 1. 序列化为 JSON 字符串 string json JsonSerializer.Serialize(status, JsonHelper.Options); // 2. 包装成 StringContent必须指定 UTF-8 和 application/json var content new StringContent( json, Encoding.UTF8, application/json); // 3. 发送 POST 请求 HttpResponseMessage response await _httpClient.PostAsync( /api/device/status, content); // 4. 读取响应字符串 string responseBody await response.Content.ReadAsStringAsync(); // 5. 非 2xx 状态码统一抛异常交给上层处理 if (!response.IsSuccessStatusCode) { throw new HttpRequestException( $请求失败状态码 {(int)response.StatusCode}响应 {responseBody}); } // 6. 反序列化为强类型对象 return JsonSerializer.DeserializeApiResponse( responseBody, JsonHelper.Options); } }逐段看。第一步序列化没什么好说关键是第二步StringContent的三个参数内容、编码、媒体类型。编码不写默认是 UTF-8但媒体类型不写就是text/plain服务端框架一看不是application/json直接返回 415。第三步PostAsync的 URL 是相对路径前提是HttpClient的BaseAddress已经配好。第五步把非成功状态码转成异常是为了让业务层用统一的try-catch处理而不是每次判断IsSuccessStatusCode。第六步反序列化如果响应是空字符串会返回 null调用方要判空。3.3 HttpClient 的生命周期单例还是每次 new这是 C# 网络编程里被讨论最多的问题之一。HttpClient实现了IDisposable直觉上应该用完就释放但它的底层HttpMessageHandler管理着连接池频繁创建销毁会导致 socket 耗尽TIME_WAIT 堆积。正确做法是// Program.cs 中的注册方式以控制台程序为例 var handler new SocketsHttpHandler { // 连接池空闲连接存活时间 PooledConnectionLifetime TimeSpan.FromMinutes(5), // 单服务器最大连接数 MaxConnectionsPerServer 10 }; var httpClient new HttpClient(handler) { // 基地址后续请求可用相对路径 BaseAddress new Uri(https://api.example.com), // 整体超时时间 Timeout TimeSpan.FromSeconds(30) }; var apiClient new ApiClient(httpClient);SocketsHttpHandler是 .NET Core 之后底层用的处理器PooledConnectionLifetime让连接定期重建避免 DNS 变更后还连旧 IP。MaxConnectionsPerServer控制并发设太小会排队设太大对服务端不友好。如果项目用依赖注入直接services.AddHttpClientApiClient()更省事框架帮你管理生命周期。我一般会把这个HttpClient注册成单例整个应用共用一个。3.4 响应模型与错误处理服务端返回的 JSON 结构要和ApiResponse对齐// Models/ApiResponse.cs using System.Text.Json.Serialization; public class ApiResponse { [JsonPropertyName(code)] public int Code { get; set; } [JsonPropertyName(message)] public string Message { get; set; } [JsonPropertyName(data)] public JsonElement Data { get; set; } }Data用JsonElement是为了兼容不同接口返回不同结构需要时再二次解析。如果每个接口返回结构固定直接定义具体类型更好。错误处理上除了HttpRequestException还要捕获TaskCanceledException超时和JsonException反序列化失败这三类异常覆盖了绝大多数线上问题。4. 避坑与排查那些让联调卡半天的细节4.1 中文乱码现象是服务端收到问号现象本地日志里 JSON 中文正常服务端入库变成???或乱码。原因通常是StringContent没指定Encoding.UTF8或者服务端按 ISO-8859-1 解析。解决构造StringContent时显式传Encoding.UTF8同时确认请求头Content-Type带charsetutf-8。如果服务端是 Java 写的检查它的RequestBody编码配置。4.2 415 Unsupported Media TypeContent-Type 缺失现象请求发出去服务端直接返回 415。原因StringContent默认媒体类型是text/plain服务端只接受application/json。解决第三个参数写application/json。有些框架还要求Accept头可以在HttpClient.DefaultRequestHeaders里加上Accept: application/json。4.3 连接耗尽循环里 new HttpClient现象程序跑一段时间后报SocketException提示无法连接或端口耗尽。原因在循环或每次请求里new HttpClient()连接池反复创建销毁。解决全局单例或依赖注入配合SocketsHttpHandler的PooledConnectionLifetime。如果确实需要不同配置用IHttpClientFactory创建命名客户端。4.4 超时无响应Timeout 设置不当现象请求卡住很久才报错或者明明服务端很快却报超时。原因HttpClient.Timeout默认 100 秒太长或者用了CancellationToken但没和超时联动。解决根据业务设合理超时一般 10 到 30 秒需要更细粒度控制时用CancellationTokenSource配合SendAsync。4.5 反序列化失败字段类型不匹配现象响应能拿到但JsonSerializer.Deserialize抛JsonException。原因服务端返回的字段类型和模型定义不一致比如code返回字符串0而模型是int。解决用JsonElement接收不确定字段或者自定义JsonConverter做兼容转换。排查时先把原始响应字符串打出来看别急着怀疑网络。5. 二次开发改造把这套代码变成你自己的接口客户端拿到这份代码后最直接的改造是换模型和换地址。但真正让它在项目里站住脚得做几件事。第一把ApiClient里的 URL 抽到配置文件。硬编码的/api/device/status在换环境时要改代码重新编译改成从appsettings.json读配合IConfiguration注入测试环境和生产环境各一份配置。第二加请求日志。在PostAsync前后各打一条日志记录请求体、响应体、耗时。线上出问题时日志是唯一的后悔药。注意别把敏感字段如 token打进日志可以在序列化前做脱敏。第三统一异常处理。定义一个ApiException把HttpRequestException、TaskCanceledException、JsonException都包装进去带上状态码和原始响应。业务层只捕获ApiException根据Code决定重试还是提示用户。第四加重试策略。对于网络抖动导致的失败可以用 Polly 库做指数退避重试。但要注意POST 请求重试可能导致重复提交服务端需要幂等支持否则重试就是灾难。第五写单元测试。用HttpMessageHandler的 mock 实现模拟各种响应200、415、超时、乱码验证ApiClient的行为。这样改代码时才有底气。我自己的习惯是每接一个新接口先用 Postman 或 curl 把请求跑通确认服务端行为再写 C# 代码。代码写完第一件事是看日志里的原始 JSON 和响应而不是直接看业务结果。从那以后我每次对接新接口都强制走一遍“先抓包、再写码、后看日志”的流程省下的排查时间比写代码的时间多得多。希望帮到你。本文还有配套的精品资源点击获取