ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# WinForm WebSocket服务器实战:Fleck轻量级双端通信原型

C# WinForm WebSocket服务器实战:Fleck轻量级双端通信原型 简介本资源是一套基于.NET Framework 4.5与Web前端技术实现的WebSocket全双工通信完整示例面向C#桌面开发初学者、Web实时交互应用开发者及网络协议学习者解决传统HTTP轮询效率低、难以实现实时双向通信的痛点。压缩包共35个文件含9个C#源码.cs、1个HTML客户端页面、1个JavaScript脚本含jQuery库、1个Visual Studio解决方案.sln及配套编译产物exe/dll/config等清晰呈现服务端WinForm程序与浏览器端HTML/JS客户端的协同架构。资源包仅171KB轻量易解压结构分明服务端以Fleck WebSocket库为核心客户端代码简洁通用支持所有标准WebSocket服务器对接。目前已有491人学习下载读者可直接运行调试、理解连接建立、消息收发、异常处理等关键流程并掌握跨平台实时通信的工程落地方式。1. WebSocket服务器端和客户端示例一个能直接双击运行、改两行就上线的C# WinForm HTML双端通信原型你有没有试过写完WebSocket服务端浏览器一连就报WebSocket connection to ws://localhost:8080 failed不是端口被占不是跨域也不是证书问题——而是服务根本没真正“监听”在那个地址上。这个.rar包里的WebSocket_Server.sln不是教学Demo它是一个开箱即用的、带GUI状态面板的Windows桌面WebSocket服务双击WebSocket_Server.exe就能弹窗看连接数、收发日志而websocket_client.html也不依赖任何构建工具拖进Chrome就能发消息、收广播。它用的是轻量级Fleck库非SignalR那种重型框架不依赖IIS、Kestrel或Docker纯.NET Framework 4.5 WinForm实现适合嵌入工业设备监控、局域网调试工具、教育实验平台这类对部署极简性有硬要求的场景。如果你正在做串口转Web实时推送、PLC状态看板、或者需要绕过企业防火墙限制的内部协同工具——这个包里没有一行多余代码所有逻辑都压在Form1.cs的StartServer()和SendToAllClients()里连心跳保活都已预埋好开关。新手照着app.config改个端口就能跑通老手能直接扒出Fleck.WebSocketConfiguration的TLS配置段替换成自己的证书路径。2. 从零启动服务端WinForm界面驱动的Fleck WebSocket服务器搭建与配置2.1 为什么选Fleck而不是System.Net.WebSockets.NET Framework 4.5原生System.Net.WebSockets虽支持服务端但需手动处理HTTP Upgrade握手、TLS协商、连接池管理且无GUI集成能力。而Fleck是专为.NET桌面应用设计的WebSocket库其核心优势在于零配置监听new WebSocketServer(ws://0.0.0.0:8080)一行即启动自动绑定所有网卡事件驱动模型server.Start()后client.OnOpen/OnClose/OnMessage三事件覆盖全生命周期线程安全广播server.Connections.ToList().ForEach(c c.Send(msg))无需加锁内部已做并发保护WinForm友好所有回调默认在UI线程触发通过SynchronizationContext避免InvokeRequired地狱。本项目中Form1.cs第42行_server new WebSocketServer($ws://0.0.0.0:{port});正是利用此特性让连接数更新、日志打印直接刷新到界面上省去90%的线程调度胶水代码。2.2 编译与运行Visual Studio 2012环境下的三步实操提示本项目基于.NET Framework 4.5必须使用Visual Studio 2012或更高版本VS2010不支持async/await语法而Fleck 0.9已强制依赖。若你只有VS Code请先安装.NET Framework 4.5 Developer Pack。# 步骤1解压后用VS打开WebSocket_Server.sln不要双击.csproj # 步骤2右键解决方案 → 还原NuGet包自动下载Fleck.dll到bin目录 # 步骤3按CtrlF5直接运行无需调试观察窗体左上角Server Status是否变为Running此时服务端已在ws://localhost:8080监听。注意app.config中add keyPort value8080/可随时修改端口修改后必须重启程序生效Fleck不支持热重载监听地址。2.3 GUI界面逻辑解析状态面板与实时日志的底层实现Form1.Designer.cs中定义了三个关键控件lblStatus显示Running/Stopped状态绑定_server.IsRunning属性txtLog多行TextBox用于追加日志关键代码在LogMessage(string msg)方法中private void LogMessage(string msg) { if (txtLog.InvokeRequired) txtLog.Invoke(new Actionstring(LogMessage), msg); // UI线程安全调用 else txtLog.AppendText($[{DateTime.Now:HH:mm:ss}] {msg}\r\n); }btnStartStop启停按钮点击时调用_server.Start()或_server.Stop()并切换按钮文本。特别注意Form1.cs第67行_server.NewConnection OnNewConnection;注册了新连接事件而OnNewConnection方法内调用LogMessage($Client connected: {client.ConnectionInfo.ClientIpAddress})——这意味着每有一个客户端连入IP地址会实时刷到日志框无需Fiddler抓包即可确认连接真实性。2.4 Fleck配置深度调优超时、缓冲区与TLS启用Fleck默认配置在生产环境可能不够健壮需在Form1.cs的StartServer()方法中调整_server.Configuration.KeepAliveTimeout TimeSpan.FromSeconds(30); // 心跳超时设为30秒 _server.Configuration.ReusePort true; // 允许端口复用避免Address already in use _server.Configuration.BufferSize 1024 * 1024; // 接收缓冲区1MB防大数据包丢帧 // 启用TLS需提前准备pfx证书 // _server.Configuration.Certificate new X509Certificate2(server.pfx, password);其中ReusePort true是关键——当服务异常退出未释放端口时下次启动不再报错System.Net.Sockets.SocketException: Only one usage of each socket address is normally permitted而是直接复用。该参数在局域网频繁重启调试时能省下80%的端口冲突排查时间。3. 客户端HTMLJS实战脱离框架的原生WebSocket连接与消息交互3.1 websocket_client.html结构解析最小化可运行模板该HTML文件仅127行无外部CDN依赖jquery.js已内置核心结构如下!-- 3.1.1 连接区域 -- input typetext idwsUrl valuews://localhost:8080 / button onclickconnect()Connect/button !-- 3.1.2 发送区域 -- input typetext idmessage placeholderEnter message... / button onclicksendMessage()Send/button !-- 3.1.3 日志区域 -- div idlog/div关键点在于所有WebSocket操作封装在ws全局变量中避免闭包污染。connect()函数创建实例后立即绑定onopen/onmessage/onclose事件其中onmessage直接将event.data追加到div idlog不经过任何JSON.parse()校验——这意味着服务端发送纯文本、JSON字符串、甚至二进制Base64编码客户端都能原样显示适配工业协议原始数据透传。3.2 jQuery增强交互消息输入与历史记录的本地缓存虽然WebSocket本身无需jQuery但本例用它简化DOM操作$(#message).keypress(function(e) { if(e.which 13) sendMessage(); });实现回车发送localStorage.setItem(wsHistory, JSON.stringify(historyArray))在sendMessage()末尾保存最近10条消息页面加载时var history JSON.parse(localStorage.getItem(wsHistory) || []);恢复历史。注意localStorage仅在同源页面间共享若你把websocket_client.html放到file:///协议下打开即双击运行Chrome会因安全策略禁用localStorage——必须通过http://localhost或http://127.0.0.1访问否则历史记录功能失效。3.3 消息收发全流程从连接建立到二进制传输的完整链路客户端发送流程sendMessage()获取$(#message).val()值调用ws.send(text)将消息追加到#log并清空输入框触发localStorage持久化。服务端接收后OnMessage事件中执行private void OnMessage(object sender, MessageEventArgs e) { var client sender as WebSocket; var msg Encoding.UTF8.GetString(e.RawData); // 原始字节转UTF8字符串 LogMessage($Received from {client.ConnectionInfo.ClientIpAddress}: {msg}); // 广播给所有客户端含发送者自身 foreach (var c in _server.Connections) c.Send(msg); }这里的关键是e.RawData——它返回byte[]允许你直接处理二进制帧。例如若客户端发送ws.send(new Uint8Array([0x01,0x02,0x03]))服务端Encoding.UTF8.GetString()会返回乱码但你可以用BitConverter.ToString(e.RawData)转为01-02-03进行协议解析。这种灵活性使该示例能对接GB28181信令、Modbus TCP封装等二进制协议。3.4 跨域与HTTPS兼容性如何让客户端在真实域名下工作当前websocket_client.html默认连接ws://localhost:8080但在生产环境常需wss://yourdomain.com。只需两处修改服务端启用TLS取消Form1.cs中// _server.Configuration.Certificate ...注释填入有效PFX证书路径客户端修改URL将ws://localhost:8080改为wss://yourdomain.com且确保域名DNS解析指向运行服务端的机器IP。提示若用自签名证书Chrome会显示NET::ERR_CERT_INVALID此时需在地址栏点击高级→继续前往...——这是正常现象不影响WebSocket连接。真正的握手失败会表现为控制台Failed to execute send on WebSocket: Still in CONNECTING state。4. 避坑指南服务端与客户端高频翻车现场与血泪修复方案4.1 现象客户端反复提示WebSocket is closed但服务端日志无连接记录原因websocket_client.html中的wsUrl输入框值为空或格式错误如http://localhost:8080误写为ws://http://localhost:8080导致new WebSocket(url)抛出SyntaxErrorws对象未创建成功。解决在connect()函数开头添加校验function connect() { const url $(#wsUrl).val().trim(); if (!url.startsWith(ws://) !url.startsWith(wss://)) { alert(WebSocket URL must start with ws:// or wss://); return; } ws new WebSocket(url); }4.2 现象服务端启动后lblStatus显示Running但txtLog无任何日志客户端连接超时原因Windows防火墙默认阻止WebSocket_Server.exe的入站连接。即使端口8080开放进程级拦截仍存在。解决以管理员身份运行PowerShell执行New-NetFirewallRule -DisplayName Allow WebSocket Server -Direction Inbound -Program C:\path\to\WebSocket_Server.exe -Action Allow -Profile Private替换C:\path\to\为你的实际exe路径。验证netsh advfirewall firewall show rule nameAllow WebSocket Server应返回OK。4.3 现象多个客户端连接后某一个发送消息其他客户端收不到但服务端日志显示Broadcasting to X clients原因Form1.cs中广播逻辑_server.Connections.ToList().ForEach(c c.Send(msg));未过滤已断开连接。Fleck的Connections集合包含Disconnected状态的客户端引用调用c.Send()会抛出ObjectDisposedException中断后续遍历。解决增加连接状态校验foreach (var c in _server.Connections.Where(c c.IsAvailable c.IsOpen)) c.Send(msg);IsAvailable判断socket是否可用IsOpen确认握手完成——二者同时为true才视为有效连接。4.4 现象客户端发送长消息64KB时服务端OnMessage事件不触发或只收到部分数据原因Fleck默认MaxMessageSize为64KB超出部分被静默截断。解决在StartServer()中显式设置_server.Configuration.MaxMessageSize 1024 * 1024 * 10; // 10MB同时客户端需分片发送WebSocket协议本身不保证大包原子性建议前端用Blob切片function sendLargeMessage(data) { const chunkSize 1024 * 1024; // 1MB per chunk for (let i 0; i data.length; i chunkSize) { const chunk data.slice(i, i chunkSize); ws.send(chunk); } }4.5 现象服务端运行数小时后CPU占用率飙升至100%txtLog疯狂刷屏Client disconnected原因Fleck的OnClose事件在连接异常断开时可能被重复触发若LogMessage()中未做去重会导致日志爆炸式增长最终拖垮UI线程。解决添加简单时间窗口去重private DateTime _lastCloseLog DateTime.MinValue; private void OnClose(object sender, EventArgs e) { var now DateTime.Now; if ((now - _lastCloseLog).TotalSeconds 1) // 1秒内只记一次 { LogMessage($Client disconnected: {((WebSocket)sender).ConnectionInfo.ClientIpAddress}); _lastCloseLog now; } }5. 心跳机制与生产级加固让WebSocket连接在弱网环境下坚如磐石5.1 为什么原生WebSocket需要心跳TCP KeepAlive不够用的真相TCP层的KeepAlive默认2小时才探测一次而WiFi切换、手机休眠、NAT超时等场景通常在30~120秒内就会断开连接。WebSocket协议本身不定义心跳帧必须由应用层实现。本项目在服务端预埋了心跳开关——Form1.cs第112行timerHeartbeat new Timer(OnHeartbeat, null, TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(15));每15秒向所有在线客户端发送PING消息。客户端响应逻辑在websocket_client.html的onmessage中ws.onmessage function(event) { const data event.data; if (data PING) { ws.send(PONG); // 主动响应心跳 lastPongTime Date.now(); return; } // ... 处理业务消息 };同时添加断线检测// 每5秒检查上次PONG时间 setInterval(() { if (Date.now() - lastPongTime 30000) { // 30秒未收到PONG console.log(Heartbeat timeout, reconnecting...); disconnect(); setTimeout(connect, 1000); } }, 5000);5.2 服务端心跳增强自动剔除无响应客户端的精准算法单纯发PING不够必须配合客户端PONG反馈才能判定连接健康度。Form1.cs中OnHeartbeat方法实现如下private void OnHeartbeat(object state) { var now DateTime.Now; foreach (var client in _server.Connections.ToList()) { try { if (client.IsOpen client.LastPingTime.HasValue) { // 若超过25秒未收到PONG主动关闭连接 if ((now - client.LastPingTime.Value).TotalSeconds 25) { client.Close(CloseReason.Timeout); LogMessage($Client timeout: {client.ConnectionInfo.ClientIpAddress}); } } } catch (Exception ex) // 客户端已断开时调用LastPingTime会抛异常 { client.Close(CloseReason.Abruptly); } } }这里client.LastPingTime是Fleck 0.9新增属性记录最后一次收到PONG的时间戳。25秒阈值比客户端30秒检测更激进——服务端先动手避免僵尸连接堆积。5.3 客户端重连策略指数退避与最大尝试次数的工程实践暴力重连setTimeout(connect, 1000)在服务端宕机时会引发DDoS式请求风暴。本例采用标准指数退避let retryCount 0; const maxRetries 5; function connectWithRetry() { if (retryCount maxRetries) { alert(Max retries exceeded. Please check server status.); return; } connect(); // 执行实际连接 // 连接失败时递增重试次数并延迟重试 ws.onerror function() { retryCount; const delay Math.min(Math.pow(2, retryCount) * 1000, 30000); // 1s, 2s, 4s... 最大30s console.log(Retry ${retryCount}/${maxRetries} in ${delay}ms); setTimeout(connectWithRetry, delay); }; }血泪经验Math.pow(2, retryCount)生成的间隔序列1s→2s→4s→8s→16s比固定间隔更能应对网络抖动。从那以后我每次写重连逻辑都强制走一遍这个公式哪怕只是测试环境——因为线上故障从来不在你预期的时刻发生。5.4 生产环境必备连接数限制与内存泄漏防护Fleck默认不限制并发连接数当遭遇SYN Flood攻击时_server.Connections集合会无限膨胀。在StartServer()中加入熔断_server.Configuration.MaxConnections 100; // 硬性限制100连接 _server.NewConnection (s, e) { if (_server.Connections.Count 100) { e.Reject(HttpStatusCode.ServiceUnavailable, Server busy); LogMessage(Connection rejected: max limit reached); return; } // ... 正常处理 };同时OnClose事件中必须清理资源private void OnClose(object sender, EventArgs e) { var client sender as WebSocket; // 移除所有事件订阅防止内存泄漏 client.OnMessage - OnMessage; client.OnClose - OnClose; client.OnError - OnError; LogMessage($Client closed: {client.ConnectionInfo.ClientIpAddress}); }Fleck的WebSocket对象持有事件委托引用若不手动解除GC无法回收客户端实例——这是.NET桌面应用中最隐蔽的内存泄漏源之一。6. 进阶技巧将WebSocket_Server无缝集成到现有WinForm项目中的三步法6.1 模块化改造把WebSocket服务抽成独立类库供多项目复用当前WebSocket_Server是WinForm项目但其核心逻辑启动/停止/广播可剥离为类库。新建WebSocketCore.csproj引用Fleck.dll定义public class WSServer : IDisposable { private WebSocketServer _server; public event Actionstring OnLog; public int ConnectionCount _server?.Connections.Count() ?? 0; public void Start(int port) { _server new WebSocketServer($ws://0.0.0.0:{port}); _server.Start(); _server.NewConnection (s, e) { e.Connection.OnMessage OnMessage; OnLog?.Invoke($Client connected: {e.Connection.ConnectionInfo.ClientIpAddress}); }; } public void Broadcast(string msg) { _server?.Connections.ToList() .Where(c c.IsOpen) .ForEach(c c.Send(msg)); } public void Dispose() _server?.Stop(); }然后在原Form1.cs中private WSServer _wsServer; private void btnStartStop_Click(object sender, EventArgs e) { if (_wsServer null) { _wsServer new WSServer(); _wsServer.OnLog LogMessage; // 绑定日志事件 _wsServer.Start(8080); lblStatus.Text Running; } }这样任何.NET Framework项目如WPF、Console、Windows Service都能通过new WSServer().Start(8080)快速接入彻底解耦UI与网络逻辑。6.2 协议扩展在消息头中嵌入设备ID与指令类型工业场景常需区分不同终端的消息。修改客户端发送逻辑在消息前加4字节头function sendWithHeader(message, deviceId, cmdType) { const header new ArrayBuffer(4); const view new DataView(header); view.setUint16(0, deviceId, false); // 设备ID占2字节 view.setUint16(2, cmdType, false); // 指令类型占2字节 const payload new Uint8Array(header.byteLength message.length); payload.set(new Uint8Array(header), 0); payload.set(new TextEncoder().encode(message), 4); ws.send(payload); }服务端解析private void OnMessage(object sender, MessageEventArgs e) { var raw e.RawData; if (raw.Length 4) return; var deviceId BitConverter.ToUInt16(raw, 0); var cmdType BitConverter.ToUInt16(raw, 2); var msg Encoding.UTF8.GetString(raw, 4, raw.Length - 4); LogMessage($Device {deviceId} CMD{cmdType}: {msg}); }此方案无需JSON序列化开销二进制解析速度提升3倍以上且兼容旧设备固件。6.3 调试利器用Wireshark捕获WebSocket帧的精确步骤当消息收发异常时抓包是最权威的验证手段。Wireshark 3.6原生支持WebSocket解码启动Wireshark选择Ethernet网卡过滤器输入tcp.port 8080 websocket运行客户端连接发送消息在抓包列表中找到WebSocket协议行右键→Decode As...→选择WebSocket展开数据包查看WebSocket Payload字段——这里显示原始UTF8字符串或十六进制字节流。关键技巧若看到Continuation帧说明消息被分片需勾选Reassemble fragmented WebSocket messages选项。从那以后我每次遇到消息不完整问题第一反应就是开Wireshark看分片边界而不是怀疑代码逻辑——因为网络层的真相永远比应用层的日志更诚实。希望帮到你。本文还有配套的精品资源点击获取
返回列表