简介:本资源是一套基于.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:按Ctrl+F5直接运行(无需调试),观察窗体左上角"Server Status"是否变为"Running"此时服务端已在ws://localhost:8080监听。注意:app.config中<add key="Port" value="8080"/>可随时修改端口,修改后必须重启程序生效(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 Action<string>(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. 客户端HTML+JS实战:脱离框架的原生WebSocket连接与消息交互
3.1 websocket_client.html结构解析:最小化可运行模板
该HTML文件仅127行,无外部CDN依赖(jquery.js已内置),核心结构如下:
<!-- 3.1.1 连接区域 --> <input type="text" id="wsUrl" value="ws://localhost:8080" /> <button onclick="connect()">Connect</button> <!-- 3.1.2 发送区域 --> <input type="text" id="message" placeholder="Enter message..." /> <button onclick="sendMessage()">Send</button> <!-- 3.1.3 日志区域 --> <div id="log"></div>关键点在于:所有WebSocket操作封装在ws全局变量中,避免闭包污染。connect()函数创建实例后,立即绑定onopen/onmessage/onclose事件,其中onmessage直接将event.data追加到<div id="log">,不经过任何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)抛出SyntaxError,ws对象未创建成功。
解决:在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 name="Allow 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 Action<string> 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看分片边界,而不是怀疑代码逻辑——因为网络层的真相永远比应用层的日志更诚实。
希望帮到你。
本文还有配套的精品资源,点击获取