资讯动态

C#上位机HTTP POST+JSON实战:HttpClient封装与避坑指南

发布时间:2026/9/9 22:33:14 来源:尧图企业网站定制
简介面向C#开发者的Newtonsoft.JsonJson.NET多版本程序集压缩包用于HTTP POST请求与JSON数据交互也适合兼容不同.NET运行环境的项目。包内针对net20、net35、net40、net45、netstandard1.0、netstandard1.3、netstandard2.0及portable等目标框架各提供对应的Newtonsoft.Json.dll可直接引用到旧版Framework或跨平台项目中共27个文件同时包含XML注释文档与PDB调试符号前者便于IDE智能提示后者便于调试序列化/反序列化逻辑时定位问题。该库支持将C#对象序列化为JSON字符串、将响应JSON还原为强类型对象覆盖HttpClient构建POST请求、StringContent指定application/json、异步发送与结果解析等核心数据转换需求。压缩包整体仅6.41MB轻量实用已有1397人学习是集成Json.NET到C#网络通信项目时省时省力的参考工具。 做C#上位机和各类服务端对接绕不开一个组合HTTP POST JSON。无论是给工厂MES系统上报设备数据、调第三方云平台接口还是跟视觉检测设备通信十次联调里有九次都在处理把请求体拼成JSON、PostAsync发出去、再把返回的JSON解析回对象这摊事。这篇文章就从C#开发的实操视角把POSTJSON这套数据交互方式拆开讲透为什么你会选POST而不是GET序列化时哪些坑能避开怎么封装一个带超时和重试的公共请求方法以及我实测过最容易翻车的五个雷区。适合刚入门上位机开发的新人也适合正在跟第三方HTTP接口较劲、需要一份参考方案的C#工程师。1. 为什么是POST JSONGET做不了的事和JSON语言优势1.1 GET和POST不是传参长短的区别很多初学者记GET和POST的区别背的是GET参数放URL后面长度有限制POST参数放请求体里没有限制。这么说没错但只停留在表象。真正决定选型的是语义GET本意是查询不改变服务器状态POST本意是提交告诉服务器有新的数据要处理。放在C#上位机场景里这个区别就很实在了。举个例子你给一台设备下发启动指令这显然不是查询是让设备干活——用POST是对的。如果你要轮询设备当前温度那用GET反而更合理。我曾见过有人把所有请求统统写成POST服务端那边虽然能跑但日志里全是POST记录排错的时候还要猜哪个是查询哪个是控制维护成本凭空多一截。另外还有一层考虑POST请求体的数据不会暴露在URL里安全性比GET好一些。不是说POST加密了而是不会像GET那样把参数留在浏览器历史、服务器访问日志里放在工业内网里反正少一事。1.2 JSON是不同语言之间的通用翻译官C#进程要和远端服务交换数据远端可能是Java、Python、Node.js甚至是一个早就停止维护的老旧C服务。大家谁也认不了谁的二进制对象格式但JSON字符串是所有人都认的。JSON的优势在于它用纯文本表达结构化数据嵌套对象、数组、数字、布尔值都能表示。拿典型的设备上报报文来说{ serialNo: SN-20240101-001, temperature: 35.6, status: running, items: [ { name: a, value: 10 }, { name: b, value: 20 } ] }C#这边定义好对应的类序列化一下发过去对方把这些字段映射到自己的类上就能直接用了。换个平台、换种语言只要JSON的结构不变数据交互就不受任何影响。1.3 上位机里最常见的POSTJSON应用场景我平时接触比较多的三种场景基本覆盖了POSTJSON的绝大多数使用面设备状态上报上位机周期性地把采集到的温度、压力、运行时长等数据拼成JSON发给监控服务器。控制指令下发服务端或MES系统下发工单、启动参数给上位机上位机解析JSON后执行动作并回传结果。对接第三方Web API云平台、告警推送、数据中台这些外部系统清一色是POST JSON的Restful接口。这三种场景的共同点是数据结构相对复杂、对实时性有要求、且需要服务器返回处理结果。理解了POSTJSON为什么是默认解法后面选工具和写封装才有基础。2. HttpClient、HttpWebRequest、RestSharp三种POST写法的取舍2.1 HttpClient.NET官方推荐的现代方案现在写C#POST JSON首选就是HttpClient这是System.Net.Http命名空间下的类。它的优势很明确原生支持async/await异步模型内部有连接池管理一个实例可以复用资源开销小。基本用法如下using System.Text; using System.Text.Json; string url http://192.168.1.100:8080/api/devices/report; var payload new { serialNo SN-001, temperature 35.6, status running }; string json JsonSerializer.Serialize(payload); using var content new StringContent(json, Encoding.UTF8, application/json); using var httpClient new HttpClient(); httpClient.Timeout TimeSpan.FromSeconds(5); HttpResponseMessage resp await httpClient.PostAsync(url, content); string respJson await resp.Content.ReadAsStringAsync(); Console.WriteLine($状态码: {resp.StatusCode}); Console.WriteLine($响应内容: {respJson});有人会问JsonSerializer.Serialize直接把匿名类型序列化成JSON那StringContent第三个参数application/json是干嘛的这是告诉服务器我这body里的内容是JSON格式。服务器端会根据Content-Type来选合适的解析器写错会报415之类的错误。编码指定UTF-8也很有讲究放在后面坑的章节细说。2.2 HttpWebRequest老项目里的老朋友HttpWebRequest是很老牌的实现跟HttpClient比代码啰嗦每次请求还要自己管流、管编码、管释放。但它有个场景还在用——维护历史遗留项目.NET Framework老代码里大量存在不能因为你新项目用HttpClient就全推翻。它的POST写法长这样var request (HttpWebRequest)WebRequest.Create(url); request.Method POST; request.ContentType application/json; charsetutf-8; request.Timeout 5000; byte[] bytes Encoding.UTF8.GetBytes(json); using (var stream request.GetRequestStream()) { stream.Write(bytes, 0, bytes.Length); } using (var response (HttpWebResponse)request.GetResponse()) using (var reader new StreamReader(response.GetResponseStream(), Encoding.UTF8)) { string result reader.ReadToEnd(); Console.WriteLine(result); }核心区别在于HttpWebRequest是直接调I/O流你不主动写请求体就是空的HttpClient封装了这些底层繁琐细节。新代码里没必要自讨苦吃。2.3 RestSharp快速交付时的第三方选项RestSharp是一个流行的第三方HTTP客户端库语法简洁甚至可以这样一行发出请求var client new RestClient(url); var request new RestRequest(api/devices/report, Method.Post); request.AddJsonBody(payload); var response client.Execute(request);它做了一些便利封装比如AddJsonBody自动序列化、自动设置Content-Type。但在现代.NET里官方HttpClient完全够用多引一个第三方库意味着多一份依赖和多一层序列化黑盒。我的建议是内部工具、快速原型可以用RestSharp正式项目还是用HttpClient出了问题你能直接追到系统库源码。三种方式放在一起对比大概是这样对比项HttpClientHttpWebRequestRestSharp语法简洁度简洁繁琐最简洁异步支持原生支持支持但写法老支持连接复用内置连接池无基于HttpClient依赖系统自带系统自带第三方NuGet包适用场景新项目、正式项目老项目维护原型、快速交付2.4 结论默认选HttpClient统一封装才省心我现在的项目里不管底层的调用目标是谁都会单独封装一个HTTP工具类内部统一用HttpClient。这么做好处很明显所有请求的超时时间、重试策略、日志打印都在一个文件里改不用每个地方散落一份PostAsync代码。后面第4节就有一个可以直接抄的工具类。3. 序列化与反序列化的坑字段大小写、数字精度、嵌套结构3.1 System.Text.Json和Newtonsoft.Json怎么选自 .NET Core 3.0 起官方System.Text.Json的性能就很好而且是内置的。Newtonsoft.Json也就是JSON.NET是以前的事实标准生态成熟第三方接口文档里很多例子都用它。我现在的选择准则是新项目用System.Text.Json老项目若已经引了Newtonsoft.Json就继续用它不必强行迁移。两者在发POST请求这个环节的常见操作对比如下// System.Text.Json string json JsonSerializer.Serialize(payload); T result JsonSerializer.DeserializeT(respJson); // Newtonsoft.Json string json JsonConvert.SerializeObject(payload); T result JsonConvert.DeserializeObjectT(respJson);3.2 字段大小写对不上的经典问题C#里类属性默认是PascalCase比如SerialNumber而第三方接口的JSON字段常常是camelCaseserialNumber或snake_caseserial_number。不配置直接序列化发给对方的数据字段名就对不上对方反序列化的结果全是null。解决方式是在序列化选项里指定命名策略var options new JsonSerializerOptions { PropertyNamingPolicy JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive true }; string json JsonSerializer.Serialize(payload, options);PropertyNameCaseInsensitive是给反序列化用的意思是对方返回的字段名大小写跟我类的属性不完全一致时也能匹配上。这两个配置加一起能省掉很多字段对不齐的排查时间。3.3 数字精度丢数据是最隐蔽的坑上位机上报的数据经常是浮点数比如温度35.6、电压220.5。序列化时 .NET 默认用double但如果对接的系统是Java JavaScript前端JavaScript的Number类型对超过一定长度的整数会丢精度。比如设备ID如果是一个19位的大整数序列化成JSON后JS那边拿到的末尾几位可能就是错的。解决思路有两个方向一是对这类长整型字段在DTO里声明为string类型字符串序列化出去精度一点都不丢二是用JsonSerializerOptions里的NumberHandling属性来处理特殊序列化行为。我在对接第三方系统时凡是ID类字段一律用string这是一个用血泪换来的习惯。3.4 嵌套对象和数组的建模之前给的那段设备报文里items是个数组。C#建模时对应的是List 或数组public class DeviceReport { public string SerialNo { get; set; } public double Temperature { get; set; } public string Status { get; set; } public ListItemData Items { get; set; } } public class ItemData { public string Name { get; set; } public int Value { get; set; } }然后反序列化DeviceReport report JsonSerializer.DeserializeDeviceReport(respJson, options);一个建议DTO类只放字段不写业务逻辑避免序列化时把无关属性搞进去。很多坑都是因为把实体类和DTO混用一个类里又挂数据库字段又挂界面字段序列化结果一团糟。4. 一个可以直接抄的POSTJSON工具类含超时、重试、日志4.1 工具类的完整代码下面的HttpJsonClient静态类是我做上位机对接时实际在用的一个简化版本把超时、重试、日志、同步/异步封装都考虑进去了using System; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading; public static class HttpJsonClient { // 静态实例全局复用连接池避免频繁 new HttpClient 导致端口耗尽 private static readonly HttpClient httpClient new HttpClient { Timeout TimeSpan.FromSeconds(5) }; /// summary /// POST JSON并返回响应字符串 /// /summary public static string Post(string url, object body, int timeoutSeconds 5) { httpClient.Timeout TimeSpan.FromSeconds(timeoutSeconds); string json JsonSerializer.Serialize(body); using var content new StringContent(json, Encoding.UTF8, application/json); HttpResponseMessage response httpClient.PostAsync(url, content).GetAwaiter().GetResult(); string result response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); // 记录日志方便联调 Console.WriteLine($[HTTP POST] {url}); Console.WriteLine($[请求体] {json}); Console.WriteLine($[状态码] {(int)response.StatusCode}); Console.WriteLine($[响应体] {result}); return result; } /// summary /// POST JSON并反序列化为指定类型 /// /summary public static T PostT(string url, object body, int timeoutSeconds 5) { string result Post(url, body, timeoutSeconds); return JsonSerializer.DeserializeT(result); } /// summary /// 带重试的POST应对网络抖动 /// /summary public static string PostWithRetry(string url, object body, int retryCount 3, int timeoutSeconds 5) { Exception lastEx null; for (int i 0; i retryCount; i) { try { return Post(url, body, timeoutSeconds); } catch (Exception ex) { lastEx ex; Console.WriteLine($[重试] 第{i 1}次失败: {ex.Message}); Thread.Sleep(1000 * (i 1)); } } throw lastEx; } }4.2 为什么这样设计先说HttpClient的static声明。很多人踩过的坑是每次请求都new一个HttpClient短时间高并发下会出现大量TIME_WAIT状态的TCP连接最后SocketException报地址已被使用。HttpClient设计上就是可复用的底层维护了连接池全局保留一个实例是标准做法。再说同步/异步。上位机程序很多是在线程池线程里跑轮询任务或者处理扫码枪触发事件不方便到处搞async/await。这里用.GetAwaiter().GetResult()代替.Wait()是为了避免在UI线程上触发死锁——这一点下一节详细解释。重试逻辑里我用了Thread.Sleep简单粗暴但配合3次重试基本够用。更精细的做法是加指数退避重试比如第一次等1秒、第二次等2秒、第三次等4秒这个可以根据接口的响应时间自行调整。4.3 日志和调试的小细节工具类里每次请求都在控制台打印URL、请求体、状态码、响应体。这做法看着简单在对接第三方接口时帮了大忙。对方说我没收到你的数据你本地看日志就知道是根本没发出去还是发了但格式不对还是对方返回了500。联调扯皮时一句我这边请求体是这样你那边收到的也一样吗能省几个小时。如果项目里已经有NLog、Serilog之类的日志框架把Console.WriteLine替换成对应组件即可思路不变。5. 实测中最容易翻车的五个雷区与排查经验5.1 中文全部变成问号StringContent编码没指定这是POST JSON最常见的新手问题。有人写new StringContent(json)就把请求发出去结果服务端收到的中文全是???。原因在于StringContent默认的ContentType是text/plain编码走的是默认编码而不是UTF-8。正确写法必须给全三个参数using var content new StringContent(json, Encoding.UTF8, application/json);这样请求头才会带上Content-Type: application/json; charsetutf-8服务端按UTF-8解码中文就正常了。这个坑我见得太多了如果你发现服务端收到的中文乱码先检查这一行。5.2 界面卡死但程序没崩溃是异步死锁不是卡了在WinForm或WPF的UI线程里如果你直接写var response httpClient.PostAsync(url, content).Result;程序会死锁。原因解释起来也不复杂UI线程在等待异步任务返回而异步任务恢复时又要回到UI线程上执行后续代码两边互相等谁也等不到对方。网上很多老教程用.Result或.Wait()新人照抄就踩坑。我给出的工具类里用的是.GetAwaiter().GetResult()它不会捕获SynchronizationContext做上下文切换所以不会死锁。当然最规范的方案是把整个调用链都改成async/await但上位机涉及大量事件回调在不改整体架构时用这个方法最稳。遇到按钮一按整个窗体卡白的问题优先排查是不是这里。5.3 服务端说收到空body请求体被分块传输或代理吞了还有一种诡异情况本地测试一切正常部署到客户现场后对方服务端一直反馈收到空body。后来抓包排查发现是透明代理或某些中间件对HttpClient默认的Transfer-Encoding: chunked处理有问题导致body分块后对方没正确解析。排查思路很简单抓包看实际发出的报文确认请求头里是Content-Length还是Transfer-Encoding。如果是chunked导致的问题可以强制走HTTP/1.1或调整连接代理设置httpClient.DefaultRequestHeaders.ConnectionClose true;这个设置会要求在请求完成后关闭TCP连接避免连接复用渠道里混入异常代理状态。不过它会影响性能建议只在确认是代理问题时才加。5.4 本地正常、现场不通代理和证书的隐忧上位机部署到客户现场经常遇到我在办公室调得好好的到现场就不通的情况。两个最典型的元凶一个是系统全局开启了HTTP代理HttpClient默认会走系统代理而现场代理网关只放行特定域名导致你连内网IP都被拦。解决方法是关闭代理HttpClientHandler handler new HttpClientHandler { UseProxy false };另一个是HTTPS证书问题。如果目标服务用的是自签名证书或者现场网络做了SSL解密HttpClient会因证书验证失败拒绝连接。调试阶段可以临时用自定义的ServerCertificateCustomValidationCallback放行但正式环境要谨慎别为绕过证书把安全问题带进来。5.5 频繁请求导致端口耗尽还是new HttpClient的锅这个在4.2提过但因为太典型专门列出来。现象是你的程序运行一段时间后会出现连接不上、报错Only one usage of each socket address查看系统网络连接能看到大量TIME_WAIT状态的连接堆着。根因就是每次请求都new HttpClient旧连接没有被连接池复用处于TIME_WAIT要等2分钟左右才释放高频率请求下端口就耗尽了。修复方式就是本文工具类里的做法HttpClient做成静态全局实例。如果你在代码审查时看到有人循环里new HttpClient可以直接把这篇文章发给他看。最后分享一个小习惯做任何POSTJSON联调重点不在会写而在会看。装上Fiddler或者用Wireshark抓一下包把请求头、请求体、响应体摊开看一遍大部分疑难杂症十分钟内都能定位出来。我虽然写了这么多年调用代码遇到三方对接问题还是先抓包再改代码切莫凭感觉瞎猜——报文不会骗人。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价