简介:实现C# HTTP POST的JSON数据交互,是许多.NET开发者会遇到的典型场景。这套资料面向需要对接Web API、构建客户端通信模块的C#程序员,系统梳理了HttpClient使用、POST请求构建、异步发送与响应读取,以及Json.NET序列化/反序列化等核心步骤。包内共27个文件,以Newtonsoft.Json各目标框架的dll为主,并包含对应XML注释与PDB调试符号,覆盖net20至netstandard2.0等多个版本,压缩包约6.41MB,便于不同项目直接选用。同时给出常见异常处理与跨域、HTTPS注意事项,能帮助读者快速掌握从发送请求到解析响应的完整链路。已有1397人学习下载,适合希望提升网络编程能力或快速集成RESTful接口的开发者参考。 最近在做一个 C# 上位机项目,设备的数据要往服务端报,服务端那边接口定得比较死:HTTP POST 协议,数据交互格式是 JSON。刚听起来挺简单,真正写起来才发现坑不少——编码、超时、序列化、连接复用,任何一个地方没处理好都会让你调试到怀疑人生。这篇不整虚的,就把我这次用 C# 调 HTTP POST JSON 接口的完整思路、代码和踩坑记录都捋一遍,给后面做同类需求的兄弟省点时间。如果你是刚接触 C# 上位机开发、或者要对接第三方 HTTP 接口但之前一直用 WebService/Socket,这篇对你有参考价值。
1. 先搞清楚:为什么偏偏是 HTTP POST 配 JSON
1.1 POST 和 GET 的本质区别
很多初学者容易把 POST 和 GET 当成“一个传参更安全、一个更直观”的关系,其实没那么简单。GET 请求的参数是拼在 URL 后面的,比如http://192.168.1.100/api/data?id=1&name=abc,这种方式有几个天生的问题:URL 长度有限制,超长参数会被截断或者直接被服务端拒绝;参数直接暴露在 URL 里,容易出现在访问日志中,谈不上什么安全性;而且 GET 本身语义是“查询”,拿它去上报数据,在 REST 风格接口里语义是拧巴的。
POST 请求则把数据放在请求体(Body)里,能承载的数据量要大得多,而且结构可以很复杂——不仅仅是几个键值对,还可以是嵌套的对象、数组、大段文本。拿我这次的上位机场景来说,一条生产数据要包含设备编号、操作员、物料批次、时间戳,还有一组质量检测结果数组,这种结构用 GET 的键值对会非常别扭,用 POST 把整个 JSON 对象塞进 Body 才合理。顺便说一句,很多人以为 POST 比 GET 安全,其实它只是解决了“参数放在哪”的问题,真正的安全还得靠 HTTPS 和服务端校验,这一点别搞混。
1.2 JSON 格式为什么成了主流选择
早年做接口对接,常见的数据格式是 XML,或者干脆是自定义的a=1&b=2这种表单结构。XML 表达能力强,但标签冗余太多,一个简单的数据能写出一大坨,解析也费劲。表单结构又太扁平,表达不了嵌套关系。JSON 恰好卡在中间:语法简洁,人眼可读,嵌套能力强,主流语言基本都内置了解析库,性能也好。
做 C# 开发,JSON 还有一个额外的好处——和对象的互转非常方便。你在代码里定义好实体类,然后用序列化工具一行代码就能把对象变成 JSON 字符串;服务端返回的 JSON,也能一行代码反序列化成 List、Dictionary 或者强类型对象。这种体验是 XML 时代不敢想的。所以现在对接第三方接口,十有八九就是“POST + JSON”,这已经成了事实上的行业惯例。
2. C# 里发起 POST 请求的两种主流写法
2.1 HttpWebRequest:老代码里最常见的传统写法
很多遗留项目或者网上搜到的老教程,用的都是 HttpWebRequest,这也是 .NET Framework 时代的标准写法。核心步骤大概是这样:
string url = "http://192.168.1.100/api/report"; string json = "{\"deviceId\":\"DEV001\",\"result\":\"ok\"}"; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.Method = "POST"; request.ContentType = "application/json; charset=utf-8"; request.Timeout = 10000; byte[] postData = Encoding.UTF8.GetBytes(json); request.ContentLength = postData.Length; using (Stream stream = request.GetRequestStream()) { stream.Write(postData, 0, postData.Length); } using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8)) { string result = reader.ReadToEnd(); Console.WriteLine(result); } }这段代码能用,但有几个明显的槽点:每次请求都要手动创建流、写字节、管理响应流,代码量偏大;遇到异常还得自己处理 WebException 才能拿到错误状态码;另外 GetResponse 在没有返回时可能直接抛超时异常,调试起来不够友好。我见过不少老项目里同一套 POST 逻辑被复制了十几遍,每次复制都略有不同,维护起来特别头疼。
2.2 HttpClient:新项目应该优先选择的现代方案
如果你现在新起一个 .NET Core / .NET 5+ 的项目,首选一定是 HttpClient。它在 API 设计上比 HttpWebRequest 清爽得多,异步支持也更顺手,配合await写出来的代码几乎是自解释的。同样一个上报逻辑,HttpClient 版本长这样:
using (HttpClient client = new HttpClient()) { client.Timeout = TimeSpan.FromSeconds(10); StringContent content = new StringContent(json, Encoding.UTF8, "application/json"); HttpResponseMessage resp = await client.PostAsync(url, content); string result = await resp.Content.ReadAsStringAsync(); Console.WriteLine(result); }代码量少了一大半,而且不用担心流关闭这种琐碎问题。核心注意点就一个:HttpClient 不能每次请求都new一个,它是为复用设计的,底层维护了连接池(这个后面单独讲)。
2.3 两种方案的选型建议
简单说,我的建议是:新项目一律 HttpClient;老框架必须跑在 .NET Framework 4.x 上,如果升级 NuGet 包不麻烦,也建议用 HttpClient(System.Net.Http 这个包在 Framework 上也能用);只有实在没法引包的极端环境才继续用 HttpWebRequest。从 HttpWebRequest 迁移到 HttpClient 的成本很低,接口语义差不多,但维护体验差很多。
3. 直接能抄的通用 POST JSON 方法封装
3.1 万能 PostJsonAsync 方法
为了不在项目里到处复制粘贴,我习惯把“POST JSON + 读取响应”封装成一个通用方法,后面所有地方都调它。这里给一份我实际在用的版本,做了超时控制、异常包装和状态码校验:
using System; using System.Net.Http; using System.Text; using System.Threading.Tasks; public static class HttpJsonClient { private static readonly HttpClient _httpClient = new HttpClient(); public static async Task<string> PostJsonAsync(string url, string json, int timeoutSeconds = 10) { using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds))) { var content = new StringContent(json, Encoding.UTF8, "application/json"); HttpResponseMessage resp; try { resp = await _httpClient.PostAsync(url, content, cts.Token); } catch (TaskCanceledException) { throw new TimeoutException($"请求超时,url: {url}"); } catch (HttpRequestException ex) { throw new HttpRequestException($"请求异常: {ex.Message}", ex); } string body = await resp.Content.ReadAsStringAsync(); if (!resp.IsSuccessStatusCode) { throw new HttpRequestException( $"HTTP {(int)resp.StatusCode} {resp.ReasonPhrase}, url: {url}, body: {body}"); } return body; } } }这里有几个细节值得说明。第一,_httpClient是静态字段,全局就一个实例,因为 HttpClient 内部有连接池,反复new会导致 TCP 连接无法复用,高并发时甚至会耗尽端口。第二,超时用CancellationTokenSource而不是直接赋值_httpClient.Timeout,好处是可控范围更精准,比如取消只针对这一次请求,不会因为改了全局超时影响别的调用。第三,状态码校验用了IsSuccessStatusCode,这样 200 和 204 都会被正确视为成功,服务端返回 400、401、500 时你能拿到状态码和响应体,排查问题快得多。
3.2 JSON 序列化:System.Text.Json 还是 Newtonsoft.Json
序列化这块,老项目大量用的是 Newtonsoft.Json(也就是 Json.NET),新项目里 .NET Core 3.0 以后微软自带的 System.Text.Json 也完全够用。两者的核心用法差别不大:
// Newtonsoft.Json string json = JsonConvert.SerializeObject(reportData); ReportData data = JsonConvert.DeserializeObject<ReportData>(json); // System.Text.Json string json = JsonSerializer.Serialize(reportData); ReportData data = JsonSerializer.Deserialize<ReportData>(json);我个人的选型倾向是:如果是新项目,优先 System.Text.Json,性能更好、和运行时版本绑定,不需要额外引包;如果项目里其他地方已经在用 Newtonsoft.Json,那就保持一致,没必要混两套。一个容易踩的坑是属性名大小写的问题。System.Text.Json 默认是大小写敏感匹配,如果服务端返回的字段是device_id,而你的实体类是DeviceId,直接反序列化结果是空的。解决办法有两个:一是给属性加[JsonPropertyName("device_id")](System.Text.Json 风格)或[JsonProperty("device_id")](Newtonsoft 风格),二是在序列化选项里设置PropertyNameCaseInsensitive = true。同样,序列化时默认输出的是实体的属性名,你可以通过命名策略或属性特性统一改成驼峰。
3.3 请求头、超时与 HTTPS 细节
通用方法只能覆盖 80% 场景,剩下 20% 往往卡在请求头上。比如很多接口要求带 Token 认证,常见做法是在请求头里加Authorization: Bearer xxxx,或者是自定义的X-Api-Key。HttpClient 加头很简单:
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); // 或者追加自定义请求头 _httpClient.DefaultRequestHeaders.Add("X-Api-Key", "abcdef123456");如果是在方法内部动态加头(比如每次请求 Token 不一样),就要用HttpRequestMessage来构造,而不是直接 PostAsync:
var req = new HttpRequestMessage(HttpMethod.Post, url); req.Headers.Add("X-Api-Key", currentApiKey); req.Content = new StringContent(json, Encoding.UTF8, "application/json"); HttpResponseMessage resp = await _httpClient.SendAsync(req);还有一个高频场景是 HTTPS。本地测试环境经常是自签名证书,直接请求会报“SSL 证书校验失败”。这个问题的正规解法是让现场把正式证书部署好,但开发调试阶段可以临时跳过校验:
HttpClientHandler handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true }; HttpClient client = new HttpClient(handler);注意这代码只能用在测试环境,上线前一定要改回来,否则等于把 HTTPS 建立在裸奔基础上,这个责任得拎清楚。
4. 实际开发中高频踩坑与排查实录
4.1 中文变乱码,服务端读不到字段
这是 POST JSON 最常见的问题,没有之一。它的根源往往不在 JSON 序列化,而在请求体的编码上。比如有人写StringContent时没指定编码:
// 错误示范:默认可能不是 UTF-8,中文服务端解析出来是乱码 StringContent content = new StringContent(json);正确写法是显式指定 UTF-8 和 media type:
StringContent content = new StringContent(json, Encoding.UTF8, "application/json");用 HttpWebRequest 的老代码同理,写请求流的时候Encoding.UTF8.GetBytes(json),读响应的时候StreamReader(stream, Encoding.UTF8),统一走 UTF-8,不要用系统默认编码。判断这个问题的快捷方法:在服务端日志里看你收到的 Content-Type,如果显示application/json; charset=iso-8859-1或者根本没带 charset,那大概率就是客户端没指定编码。
4.2 接口返回 400/415,JSON 格式对不对?
先区分两个常见状态码:400 是“Bad Request”,服务端压根解析不了你发的东西;415 是“Unsupported Media Type”,说明你 Content-Type 不是application/json。遇到这两类错误,我的排查顺序很固定。第一步,先拿 Postman、Apifox 或者在线 POST 工具把接口调通,确认接口本身逻辑没问题;第二步,把 C# 代码里实际发送的 JSON 字符串打出来,放到 JSON 格式化工具里检查一下——很多时候是序列化出来的 JSON 里某个字段类型不对,比如服务端要 int,你传了字符串,或者缺少必填字段。第三步,检查 Content-Type 是不是application/json; charset=utf-8,这一步能排除一半以上的 415。
另外提醒一句:现在不少接口框架对 JSON 解析失败会返回非常直白的错误提示,比如我在一份错误日志里看到过类似Failed to deserialize the JSON body into the target type: missing field的信息。这种信息就是金矿,直接告诉你是字段缺失了,对着实体类一个个排查很快就能定位,不用瞎猜。
4.3 请求一直卡住或者偶发超时
偶发超时在 C# 里是个特别容易让人心态炸裂的问题,因为你本地调试可能一切正常,一到现场高强度跑就间歇性超时。这背后通常有两个原因。第一,HttpClient 没有复用,每次请求都 new 一个新实例,旧实例的 TCP 连接要等待超时才会释放,短时间大量请求会耗尽可用端口,表现就是偶发超时。解决办法就是全局复用 HttpClient;如果并发量真的很大,可以考虑用IHttpClientFactory做更精细的连接管理。第二,服务端处理不过来,尤其是一次请求里带了超大数组的数据,服务端序列化耗时增加,客户端默认 100 秒超时虽然不短,但如果你自己设置了只有 5 秒,高峰期就可能超。另外在 Windows 上,ServicePointManager.DefaultConnectionLimit默认值偏保守,如果需要单机高并发,可以在程序启动时调大:
System.Net.ServicePointManager.DefaultConnectionLimit = 64; System.Net.ServicePointManager.Expect100Continue = false;Expect100Continue = false是我在工业设备上报场景里验证过多次的一招,能砍掉一次多余的“询询”握手,对高并发延迟有明显改善。
4.4 如何判断接口到底执行完成了没有
这个问题看起来基础,但真有不少人没搞清楚。HTTP POST 请求发出后,客户端只知道“服务端有没有返回响应”以及“响应状态码是多少”,它并不关心服务端业务执行到哪一步。如果你的业务要求“必须确认执行完成才能继续”,那要分两种情况处理。
第一种是服务端在响应体里返回业务执行结果,比如{"code":0,"message":"success"},这种情况下,客户端要做的就是读到响应、反序列化、判断 code 字段,不要只靠 HTTP 状态码 200 就想当然认为业务成功了——很多服务端 200 只是“请求收到并解析成功”,业务逻辑可能还是失败的。第二种是服务端接单后异步处理,立刻返回202 Accepted和一个任务 ID,真正的执行结果要通过另一个查询接口轮询拿。这种模式在导入导出、批量任务场景非常常见。我自己的习惯是:任何 POST 请求的返回,都严格走“读响应体 -> 解析约定字段 -> 判断业务状态 -> 决定重试或抛出异常”的路径,绝不去赌状态码。
4.5 从 byte[] 到 string:流读取的编码细节
读取响应流时,最忌讳的是用默认编码去转字符串。C# 的StreamReader如果不指定 Encoding,会用 UTF-8 探测,但如果服务端返回的是 GBK 编码,或者流里正好有 BOM 标记,就可能解析出错。稳妥做法是明确指定编码。另外如果你手上有的是byte[],比如从 TCP 层拿到的原始数据,转字符串参考:
byte[] bytes = responseBytes; // 假设这是从某个流里读出来的原始字节 string text = Encoding.UTF8.GetString(bytes);在 HTTP 场景里,响应字符串的来源是ReadAsStringAsync(),它内部已经帮你处理了编码探测,但前提是服务端在响应头里正确声明了 charset,或者响应体里有 BOM。如果遇到中文乱码,优先检查服务端返回的 Content-Type,而不是在客户端反复改编码。
5. 上位机场景实战:扫码枪触发上报和协议选型
5.1 串口扫码枪触发一次 POST 上报
C# 上位机里最常见的活之一,就是扫码枪扫到一个条码,然后触发一次上报。扫码枪一般通过串口或者 HID 输入,串口方式用SerialPort类就能收。这里我贴一个串口扫码触发上报的简化思路:
SerialPort sp = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One); sp.DataReceived += async (sender, e) => { string code = sp.ReadExisting().Trim(); // 实际项目中要注意粘包、半包问题 if (string.IsNullOrEmpty(code)) return; var payload = new { deviceId = DeviceConfig.DeviceId, scanCode = code, timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; string json = JsonSerializer.Serialize(payload); try { string resp = await HttpJsonClient.PostJsonAsync(DeviceConfig.ApiUrl, json, 5); // 根据 resp 内容做 UI 反馈或记录日志 } catch (Exception ex) { // 记录失败,考虑是否需要重试队列 } };这里有几个界面开发的经验点。第一,DataReceived事件是在后台线程触发的,千万不要在事件里直接操作 UI 控件,要用Invoke或者Dispatcher回到 UI 线程再更新界面。第二,扫码枪的串口数据很可能不是一次完整到达的,尤其是条码比较长的时候,直接ReadExisting()可能只读到一半。严谨做法是维护一个接收缓冲区,根据结束符(扫码枪一般末尾带回车)判断一帧数据是否完整。第三,POST 上报如果失败,不要直接在界面弹窗就完了,最好维护一个失败重试队列,或者至少把失败记录写进本地日志文件,现场排查时不至于什么都看不到。
5.2 C# 和 VisionMaster 这类视觉软件通信,选 HTTP 还是 Socket
经常有人问:C# 上位机要跟海康 VisionMaster 通信,用什么协议比较好?这个问题没有标准答案,但我可以说说我自己的选型经验。如果你的交互节奏是“结果型”——比如视觉软件检测完成,把 OK/NG 结果、坐标信息发给上位机,上位机不关心中间过程——那 HTTP POST + JSON 就是很合适的方案,优势是开发快、字段清晰、调试方便,浏览器一开就能看格式。如果你对实时性要求极高,比如每帧图像都要做位置同步或者要传图像数据本身,那 TCP Socket 或者更底层的协议才合适,因为它没有 HTTP 头部的额外开销,延迟更低、吞吐更大。
有一点要注意,HTTP 连接是“每次请求一个来回”的模型,服务端不能主动推数据给客户端。如果视觉软件想主动推送结果给上位机,光用 HTTP 就比较别扭,这时候要么上位机轮询,要么干脆换 Socket 长连接或者内置的消息中间件。我遇到过的很多项目,最后都是“HTTP + JSON”做业务上报、“Socket 长连接”做状态推送,两者配合使用,没必要非用一个方案包打天下。
5.3 关于 HTTP 连接复用再啰嗦两句
前面多次提到连接复用,这里单独强调一下它的重要性。HTTP 底层其实是 TCP 连接,每次new HttpClient再用完丢弃,那么这个 TCP 连接会在 TIME_WAIT 状态里停留很长一段时间,高频率请求时端口会大量占用,最终表现为“过几分钟就全部超时,重启程序又好了”。
正确做法从根上说就一条:让 HttpClient 实例长期存活。如果你用的是框架级的依赖注入,AddHttpClient默认就是帮你管理连接池的;如果像我上面封装的那样用静态字段,也能达到差不多的效果。关键理解就一句话:TCP 连接能复用就不要频繁建立,这不是玄学,是操作系统层的资源管理原则。
最后再说一个调试技巧:怀疑请求有问题时,第一时间不是改代码,而是抓包或者看请求日志。Windows 上可以用 Fiddler 抓本地 HTTP 流量,Linux 上 tcpdump 也顺手。你亲眼看到实际发出去的请求头、请求体、响应内容,比在代码里加一百句 Console.WriteLine 都管用。我个人的习惯是调试阶段先通过 Fiddler 把请求包调对,再回到代码里跑,能省至少一半的联调时间。这些经验都是拿项目交付周期换出来的,希望对你这边有帮助。
本文还有配套的精品资源,点击获取