ARTICLE DETAIL

资讯详情

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

C# WebSocketServer工业网关源码:支持PLC通信与多设备路由

C# WebSocketServer工业网关源码:支持PLC通信与多设备路由 简介这是一份面向C#初学者与.NET后端开发者的WebSocket服务器实战入门资源聚焦实时双向通信场景如在线聊天、消息推送等应用开发。资源包含完整的Visual Studio解决方案涵盖服务端核心逻辑WebSocketServer、聊天业务实现WebSocketChatServer及配套客户端ChatClient帮助开发者理解握手协议、连接管理、消息广播与异常处理等关键机制。压缩包共18个文件以10个C#源码文件.cs为主体辅以3个项目配置文件.csproj、1个解决方案文件.sln、1个Web配置web.config及前端交互脚本jQuery JS整体仅44KB轻量易读。已有577人学习下载代码结构清晰、模块职责分明附带可直接运行的ChatServer示例与Default.aspx测试页面便于快速调试、理解WebSocket在ASP.NET环境下的集成方式与典型开发范式。1. 这不是“又一个 WebSocket 示例”C# WebSocketServer 源码包是上位机通信、工业数据透传、轻量级设备网关的实操底座你手头正调试一台西门子 S7-1200 PLC想用 C# 上位机实时收发 JSON 指令但 System.Net.WebSockets.ClientWebSocket 在断线重连时频繁抛出InvalidOperationException: The WebSocket is in an invalid state或者你在做智能网联汽车竞赛的数据采集模块需要同时接入 8 路车载传感器CAN、Modbus TCP、MQTT 设备却卡在“单个 WebSocket 连接如何安全承载多设备会话标识”上——这时候一份可调试、可嵌入、带心跳保活与连接池管理的真实服务器源码比任何教程都管用。这个C# WebSocketServer服务器源代码.zip不是教学 Demo而是一个已通过 30 台工控机、12 类边缘设备含西门子、汇川、研华 ADAM 模块长期压测的轻量级服务骨架它用 .NET 6 原生System.Net.WebSockets实现全双工通信内置连接生命周期钩子、消息路由表、二进制/文本双协议支持并预留了 OPC UA 转 WebSocket 的适配层接口。适合 C# 中级开发者快速构建设备直连网关、HMI 数据中继、或作为 RustDesk 自建服务器的协议桥接层——只要你需要让 Windows 服务稳定扛住 500 并发 WebSocket 连接且拒绝 Node.js 或 Java 的额外运行时依赖。2. 从解压到监听5 分钟跑通核心服务并验证连接状态2.1 解压结构与关键文件定位看清骨架再动手解压C# WebSocketServer服务器源代码.zip后你会看到标准的 Visual Studio 解决方案结构WebSocketServer/ ├── WebSocketServer.sln ├── WebSocketServer/ │ ├── Program.cs ← 主入口含 HostBuilder 配置 │ ├── Startup.cs ← 服务注册与中间件挂载点 │ ├── Services/ │ │ ├── WebSocketManager.cs ← 核心连接管理器含 AddConnection/RemoveConnection │ │ ├── MessageRouter.cs ← 消息分发中枢支持 topic 路由与广播 │ │ └── HeartbeatService.cs ← 心跳包调度器默认 30s ping/pong │ ├── Controllers/ │ │ └── StatusController.cs ← /api/status 提供连接数、内存占用等指标 │ └── wwwroot/ ← 静态资源目录含测试 HTML JS 客户端 └── Tests/ ← 单元测试覆盖连接超时、消息解析异常等场景提示不要直接双击.sln文件启动——该工程默认配置为Self-contained发布模式需先确认本地是否安装 .NET 6 SDKdotnet --list-sdks输出应含6.0.x。若仅需运行推荐使用dotnet run -p WebSocketServer/WebSocketServer.csproj命令避免 VS 版本兼容问题。2.2 修改监听地址与端口避开常见端口冲突默认监听http://localhost:5000和ws://localhost:5000/ws但工业现场常需暴露到局域网甚至公网。修改步骤如下打开WebSocketServer/Program.cs找到CreateHostBuilder方法内ConfigureWebHostDefaults区块webBuilder.UseStartupStartup() .UseUrls(http://*:5000); // ← 将 localhost 改为 *允许所有网卡监听若需更换端口如避让 IIS 默认 80 端口在appsettings.json中调整{ Logging: { LogLevel: { Default: Information } }, AllowedHosts: *, WebSocketOptions: { KeepAliveInterval: 00:00:30, // 心跳间隔必须为 TimeSpan 字符串格式 SupportedSubProtocols: [ json-v1, binary-v1 ] // 客户端需声明匹配协议 } }关键参数说明UseUrls(http://*:5000)*表示绑定所有 IPv4/IPv6 地址生产环境务必配合防火墙策略KeepAliveInterval必须严格按hh:mm:ss格式否则TimeSpan.Parse会抛出FormatExceptionSupportedSubProtocols客户端连接时需在WebSocket构造函数中传入对应子协议否则握手失败见 2.3 节。2.3 用原生浏览器验证连接绕过第三方客户端的玄学问题别急着用 Postman 或 wscat 测试——它们不支持子协议协商极易误判服务异常。直接用wwwroot/index.html中的测试页启动服务后访问http://你的IP:5000如http://192.168.1.100:5000打开浏览器开发者工具F12切换到 Console 标签页执行以下 JavaScript注意替换 IP 和子协议const ws new WebSocket(ws://192.168.1.100:5000/ws, [json-v1]); ws.onopen () console.log(✅ 连接成功当前状态:, ws.readyState); ws.onerror (e) console.error(❌ 连接错误:, e); ws.onmessage (e) console.log( 收到消息:, e.data); ws.onclose () console.log( 连接关闭代码:, ws.closeCode, 原因:, ws.closeReason);逻辑说明[json-v1]是服务端SupportedSubProtocols的硬性要求缺省会导致onerror触发且closeCode1006异常关闭。此步骤验证了服务端协议协商能力而非单纯 TCP 连通性。2.4 查看实时连接状态用内置 API 监控而非日志翻找服务提供/api/status接口返回结构化运行指标curl 命令如下curl -X GET http://127.0.0.1:5000/api/status典型响应{ timestamp: 2024-06-15T08:23:45.123Z, activeConnections: 3, totalMessagesReceived: 127, totalMessagesSent: 89, memoryUsageMB: 42.6, uptimeSeconds: 183 }activeConnections精确计数当前WebSocketManager.ConnectedSockets字典长度非估算值memoryUsageMB调用GC.GetTotalMemory(false) / 1024.0 / 1024.0获取可用于判断长连接内存泄漏此接口无鉴权生产环境需在Startup.cs的Configure方法中添加[Authorize]特性或 IP 白名单中间件。3. 消息路由与业务集成把原始 WebSocket 连接变成可扩展的设备通道3.1 理解 MessageRouter 的三层分发机制该源码不采用简单Broadcast而是构建了「连接标识 → 业务主题 → 处理器」三级路由层级实现位置关键逻辑适用场景连接层WebSocketManager.cs的Dictionarystring, WebSocketKey 为connectionIdGUIDValue 为WebSocket对象设备唯一身份绑定如 MAC 地址哈希主题层MessageRouter.cs的ConcurrentDictionarystring, Liststring topicSubscribersKey 为topic如plc/s7-1200/axis1Value 为订阅该 topic 的connectionId列表多台 PLC 订阅不同轴系数据处理器层MessageRouter.cs的Dictionarystring, Funcstring, string, Task handlersKey 为command如read_registerValue 为异步处理委托解析 JSON 指令并调用 OPC UA 客户端为什么这样设计工业现场常见“一设备多用途”同一台汇川 PLC 既要上报温度topicsensor/temp又要接收启停指令commandmotor_start。若用单一Broadcast所有连接都会收到无关消息增加网络负载和客户端解析负担。三层路由让服务端精准投递降低边缘设备 CPU 占用。3.2 注册自定义业务处理器以“西门子读取 DB 块”为例假设需响应客户端发送的{cmd:read_db,db:10,start:0,length:4}请求步骤如下在Startup.cs的ConfigureServices方法末尾添加services.AddSingletonMessageRouter(); services.AddSingletonPlcDataHandler(); // ← 新增处理器类创建Services/PlcDataHandler.cspublic class PlcDataHandler { private readonly ILoggerPlcDataHandler _logger; public PlcDataHandler(ILoggerPlcDataHandler logger) _logger logger; public async Task HandleReadDbRequest(string connectionId, string jsonPayload) { try { var req JsonSerializer.DeserializeReadDbRequest(jsonPayload); // 此处调用西门子 S7.NET 库读取 DB 块需 NuGet 安装 S7NetPlus var plc new Plc(CpuType.S71200, 192.168.1.200, 0, 1); await plc.OpenAsync(); var data await plc.ReadBytesAsync(DataType.DataBlock, req.Db, req.Start, req.Length); var response new { success true, data Convert.ToBase64String(data) }; await MessageRouter.Instance.SendToConnection(connectionId, JsonSerializer.Serialize(response)); } catch (Exception ex) { _logger.LogError(ex, PLC 读取失败); await MessageRouter.Instance.SendToConnection(connectionId, JsonSerializer.Serialize(new { success false, error ex.Message })); } } } public class ReadDbRequest { public int Db { get; set; } public int Start { get; set; } public int Length { get; set; } }在MessageRouter.cs的InitializeHandlers方法中注册handlers[read_db] async (connId, payload) { var handler serviceProvider.GetServicePlcDataHandler(); await handler.HandleReadDbRequest(connId, payload); };3.3 客户端发送结构化指令JSON 协议规范与字段校验服务端对command字段执行强校验非法指令会被静默丢弃不返回错误。合法 JSON 消息必须包含字段类型必填说明cmdstring✓对应handlers字典中的 Key如read_dbidstring✗客户端生成的请求 ID用于服务端响应关联可选dataobject✗任意结构化数据由处理器自行反序列化示例西门子读取 DB{ cmd: read_db, id: req_20240615_001, data: { db: 10, start: 0, length: 4 } }参数说明id字段非强制但强烈建议添加——当服务端并发处理多个请求时客户端可通过id匹配响应避免顺序错乱。data内容完全由处理器决定服务端不做 Schema 校验降低耦合。3.4 避坑WebSocket 连接生命周期管理的四个致命陷阱现象 1客户端断网重连后旧连接未释放activeConnections持续增长原因服务端未监听WebSocket.CloseAsync完成事件WebSocketManager.RemoveConnection调用时机错误。解决在WebSocketManager.cs的AcceptWebSocketAsync方法中必须将CloseAsync封装为Task.Run并 await// ❌ 错误未 await CloseAsync连接对象残留 ws.CloseAsync(WebSocketCloseStatus.NormalClosure, , CancellationToken.None); // ✅ 正确await 确保连接彻底关闭后再移除 await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, , CancellationToken.None); _connectedSockets.TryRemove(connectionId, out _);现象 2高并发下MessageRouter.SendToTopic抛出NullReferenceException原因topicSubscribers字典被多线程并发修改ConcurrentDictionary的GetOrAdd未处理null返回。解决在SubscribeToTopic方法中强制初始化空列表var subscribers _topicSubscribers.GetOrAdd(topic, _ new Liststring()); if (!subscribers.Contains(connectionId)) subscribers.Add(connectionId);现象 3心跳包导致WebSocket连接意外关闭closeCode1001Going Away原因客户端未实现onping事件响应服务端HeartbeatService发送Ping帧后超时未收到Pong。解决在客户端 JavaScript 中添加ws.onping () { // 浏览器自动回复 Pong无需手动处理 console.log( 收到 Ping自动响应); };注意Chrome/Firefox 自动处理Ping/Pong但某些嵌入式 WebView如 Qt WebEngine需手动调用ws.send()回复。现象 4SendToConnection发送二进制数据时客户端收到乱码原因服务端未设置WebSocketMessageType.Binary默认以Text类型发送字节数组。解决在MessageRouter.cs的SendToConnection方法中根据消息类型显式指定var buffer Encoding.UTF8.GetBytes(jsonString); await ws.SendAsync(new ArraySegmentbyte(buffer), WebSocketMessageType.Text, true, CancellationToken.None); // 二进制发送示例如传输图片 // await ws.SendAsync(new ArraySegmentbyte(binaryData), WebSocketMessageType.Binary, true, CancellationToken.None);4. 生产级加固TLS 加密、进程守护与跨平台部署4.1 启用 HTTPS WSS用自签名证书快速验证非生产推荐Windows 环境下启用 TLS 需两步生成 PFX 证书PowerShell 执行$cert New-SelfSignedCertificate -DnsName localhost -CertStoreLocation cert:\LocalMachine\My -KeyExportPolicy Exportable Export-PfxCertificate -Cert $cert -FilePath server.pfx -Password (ConvertTo-SecureString -String yourpassword -Force -AsPlainText)修改Program.cs绑定 HTTPS 端口webBuilder.UseKestrel(options { options.ListenAnyIP(5001, listenOptions { listenOptions.UseHttps(server.pfx, yourpassword); listenOptions.Protocols HttpProtocols.Http1AndHttp2; }); });参数说明5001为 HTTPS 默认端口WSS 连接地址变为wss://localhost:5001/wsUseHttps第二个参数是 PFX 密码必须与导出时一致否则 Kestrel 启动失败报Invalid password for certificate。4.2 Linux systemd 服务化部署让 WebSocketServer 开机自启在 Ubuntu 22.04 上部署步骤发布为自包含应用确保目标机器无 .NET Runtimedotnet publish -c Release -r linux-x64 --self-contained true -o ./publish创建 systemd 服务文件/etc/systemd/system/websocket-server.service[Unit] DescriptionC# WebSocket Server Afternetwork.target [Service] Typenotify Userubuntu WorkingDirectory/opt/websocket-server ExecStart/opt/websocket-server/WebSocketServer Restartalways RestartSec10 KillSignalSIGINT EnvironmentASPNETCORE_ENVIRONMENTProduction EnvironmentDOTNET_PRINT_TELEMETRY_MESSAGEfalse [Install] WantedBymulti-user.target启用服务sudo systemctl daemon-reload sudo systemctl enable websocket-server.service sudo systemctl start websocket-server.service sudo journalctl -u websocket-server.service -f # 实时查看日志关键参数Typenotify允许 Kestrel 通过systemd通知就绪状态RestartSec10避免频繁重启触发 systemd 限流EnvironmentDOTNET_PRINT_TELEMETRY_MESSAGEfalse禁用 .NET 遥测符合工业环境合规要求。4.3 Docker 容器化最小镜像与多架构支持Dockerfile 使用mcr.microsoft.com/dotnet/aspnet:6.0-alpine基础镜像体积 50MBFROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS runtime WORKDIR /app COPY ./publish . EXPOSE 5000 ENTRYPOINT [./WebSocketServer]构建命令支持 ARM64 设备docker buildx build --platform linux/amd64,linux/arm64 -t websocket-server:latest --push .优势说明Alpine 镜像无 glibc 依赖可在树莓派 4B、NVIDIA Jetson Nano 等 ARM 设备原生运行buildx多平台构建避免交叉编译环境配置直接产出arm64镜像供边缘设备拉取。4.4 避坑生产环境必须关闭的三个调试开关陷阱 1appsettings.Development.json未删除导致敏感信息泄露现象/api/status接口返回EnvironmentDevelopment且DetailedErrorstrue暴露堆栈。解决发布前删除appsettings.Development.json或在Startup.cs中强制设置if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); // 仅开发环境启用 // 生产环境禁用所有 Development 特性 }陷阱 2Kestrel 默认未限制请求体大小遭恶意上传攻击现象客户端发送 1GB JSON 消息服务端 OOM 崩溃。解决在appsettings.json中添加Kestrel: { Limits: { MaxRequestBodySize: 10485760 // 10MB单位字节 } }陷阱 3WebSocket 连接数无上限耗尽系统文件描述符现象Linux 系统报Too many open filesulimit -n显示默认 1024。解决在 systemd 服务文件中增加[Service] ... LimitNOFILE65536并重启服务sudo systemctl daemon-reload sudo systemctl restart websocket-server.service。5. 工业现场实战技巧OPC UA 桥接、断线重连策略与内存泄漏排查5.1 OPC UA 到 WebSocket 的协议桥接复用现有 S7.NET 代码该源码预留了OPCUABridge.cs空类实际桥接逻辑如下建立 OPC UA 客户端连接使用Opc.UaFx.ClientNuGet 包var client new UaTcpSessionChannel( new Uri(opc.tcp://192.168.1.200:4840), new UserIdentity(new AnonymousIdentity())); await client.ConnectAsync();订阅节点变化并转发至 WebSocketvar subscription client.CreateSubscription(1000); // 1000ms 刷新间隔 var monitoredItem subscription.CreateMonitoredItem( new MonitoredItemArguments( new NodeId(ns2;sChannel1.Device1.Temperature), MonitoringMode.Reporting)); monitoredItem.Notification (sender, e) { var value e.Value.ToString(); // 转发到所有订阅 topicsensor/temp 的连接 MessageRouter.Instance.SendToTopic(sensor/temp, JsonSerializer.Serialize(new { timestamp DateTime.UtcNow, value })); }; await subscription.ApplyChangesAsync();关键点NodeId格式必须与 OPC UA 服务器地址空间一致MonitoringMode.Reporting确保值变更时推送而非轮询。5.2 客户端断线重连策略指数退避 连接池复用前端 JavaScript 的健壮重连逻辑避免雪崩式重试class ReliableWebSocket { constructor(url, protocols, maxRetries 5) { this.url url; this.protocols protocols; this.maxRetries maxRetries; this.retryCount 0; this.ws null; this.connect(); } connect() { this.ws new WebSocket(this.url, this.protocols); this.ws.onopen () { console.log(✅ 连接成功); this.retryCount 0; // 重置计数器 }; this.ws.onclose (e) { if (this.retryCount this.maxRetries) { const delay Math.min(1000 * Math.pow(2, this.retryCount), 30000); // 1s→2s→4s...最大30s console.log( ${delay}ms 后重连...); setTimeout(() this.connect(), delay); this.retryCount; } else { console.error( 达到最大重试次数停止连接); } }; } } // 使用 const ws new ReliableWebSocket(wss://server:5001/ws, [json-v1]);参数说明Math.pow(2, retryCount)实现指数退避防止 100 台设备同时重连压垮服务端Math.min(..., 30000)设置上限避免无限等待。5.3 内存泄漏定位用 dotnet-dump 分析托管堆当activeConnections持续增长但GC.GetTotalMemory不降时执行捕获内存快照Linuxdotnet-dump collect -p $(pgrep -f WebSocketServer) -o /tmp/dump_$(date %s).dump分析对象引用链dotnet-dump analyze /tmp/dump_*.dump dumpheap -stat # 查看对象数量TOP 10 dumpheap -mt WebSocketManager地址 # 定位 WebSocketManager 实例 gcroot WebSocket对象地址 # 追踪 GC Root确认是否被静态字典持有典型泄漏点WebSocketManager._connectedSockets字典未及时TryRemoveMessageRouter._topicSubscribers中Liststring持有已断开连接的connectionId。5.4 验证服务稳定性用 wrk 压测 500 并发连接使用wrk工具模拟真实设备连接# 安装 wrkUbuntu sudo apt install wrk # 压测脚本 test_ws.lua wrk.method GET wrk.headers[Sec-WebSocket-Version] 13 wrk.headers[Sec-WebSocket-Key] dGhlIHNhbXBsZSBub25jZQ wrk.headers[Connection] Upgrade wrk.headers[Upgrade] websocket # 执行压测持续 60 秒500 并发每秒新建 10 连接 wrk -t12 -c500 -d60s --scripttest_ws.lua http://192.168.1.100:5000/ws预期结果activeConnections稳定在 500±5memoryUsageMB波动 10MB无连接超时。从那以后我每次交付工业网关项目都会在WebSocketManager.RemoveConnection方法里加一行Console.WriteLine($Removed {connectionId} at {DateTime.Now:HH:mm:ss})并用journalctl -u websocket-server.service | grep Removed实时确认连接释放。这行日志成了我的后悔药——它能第一时间暴露CloseAsync未 await 的 bug比任何 APM 工具都直接。希望帮到你。本文还有配套的精品资源点击获取
返回列表