简介这份资源面向使用C#进行网络通信开发的程序员聚焦HTTP POST协议下以JSON格式完成数据交互这一常见场景帮助解决请求构建、序列化与反序列化、异常处理等实际问题。压缩包共27个文件以9个dll动态库、9个xml文档和9个pdb调试符号为主整体约6.41MB其中dll为可直接引用的程序集xml提供API注释说明pdb便于调试定位覆盖net20至netstandard2.0等多个目标框架版本。资源围绕HttpClient发送POST请求、Json.NET序列化对象、SendAsync异步接收响应、JsonConvert反序列化结果以及try-catch错误处理等要点展开适合初、中级开发者对照学习。目前已有1399人学习下载可作为搭建稳定高效网络通信模块的参考素材。1. 从一次接口联调翻车说起C# 里 POST JSON 到底难在哪上周帮一个做上位机的小团队看接口问题现象很典型用 C# 写HttpClient往服务端 POST 一段 JSON本地调试返回 200部署到现场机器就间歇性 400日志里服务端说“请求体为空”。代码翻出来一看StringContent没指定Encoding.UTF8也没设Content-Type本地因为默认编码恰好对上了现场机器区域设置一变就翻车。这类问题在 C# 网络协议交互里太常见了——不是不会写而是不知道哪几个参数在什么环境下会咬人。这篇笔记围绕「C# 用 HTTP POST 做 JSON 数据交互」这条主线把请求构造、序列化选型、异步调用、异常排查、批量压测这几件事拆开讲。适合正在做服务端对接、上位机通信、微服务之间传数据的 C# 开发者尤其是那种“能跑但不敢上生产”的代码。下面所有示例都基于 .NET 6 的HttpClient和System.Text.Json老项目用HttpWebRequest或Newtonsoft.Json的我会在对应位置标出差异。2. 请求构造HttpClient 发 JSON 的四个关键参数2.1 为什么优先用 HttpClient 而不是 HttpWebRequestHttpWebRequest是 .NET Framework 时代的老接口能用但每个请求都要手动管理连接、流和释放写多了容易漏Dispose。HttpClient从 .NET 4.5 引入核心优势是连接复用——它内部维护HttpMessageHandler和连接池同一个实例反复发请求不会每次重新握手。这一点在批量 POST JSON 的场景下差别很明显我实测过 1000 次请求复用HttpClient比每次new一个快将近 3 倍而且不会出现端口耗尽。但HttpClient有个经典坑不要用using包着每次 new。很多人照着using (var client new HttpClient())写结果高并发下 TIME_WAIT 堆积。正确做法是静态单例或者用IHttpClientFactory注入。控制台小工具用静态字段就够ASP.NET Core 项目走工厂。// 推荐静态单例整个应用生命周期复用一个实例 private static readonly HttpClient _client new HttpClient { // 全局超时别用默认的 100 秒生产环境一般 10~30 秒 Timeout TimeSpan.FromSeconds(15) };参数说明Timeout是整次请求含连接、发送、读取的总超时不是单次读超时。如果服务端处理慢但连接正常设太小会误杀。需要更细粒度控制就用CancellationToken配合SendAsync。2.2 StringContent 的三个参数编码、媒体类型、序列化POST JSON 最核心的一行就是构造StringContent。它有三个参数内容字符串、编码、媒体类型。三个都不能省。string json {\deviceId\:\A-001\,\temperature\:36.5}; // 正确写法显式指定 UTF-8 和 application/json var content new StringContent(json, Encoding.UTF8, application/json); var response await _client.PostAsync(https://api.example.com/report, content); string result await response.Content.ReadAsStringAsync();逻辑说明Encoding.UTF8决定了字符串转字节时的编码不指定的话默认也是 UTF-8但显式写出来能避免跨平台差异。第三个参数application/json会设置Content-Type头服务端框架比如 ASP.NET Core 的[FromBody]靠这个头决定用哪个格式化器解析。少了它很多服务端直接返回 415 Unsupported Media Type。提示如果 JSON 里有中文编码必须是 UTF-8。用 GB2312 或 Default 编码发出去服务端按 UTF-8 解析就是乱码这种问题在日志里表现为“字段值变成问号”。2.3 用对象序列化代替手拼字符串手拼 JSON 字符串在字段少的时候还行字段一多就容易漏引号、漏逗号而且特殊字符不转义。常见做法是定义 DTO 类用System.Text.Json序列化。public class DeviceReport { public string DeviceId { get; set; } public double Temperature { get; set; } public DateTime Timestamp { get; set; } } var report new DeviceReport { DeviceId A-001, Temperature 36.5, Timestamp DateTime.UtcNow }; // 序列化选项驼峰命名、忽略 null、时间用 ISO 8601 var options new JsonSerializerOptions { PropertyNamingPolicy JsonNamingPolicy.CamelCase, DefaultIgnoreCondition JsonIgnoreCondition.WhenWritingNull, WriteIndented false }; string json JsonSerializer.Serialize(report, options); var content new StringContent(json, Encoding.UTF8, application/json);参数说明PropertyNamingPolicy.CamelCase把 C# 的DeviceId转成 JSON 的deviceId这是前后端对接最常见的命名约定。DefaultIgnoreCondition控制 null 值是否输出服务端如果对 null 敏感就设WhenWritingNull。WriteIndented生产环境设 false省带宽调试时可以设 true 方便看日志。老项目用Newtonsoft.Json的话对应写法是JsonConvert.SerializeObject(report, new JsonSerializerSettings { ContractResolver new CamelCasePropertyNamesContractResolver() })逻辑一样只是 API 不同。3. 异步调用与响应处理别让 .Result 把线程锁死3.1 async/await 的正确姿势与同步调用的代价HttpClient的所有网络方法都有异步版本PostAsync、SendAsync、ReadAsStringAsync。在 UI 线程或 ASP.NET 请求线程里必须用await不能用.Result或.Wait()。原因很直接.Result会阻塞当前线程等任务完成如果当前线程持有同步上下文UI 线程、ASP.NET 经典管道而任务完成后的续体又要回到这个上下文就死锁了。现象是界面卡死或请求永远不返回。血泪经验我见过一个 WinForm 项目按钮点击里写client.PostAsync(...).Result点一次卡一次最后只能杀进程。// 正确全链路 async public async Taskstring PostJsonAsync(string url, object payload) { var json JsonSerializer.Serialize(payload); var content new StringContent(json, Encoding.UTF8, application/json); using var response await _client.PostAsync(url, content); response.EnsureSuccessStatusCode(); // 非 2xx 直接抛异常 return await response.Content.ReadAsStringAsync(); }逻辑说明EnsureSuccessStatusCode在状态码不是 2xx 时抛HttpRequestException比手动判断IsSuccessStatusCode更简洁。using var确保响应对象释放但注意HttpResponseMessage释放后Content就读不到了所以要先读完再释放。3.2 反序列化响应处理大小写和缺失字段服务端返回的 JSON 字段名可能和 DTO 不一致比如服务端用下划线device_id客户端用驼峰。System.Text.Json默认大小写敏感Newtonsoft默认不敏感这个差异经常导致“字段全是 null”。var readOptions new JsonSerializerOptions { PropertyNameCaseInsensitive true, // 大小写不敏感 PropertyNamingPolicy JsonNamingPolicy.CamelCase }; var apiResult JsonSerializer.DeserializeApiResponse(result, readOptions);如果字段名差异大用[JsonPropertyName(device_id)]特性逐个映射。缺失字段默认给类型默认值string 为 nullint 为 0如果业务上不能接受默认值就在反序列化后做校验别指望序列化器帮你兜底。3.3 超时、重试与 CancellationToken 的组合生产环境的网络请求不能只靠全局Timeout。常见做法是给每个请求传CancellationToken配合CancellationTokenSource做超时和取消。using var cts new CancellationTokenSource(TimeSpan.FromSeconds(8)); try { var response await _client.PostAsync(url, content, cts.Token); // 处理响应 } catch (TaskCanceledException) when (cts.IsCancellationRequested) { // 超时或主动取消记录日志 }参数说明CancellationTokenSource的构造函数参数是超时时间到点自动取消。TaskCanceledException在超时和主动取消时都会抛用when过滤区分。重试策略不要自己写循环用Polly库的WaitAndRetryAsync指数退避避免雪崩。4. 避坑与排查五个让 POST JSON 翻车的细节4.1 现象服务端收到空 body客户端返回 400原因StringContent没设Content-Type或者设成了text/plain。某些服务端框架如 Spring MVC 的RequestBody严格要求application/json否则不解析 body。解决构造StringContent时第三个参数必须写application/json。如果已经构造了 content可以手动加content.Headers.ContentType new MediaTypeHeaderValue(application/json);。4.2 现象中文变乱码英文正常原因编码不是 UTF-8。可能是StringContent没指定编码或者项目文件保存时用了 GB2312或者服务端按 ISO-8859-1 解析。解决客户端显式Encoding.UTF8服务端确认请求解析编码抓包看原始字节确认Content-Type里有没有charsetutf-8。4.3 现象高并发下报“无法连接到远程服务器”或端口耗尽原因每次请求new HttpClient()连接不复用TIME_WAIT 状态堆积。解决静态单例或IHttpClientFactory。如果必须多实例至少复用HttpMessageHandler。4.4 现象反序列化后对象字段全是 null原因字段名大小写不匹配或者 JSON 结构嵌套层级和 DTO 不一致。解决PropertyNameCaseInsensitive true用[JsonPropertyName]显式映射先用JsonDocument.Parse打印原始结构确认层级。4.5 现象请求发出去了但服务端说没收到客户端也没报错原因PostAsync返回了但没读response.Content或者response被提前释放。还有一种情况是HttpClient的BaseAddress和相对路径拼接出错请求发到了错误地址。解决始终await response.Content.ReadAsStringAsync()读完再释放用 Fiddler 或HttpClient的Logging中间件抓实际请求 URL 和 body。5. 进阶批量压测与性能验证的实操技巧5.1 用 SemaphoreSlim 控制并发数批量 POST JSON 时无限制并发会把服务端打挂也会让客户端线程池饥饿。常见做法是用SemaphoreSlim限制同时进行的请求数。private static readonly SemaphoreSlim _semaphore new SemaphoreSlim(10); // 最多 10 并发 public async Task PostBatchAsync(ListDeviceReport reports) { var tasks reports.Select(async report { await _semaphore.WaitAsync(); try { var json JsonSerializer.Serialize(report); var content new StringContent(json, Encoding.UTF8, application/json); var response await _client.PostAsync(https://api.example.com/report, content); response.EnsureSuccessStatusCode(); } finally { _semaphore.Release(); } }); await Task.WhenAll(tasks); }参数说明SemaphoreSlim(10)的 10 是并发上限根据服务端承载能力和客户端资源调整。WaitAsync在信号量满时异步等待不阻塞线程。finally里Release确保异常时也能释放否则信号量泄漏会导致后续请求永久等待。5.2 用 Stopwatch 和计数器验证吞吐压测不能只看“跑完了”要看耗时和成功率。我一般会在批量任务前后加Stopwatch并统计成功/失败数。var sw Stopwatch.StartNew(); int success 0, fail 0; var tasks reports.Select(async report { await _semaphore.WaitAsync(); try { // ... 发送请求 Interlocked.Increment(ref success); } catch { Interlocked.Increment(ref fail); } finally { _semaphore.Release(); } }); await Task.WhenAll(tasks); sw.Stop(); Console.WriteLine($总数 {reports.Count}成功 {success}失败 {fail}耗时 {sw.ElapsedMilliseconds} ms);逻辑说明Interlocked.Increment保证多线程下计数准确直接success会有竞态。耗时除以总数得到平均单次耗时如果远大于单次请求的Timeout说明并发控制或服务端处理有问题。5.3 验证 JSON 结构是否被服务端正确接收最可靠的验证方式是服务端回显。如果服务端支持在响应里返回接收到的字段摘要如果不支持用抓包工具看原始请求体。另一个技巧是在客户端序列化后先打印 JSON 字符串和抓包结果对比确认没有被中间件改写。注意生产环境打印完整 JSON 可能泄露敏感字段日志里只打字段名和长度别打值。从那以后我每次写 POST JSON 的代码都会强制走一遍检查清单HttpClient是不是单例、StringContent有没有 UTF-8 和application/json、异步链路有没有.Result、反序列化有没有处理大小写、批量请求有没有并发限制。这五条过一遍基本不会再出联调事故。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?