资讯详情

C# WinForm中用HttpClient与HttpListener实现JSON通信

📅 2026/9/10 8:25:59 | 华诺云谱 👁 阅读
C# WinForm中用HttpClient与HttpListener实现JSON通信
1. 需求拆解与技术选型1.1 先搞清楚“请求WinForm服务器”到底是什么场景我在做C#上位机开发的这几年碰到这个需求不下十次。标题里说“请求winform服务器”十有八九是下面这几种情况之一WinForm程序里嵌了一个HttpListener作为一个轻量级的本地HTTP服务接收来自扫码枪、手持PDA、其他电脑上位机发来的数据。你的WinForm客户端需要对接局域网里另一台运行着WinForm程序的电脑而那边用第三方库比如EmbedIO、Nancy挂了HTTP接口。公司内部的MES、WMS系统是B/S架构但客户端是WinForm需要用它去调后端的Web API接口要求传JSON。不管哪一种本质都是一件事在C#的桌面程序里写一个稳的HTTP客户端能够用POST、GET两种方式把JSON数据发出去再把服务器返回的JSON读回来。很多老项目里还在用HttpWebRequest代码又臭又长而且处理异步特别别扭我建议新项目直接上HttpClient老项目能改也尽量改。1.2 技术选型HttpClient才是正道先看一下选型对比方便你判断自己项目里该用哪个方案异步支持连接复用代码量适用场景HttpWebRequest差老式IAsyncResult手动管理容易端口耗尽多遗留老项目WebClient一般较弱少但功能受限简单下载上传HttpClient原生async/await默认复用连接池自动管理少清晰新项目首选RestSharp好基于HttpClient极少快速对接第三方API我的经验是HttpClient在桌面程序里用起来最顺手。它一个很重要的特性就是连接复用底层自动维护连接池不会像HttpWebRequest那样频繁创建和销毁连接导致端口不够用。不过别踩一个经典坑反复new HttpClient()。如果一个程序里频繁创建HttpClient每次都会建立新的底层连接最后网卡端口被占满直接报SocketException: Only one usage of each socket address。正规做法是定义一个静态字段整个应用生命周期只用一个实例。private static readonly HttpClient httpClient new HttpClient() { Timeout TimeSpan.FromSeconds(10) };有一个细节要注意HttpClient.Timeout设的是整个请求的最长耗时不是 DNS 解析或者连接建立的单独超时所以要做区分场景。如果是本地局域网内的WinForm服务我一般设5到10秒如果是公网API设30秒左右不然文件上传或者大报文POST很容易误触发超时。2. 核心客户端封装GET和POST一次搞定2.1 定义请求参数和返回结构写工具类之前先把数据模型定好。不要直接用五花八门的匿名对象到处传参项目大了会非常难维护。我习惯先定义一个请求参数类把所有HTTP请求需要的信息统一管理public class RequestOptions { public string Url { get; set; } public Dictionarystring, string QueryParams { get; set; } public object Body { get; set; } public Dictionarystring, string Headers { get; set; } public int TimeoutSeconds { get; set; } 10; }返回结果也做一个统一封装。我比较喜欢返回(bool IsSuccess, string Message, T Data)这种三元组或者干脆定义一个泛型结果类public class ApiResultT { public bool Success { get; set; } public string Message { get; set; } public T Data { get; set; } }这样在WinForm里调用的时候直接判断result.Success就能决定弹错误提示还是继续处理不用每个方法都重复写try/catch。2.2 GET请求封装拼接参数是门细致活GET请求把参数拼到URL后面看起来简单但容易在特殊字符上翻车。中文、、、空格这些字符直接拼上去服务器那边解析十有八九乱掉。所以必须用Uri.EscapeDataString做编码public static string BuildQueryString(Dictionarystring, string parameters) { if (parameters null || parameters.Count 0) { return string.Empty; } var pairs parameters.Select(kv ${Uri.EscapeDataString(kv.Key)}{Uri.EscapeDataString(kv.Value)}); return string.Join(, pairs); } public static async Taskstring GetAsync(RequestOptions options) { var url options.Url; if (options.QueryParams ! null options.QueryParams.Count 0) { // 判断原URL带不带问号 var separator url.Contains(?) ? : ?; url separator BuildQueryString(options.QueryParams); } using var request new HttpRequestMessage(HttpMethod.Get, url); if (options.Headers ! null) { foreach (var header in options.Headers) { request.Headers.TryAddWithoutValidation(header.Key, header.Value); } } var response await httpClient.SendAsync(request); return await response.Content.ReadAsStringAsync(); }这里有个细节我想多说一句HttpRequestMessage用using包起来是为了及时释放消息资源但HttpClient本身不要释放因为它是全局的。很多人以为每次请求都要using (var client new HttpClient())这个习惯一定要改掉。2.3 POST请求封装ContentType决定服务端能否解析POST请求最关键的一步是设置Content-Type。既然标题明确了“Json格式”那Content-Type就一定要写application/json。我见过很多新手用application/x-www-form-urlencoded去传JSON字符串结果WinForm服务端那边用ReadAsStringAsync()读出来是一堆keyvaluekeyvalue压根不是JSON结构两边对不上。建议POST方法接收一个对象方法内部自己完成序列化这样调用方不需要关心JSON字符串怎么拼public static async Taskstring PostJsonAsync(RequestOptions options) { var json JsonConvert.SerializeObject(options.Body, new JsonSerializerSettings { // 忽略值为null的属性减少报文体积 NullValueHandling NullValueHandling.Ignore, // 日期格式统一避免服务器解析歧义 DateFormatString yyyy-MM-dd HH:mm:ss }); using var request new HttpRequestMessage(HttpMethod.Post, options.Url) { Content new StringContent(json, Encoding.UTF8, application/json) }; if (options.Headers ! null) { foreach (var header in options.Headers) { request.Headers.TryAddWithoutValidation(header.Key, header.Value); } } var response await httpClient.SendAsync(request); return await response.Content.ReadAsStringAsync(); }StringContent构造函数的第三个参数传application/json它会自动设置Content-Type: application/json; charsetutf-8。如果只写了Encoding.UTF8而不写MediaType默认的Content-Type是text/plain服务端解析JSON的时候很容易直接报错。如果你用的不是Newtonsoft.Json而是微软官方的System.Text.Json就把JsonConvert.SerializeObject换成JsonSerializer.Serialize。但是要注意System.Text.Json默认对属性的匹配是大小写敏感的如果WinForm服务端的模型属性和你的不一致序列化那边会莫名拿到一堆默认值。遇到这种情况写个JsonSerializerOptions { PropertyNameCaseInsensitive true }就能解决。2.4 异步到底怎么用才不卡UIWinForm程序员最痛的一个问题就是请求发出去之后界面整个卡死鼠标转圈圈。原因很简单你在UI线程上同步调用了.Result或者.Wait()而HTTP请求是实打实的网络IO中间可能要等好几秒UI线程被阻塞界面当然就假死了。正确姿势是按钮事件写成async void方法内部用awaitprivate async void btnQuery_Click(object sender, EventArgs e) { btnQuery.Enabled false; try { var options new RequestOptions { Url http://127.0.0.1:8080/api/order, QueryParams new Dictionarystring, string { { orderNo, txtOrderNo.Text.Trim() } } }; var response await HttpHelper.GetAsync(options); var result JsonConvert.DeserializeObjectApiResultListOrderModel(response); if (result.Success) { dataGridView1.DataSource result.Data; } else { MessageBox.Show(result.Message); } } catch (Exception ex) { MessageBox.Show($请求异常{ex.Message}); } finally { btnQuery.Enabled true; } }await之后代码默认会回到UI线程上下文所以直接操作dataGridView1不会报“线程间操作无效”的错误。如果你的代码里用了.ConfigureAwait(false)那就必须手动Invoke回UI线程不然WinForm的控件操作会抛异常。这个点很多人不知道我也是踩了坑才长记性。3. WinForm服务器端怎么接内置HttpListener方案3.1 为什么推荐HttpListener既然标题是“请求winform服务器”那客户端的请求发出来了WinForm服务器端也得能接收才行。我见到最多的方案是在WinForm里启动一个HttpListener监听本机端口。原因很简单不依赖IIS、不依赖外部框架.NET Framework自带WinForm里几行代码就能跑起来。HttpListener的模型就是监听一个前缀比如http://*:8080/然后不停地GetContext()拿到HTTP请求处理完返回响应。这种模式特别适合做部署在车间电脑上的数据中转服务。3.2 服务端核心代码先初始化并启动监听private HttpListener listener; private void StartHttpServer() { listener new HttpListener(); // 监听所有网卡的8080端口 listener.Prefixes.Add(http://:8080/); listener.Start(); // 开一个后台线程持续接收请求 Task.Run(() ListenLoop()); } private async Task ListenLoop() { while (listener.IsListening) { try { var context await listener.GetContextAsync(); // 这里不要阻塞直接丢到线程池处理 _ Task.Run(() ProcessRequest(context)); } catch (Exception ex) { // listener停止时会抛异常这里做好日志记录 LogHelper.WriteLog(监听异常, ex); } } }处理请求的核心逻辑第一步是区分GET和POST第二步是读取请求体里的JSON字符串private void ProcessRequest(HttpListenerContext context) { var request context.Request; var response context.Response; // 统一以UTF-8输出避免中文乱码 response.ContentEncoding Encoding.UTF8; response.Headers.Add(Content-Type, application/json; charsetutf-8); response.StatusCode 200; try { string resultJson ; if (request.HttpMethod GET) { // GET参数从QueryString取 var orderNo request.QueryString[orderNo]; resultJson HandleGet(orderNo); } else if (request.HttpMethod POST) { // POST的JSON从输入流读取 using var reader new StreamReader(request.InputStream, request.ContentEncoding ?? Encoding.UTF8); var body reader.ReadToEnd(); // 注意body是JSON字符串反序列化成对象后再处理 var requestData JsonConvert.DeserializeObjectJObject(body); resultJson HandlePost(requestData); } else { response.StatusCode 405; resultJson JsonConvert.SerializeObject(new { Success false, Message 不支持的请求方式 }); } var buffer Encoding.UTF8.GetBytes(resultJson); response.ContentLength64 buffer.Length; response.OutputStream.Write(buffer, 0, buffer.Length); } catch (Exception ex) { response.StatusCode 500; var errorJson JsonConvert.SerializeObject(new { Success false, Message ex.Message }); var buffer Encoding.UTF8.GetBytes(errorJson); response.OutputStream.Write(buffer, 0, buffer.Length); } finally { response.OutputStream.Close(); } }3.3 服务端收到请求以后怎么更新UI这是WinForm做HTTP服务最核心的一个技巧点也是网上教程很少讲透的地方。HttpListener的回调线程是线程池后台线程你直接在里面写label1.Text 收到数据肯定会报“线程间操作无效从未创建控件的线程访问它”。网上很多人教你用this.Invoke但要注意一个陷阱ProcessRequest是异步线程里的静态方法或者类方法this.Invoke如果窗体正在关闭会抛ObjectDisposedException。所以我的习惯是定义一个事件收到请求后触发事件由WinForm主窗体在Load事件里订阅并在UI线程上处理public event ActionJObject DataReceived; private string HandlePost(JObject data) { // 通过事件把数据抛给UI层UI层自行决定用Invoke还是直接更新 DataReceived?.Invoke(data); // 同步返回给客户端告知已收到 return JsonConvert.SerializeObject(new { Success true, Message 已收到 }); }主窗体订阅的地方用BeginInvoke来确保在UI线程执行控件更新server.DataReceived data { BeginInvoke(new Action(() { txtLog.AppendText($收到数据{data.ToString()}{Environment.NewLine}); })); };这样客户端POST过来的JSON就能非常顺畅地显示在WinForm界面上不卡界面、不闪退接口响应也及时。4. 联调实战一个扫码枪数据上报的例子4.1 数据库表结构的简化设计光说工具类有点虚我拿一个实际场景串一遍车间里有一台扫码枪扫出来的条码要实时上报给一台装有WinForm程序的工位电脑工位电脑那边要把数据显示在界面上并同步写入数据库。假设数据库里有一张ScanRecords表简化成三个字段CREATE TABLE ScanRecords ( Id INT IDENTITY(1,1) PRIMARY KEY, Barcode NVARCHAR(64) NOT NULL, ScanTime DATETIME DEFAULT GETDATE() );4.2 客户端编码与上报扫码枪在C#里的触发方式一般是串口或者HID直连我这里简化处理用一个文本框模拟扫码后的数据到达事件然后通过我们封装的PostJsonAsync上报数据private async void txtBarcode_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode Keys.Enter) { var barcode txtBarcode.Text.Trim(); if (string.IsNullOrEmpty(barcode)) return; var payload new { Barcode barcode }; var options new RequestOptions { Url http://192.168.1.50:8080/api/scan, Body payload }; var response await HttpHelper.PostJsonAsync(options); var result JsonConvert.DeserializeObjectApiResultobject(response); if (result.Success) { txtLog.AppendText(${DateTime.Now:HH:mm:ss} 上报成功{barcode}{Environment.NewLine}); } else { txtLog.AppendText(${DateTime.Now:HH:mm:ss} 上报失败{result.Message}{Environment.NewLine}); } txtBarcode.Clear(); txtBarcode.Focus(); } }4.3 服务端接收入库WinForm服务端收到POST之后把JSON反序列化成实体类再用Dapper或者ADO.NET插入数据库。这里要注意一点ScanRecords表里的ScanTime是数据库默认值所以插入语句不需要显式传时间private string HandleScanData(JObject body) { var barcode body[Barcode]?.ToString(); if (string.IsNullOrEmpty(barcode)) { return JsonConvert.SerializeObject(new { Success false, Message 条码为空 }); } using var connection new SqlConnection(connString); var affectRows connection.Execute( INSERT INTO ScanRecords (Barcode) VALUES (Barcode), new { Barcode barcode }); return affectRows 0 ? JsonConvert.SerializeObject(new { Success true, Message 入库成功 }) : JsonConvert.SerializeObject(new { Success false, Message 入库失败 }); }这样整个链路就通了扫码枪扫码 → WinForm客户端封装JSON → POST → WinForm服务端监听解析 → 入库 → 返回结果 → 客户端界面刷新。4.4 调试接口的辅助工具联调的时候有个好工具能省一半时间。我比较常用Postman但测试WinForm服务端还有个更轻的方式浏览器直接敲GET地址或者在命令行用curl发POSTcurl -X POST http://192.168.1.50:8080/api/scan ^ -H Content-Type: application/json ^ -d {\Barcode\:\TEST12345\}如果返回的JSON里Success是true就说明WinForm服务端的监听、解析、入库逻辑都没问题BUG 基本只能出现在扫码枪硬件或者客户端逻辑上。这个排除法做联调效率非常高。5. 常见问题与排查技巧实录5.1 高频问题速查表现象可能原因解决方案请求超时HttpClient超时设置太短服务端没启动调大Timeout检查服务端监听状态中文乱码服务端没有设置Response编码为UTF-8设置response.ContentEncoding Encoding.UTF8服务端读到null或空字符串POST的Content-Type写错了确认是application/json而不是text/plainWinForm界面卡死在UI线程用了.Result或.Wait()改成async voidawait线程间操作无效后台线程直接更新控件用BeginInvoke包装控件更新端口被占用上一个HttpListener没停止检查进程或者监听前检测HttpListener.IsSupported并加try/catch返回500服务端处理请求时抛异常看异常日志多半是JSON格式对不上模型GET含中文乱码URL参数没做UrlEncode用Uri.EscapeDataString编码每个参数5.2 一些别人不常提的避坑心得第一个心得是关于HttpListener的URL前缀。监听http://:8080/时plus号表示监听所有IP但这个语法在非管理员权限下经常报“拒绝访问”。我的处理办法是安装程序时让用户以管理员运行或者干脆监听http://localhost:8080/再配合http://127.0.0.1:8080/一起加进去。局域网里其他电脑要访问时让程序跑在一个有权限的服务账户下省得被这个坑拦住半天。第二个心得是如果服务器和客户端是同一台机器跨进程调用时建议不要用localhost走TCP回环直接走HTTP没问题但WinForm程序之间其实也可以考虑NamedPipe。不过HTTP的好处是随时能用浏览器和curl调试排查问题容易得多所以我更倾向HTTP。第三个心得也是很实用的一条强烈建议所有HTTP请求都加一个请求ID。客户端在请求JSON里加个Guid服务端返回响应时把这个ID原样带回来。出问题的时候两边日志一对照谁丢了包、谁延迟高一目了然。这在多设备并发上报的场景下特别有用。第四个心得是关于序列化性能。WinForm程序处理高频数据比如扫码枪1秒扫好几次、PLC循环上报时Newtonsoft.Json默认的反射序列化速度偏慢。如果性能瓶颈明显把序列化对象换成System.Text.Json能快不少而且内存占用更低。数据量不大、一天几千条的话就不必折腾了怎么方便怎么来。6. 最后的经验总结关于“C#实现Http Json格式Post、Get方法请求winform服务器”这套东西我用一句话归纳把HttpClient的异步封装做好把服务端HttpListener的线程模型理清再搞明白UI线程更新控件的正确姿势这个需求就算彻底解决了。一个很多资料不会写但实际非常影响体验的细节是尽量把所有HTTP相关的代码抽到一个独立的HttpHelper类里不要在窗体代码里到处散落HttpClient调用。这样后续如果要换成RestSharp、要加统一鉴权、要加日志都是一个文件的事。我在项目里已经吃过“客户端代码散落各处改鉴权改到吐”的亏后来强迫自己把所有请求收敛到一个入口维护成本立刻降下来。另一个值得养成的习惯是请求和响应都保留一份原始报文日志。在调试阶段把服务端收到的报文完整记录到本地txt或数据库客户端收到响应也一样记录。等到现场出了问题、两边对不上账的时候这份日志就是破案的关键线索。这个习惯帮我排掉过好几次生产环境的疑难问题强烈建议保留。如果这套方案正好解决了你的问题或者你服务端用的是WinForm嵌入HttpListener那建议你把HttpListener维护在一个独立线程中并在程序退出时显式调用listener.Stop()和listener.Close()避免端口长时间被占用的尴尬。这些都是小事但积累多了代码的稳定性会有非常明显的提升。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。