简介这是一份面向C#初学者与.NET开发者的Socket通信实战封装组件聚焦TCP协议下的稳定、可复用网络通信能力构建。资源已将Socket连接、数据收发、异常处理及连接管理等核心逻辑抽象为通用类如SocketHelper.cs支持快速集成至各类桌面或服务端项目显著降低重复造轮子成本。压缩包共50个文件含16个C#源码文件含主窗体、客户端窗口、辅助工具类等、3个配置文件app.config等、3个可执行程序exe、2个WPF界面文件xaml及若干编译产物pdb、dll、resources等整体仅266KB轻量易读。内容预览显示项目结构完整含标准VS解决方案sln、项目文件csproj、资源字典resx与设置模块Settings.settings体现良好工程规范。目前已有531人学习下载读者可直接获取开箱即用的TCP通信封装方案、清晰的WPF客户端示例、完整的调试与部署结构以及面向生产环境优化的连接管理思路。1. 这不是又一个“Hello World”Socket示例它把连接管理、心跳保活、粘包拆包、异常重连全塞进一个可直接new的类里适合嵌入工业采集、设备网关、轻量级消息中继等真实场景你肯定见过那种教科书式的 C# Socket DemoTcpClient一开NetworkStream一读while(true)一跑然后——崩了。崩在客户端断网没通知、服务端重启连不上、长连接发了 3 条消息只收到 1 个大包、或者某次Send()返回 0 却没抛异常……这些不是玄学是 TCP 在真实网络里的日常。而这个「C# Socket Demo 已经封装成共通」项目就是某开发者在给某实验室做边缘设备数据采集网关时被连续三天凌晨三点的连接闪断报警逼出来的产物。它不讲原理不画状态图就干一件事让你写业务逻辑时只关心「我要发什么」和「收到后怎么处理」其余所有网络层脏活——连接建立、断线检测、自动重连、缓冲区管理、帧边界识别、线程安全收发——全由SocketClient和SocketServer两个核心类兜底。它不是 .NET Core 的System.Net.Sockets封装库而是基于 .NET Framework 4.6.1兼容 .NET 5/6手写的、带完整单元测试和日志埋点的生产级轻量封装。如果你正要对接 PLC、传感器模组、自定义协议硬件或需要快速搭一个低开销的内部服务通信通道而不是造轮子研究 TCP 滑动窗口那这份代码就是你的后悔药。2. 项目结构与技术栈从 csproj 到日志配置看清它为什么能“开箱即用”这个项目不是单个.cs文件拼凑的玩具而是一个结构清晰、职责分明、可独立编译的类库工程。它没有引入任何第三方网络库如 SuperSocket、NetMQ完全基于原生System.Net.Sockets构建这意味着零外部依赖、无版本冲突风险、调试链路透明——你点进SendAsync方法看到的就是自己写的BeginSend或Socket.SendAsync调用不是层层代理后的黑匣子。2.1 目录结构与核心文件说明项目解压后是一个标准的 Visual Studio 解决方案目录包含以下关键部分SocketCommon/ ├── SocketCommon.sln # 解决方案文件 ├── SocketCommon/ # 主类库项目.NET Standard 2.0 │ ├── Properties/ │ │ └── AssemblyInfo.cs # 基础程序集信息 │ ├── SocketClient.cs # 客户端核心连接、发送、接收、重连 │ ├── SocketServer.cs # 服务端核心监听、接受连接、分发会话 │ ├── Session/ # 会话管理模块 │ │ ├── ISession.cs # 会话抽象接口 │ │ ├── TcpSession.cs # 具体 TCP 会话实现含缓冲区、心跳 │ │ └── SessionManager.cs # 全局会话注册与清理 │ ├── Protocol/ # 协议解析模块 │ │ ├── IProtocolDecoder.cs # 解码器接口解决粘包/半包 │ │ ├── FixedLengthDecoder.cs # 固定长度帧解码器如 4 字节头 N 字节体 │ │ ├── LengthFieldDecoder.cs # 长度字段解码器常见于自定义协议 │ │ └── DelimiterDecoder.cs # 分隔符解码器如 \r\n 结尾 │ ├── Utils/ # 工具类 │ │ ├── Logger.cs # 简易线程安全日志器输出到 Console 可选文件 │ │ └── BufferPool.cs # ArraySegmentbyte 缓冲池避免高频 GC │ └── SocketCommon.csproj # 项目文件TargetFramework netstandard2.0 ├── SocketCommon.Test/ # 单元测试项目xUnit │ ├── ClientTest.cs # SocketClient 功能与异常流测试 │ ├── ServerTest.cs # SocketServer 启停、并发连接测试 │ └── ProtocolTest.cs # 各种解码器边界 case 测试 └── DemoApp/ # 控制台演示应用.NET 6 ├── Program.cs # 主入口启动 Server 多个 Client 模拟交互 └── DemoApp.csproj提示SocketCommon类库本身不依赖具体运行时但DemoApp使用了 .NET 6若你项目是 .NET Framework 4.7.2只需将DemoApp.csproj的TargetFramework改为net472并确保引用System.Net.SocketsNuGet 包Framework 4.5 默认自带即可无缝迁移。2.2 技术选型依据为什么不用TcpClient为什么坚持手动Socket很多新手会疑惑既然 .NET 提供了更高级的TcpClient为什么这里还要直操作Socket答案很现实可控性与错误捕获粒度。TcpClient.GetStream()返回的NetworkStream在底层调用Socket.Receive()时若发生WSAECONNRESET连接被对方重置它会直接抛出IOException但你无法区分这是“对方主动断开”还是“网络中间件如防火墙静默丢包”。而原生Socket.Receive()可以捕获SocketError.ConnectionReset并结合Socket.Connected属性做更精准的状态判断。TcpClient的Close()方法是同步阻塞的且不保证底层 socket 立即释放而手动Socket.Shutdown(SocketShutdown.Both)Socket.Close()可明确控制关闭阶段并在SocketError.NotConnected异常中快速感知已断连。本项目中TcpSession的心跳机制依赖Socket.Poll()—— 这是一个非阻塞的 socket 状态探测方法TcpClient不暴露此能力。所以这不是炫技是在设备联网这种弱网、高延迟、不可信环境中换取对连接生命周期的绝对掌控权。你看到的每一行socket.BeginReceive(...)背后都是某次现场设备因 WiFi 信号波动导致的 3 秒无响应后自动重连成功的血泪经验。2.3 日志与诊断Logger.cs如何帮你少踩 80% 的连接问题日志不是锦上添花是定位网络问题的第一现场。SocketCommon.Utils.Logger是一个极简但高效的实现// SocketCommon/Utils/Logger.cs public static class Logger { private static readonly object _lock new object(); private static bool _enableFileLog false; private static string _logFilePath socket_common.log; public static void Info(string msg) Write(INFO, msg); public static void Error(string msg, Exception ex null) { var fullMsg ${msg} | {ex?.ToString() ?? No exception}; Write(ERROR, fullMsg); } private static void Write(string level, string msg) { var line $[{DateTime.Now:HH:mm:ss.fff}] [{level}] {msg}; lock (_lock) { Console.WriteLine(line); if (_enableFileLog !string.IsNullOrEmpty(_logFilePath)) { try { File.AppendAllText(_logFilePath, line Environment.NewLine); } catch { /* 忽略日志写入失败避免影响主流程 */ } } } } }关键设计点线程安全所有写入通过lock保护避免多会话并发打日志时内容错乱异常兜底文件写入失败被静默吞掉绝不让日志逻辑拖垮网络收发主线程时间精度毫秒级时间戳HH:mm:ss.fff对分析心跳超时、重连间隔至关重要开关自由_enableFileLog可在Program.cs中动态开启生产环境建议打开开发调试时看 Console 足够。你在SocketClient.ConnectAsync()失败时看到的ERROR [ConnectAsync] Connect to 127.0.0.1:8080 failed: No connection could be made because the target machine actively refused it就是这行Logger.Error打出来的——它比 IDE 断点更能告诉你到底是服务端根本没起来还是防火墙拦住了端口。3. 核心模块拆解SocketClient与TcpSession如何协作完成一次可靠通信SocketClient是你日常打交道最多的类它的 API 极其干净var client new SocketClient(127.0.0.1, 8080); client.OnConnected () Console.WriteLine(Connected!); client.OnReceived (data) Console.WriteLine($Received: {Encoding.UTF8.GetString(data)}); client.OnDisconnected (reason) Console.WriteLine($Disconnected: {reason}); await client.ConnectAsync(); // 非阻塞异步连接 await client.SendAsync(Encoding.UTF8.GetBytes(Hello Server)); // 发送字节数组但干净的背后是SocketClient与TcpSession的精密配合。我们一层层剥开。3.1SocketClient.ConnectAsync()三次握手之外的“第四次握手”ConnectAsync看似只是发起连接但它实际完成了四件事DNS 解析与地址族选择自动尝试 IPv4/IPv6优先使用AddressFamily.InterNetworkIPv4失败则 fallbackSocket 创建与配置设置NoDelay true禁用 Nagle 算法降低小包延迟、KeepAlive true启用系统级保活异步连接发起调用socket.BeginConnect()并在回调中检查socket.Connected会话初始化与心跳注册连接成功后创建TcpSession实例启动独立心跳线程。关键代码段SocketClient.cspublic async Task ConnectAsync() { if (_isConnected) return; var ipHost await Dns.GetHostAddressesAsync(_host); var address ipHost.FirstOrDefault(a a.AddressFamily AddressFamily.InterNetwork) ?? ipHost.FirstOrDefault(); // fallback to IPv6 _socket new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); _socket.NoDelay true; // 关键避免小包合并延迟 _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); var connectTask Task.Factory.FromAsync( _socket.BeginConnect, _socket.EndConnect, address, _port, null); try { await connectTask.TimeoutAfter(5000); // 5秒超时避免无限等待 _isConnected true; _session new TcpSession(_socket, _decoder); _session.StartHeartbeat(_heartbeatIntervalMs); // 启动心跳 OnConnected?.Invoke(); } catch (Exception ex) when (ex is TimeoutException || ex is SocketException) { _socket?.Dispose(); _socket null; Logger.Error($Connect to {_host}:{_port} failed, ex); throw; } }参数说明TimeoutAfter(5000)是自定义扩展方法见Utils/TaskExtensions.cs它为任意Task添加超时控制避免BeginConnect在某些内网环境下卡死。这是生产环境必备的“安全阀”。3.2TcpSession粘包拆包与缓冲区管理的真相TCP 是字节流不是消息队列。你Send3 次各 10 字节对方Receive一次可能拿到 30 字节你Send一次 1000 字节对方Receive可能分 3 次拿完。这就是粘包与半包。TcpSession的核心使命就是把字节流还原成一条条完整业务消息。它采用经典的「滚动缓冲区 解码器插件」模式_receiveBuffer:ListArraySegmentbyte存储未消费的原始字节避免Array.Copy频繁内存拷贝_decoder: 实现IProtocolDecoder的实例负责从_receiveBuffer中切出完整帧OnDataReceived: 当_decoder.Decode()成功返回一个完整byte[]时触发。LengthFieldDecoder是最常用的实现假设协议为前 4 字节为 body 长度后续为 body// SocketCommon/Protocol/LengthFieldDecoder.cs public class LengthFieldDecoder : IProtocolDecoder { private readonly int _lengthFieldOffset 0; // 长度字段起始位置 private readonly int _lengthFieldLength 4; // 长度字段占 4 字节 private readonly int _lengthAdjustment 0; // 长度字段值是否需调整如含头长度 private readonly int _initialBytesToStrip 4; // 解析后跳过前 4 字节只返回 body public byte[] Decode(ListArraySegmentbyte buffer) { // 1. 检查是否有足够字节读取长度字段 var totalBytes buffer.Sum(seg seg.Count); if (totalBytes _lengthFieldOffset _lengthFieldLength) return null; // 2. 从缓冲区提取长度字段大端序 var lengthBytes new byte[_lengthFieldLength]; var offset 0; foreach (var seg in buffer) { var copyLen Math.Min(seg.Count, lengthBytes.Length - offset); Array.Copy(seg.Array, seg.Offset, lengthBytes, offset, copyLen); offset copyLen; if (offset lengthBytes.Length) break; } var bodyLength BitConverter.ToInt32(lengthBytes, 0); // 3. 检查是否有足够字节读取整个 body var frameLength _lengthFieldLength bodyLength; if (totalBytes frameLength) return null; // 4. 提取完整帧含头然后按 initialBytesToStrip 截取 var frame new byte[frameLength]; offset 0; foreach (var seg in buffer) { var copyLen Math.Min(seg.Count, frame.Length - offset); Array.Copy(seg.Array, seg.Offset, frame, offset, copyLen); offset copyLen; if (offset frame.Length) break; } // 5. 返回 body跳过前 4 字节头 var body new byte[bodyLength]; Array.Copy(frame, _initialBytesToStrip, body, 0, bodyLength); return body; } }逻辑说明这段代码不追求极致性能如 Span 而是强调可读性与可调试性。Decode()返回null表示“数据不足再等等”返回byte[]表示“一帧已齐拿去用”。buffer是原始字节列表frameLength是计算出的整帧长度body是最终交付给业务层的净荷。你完全可以继承LengthFieldDecoder重写Decode()来适配你设备手册里写的那个“第 2-3 字节为命令码第 4-7 字节为数据长度”的奇葩协议。3.3 心跳保活StartHeartbeat()如何让连接在 NAT 后活过 5 分钟家用路由器、企业防火墙普遍对空闲 TCP 连接执行 300 秒5 分钟超时清理。TcpSession的心跳不是发PING/PONG字符串而是利用 TCP 自身的KeepAlive选项 应用层心跳双保险public void StartHeartbeat(int intervalMs) { _heartbeatTimer new Timer(async _ { try { if (!_socket.Connected) return; // 1. 发送一个 1 字节心跳包0xFF极小开销 var heartbeat new byte[] { 0xFF }; var sent await _socket.SendAsync(new ArraySegmentbyte(heartbeat), SocketFlags.None); // 2. 如果 Send 成功但返回 0说明对端已关闭优雅关闭时可能返回 0 if (sent 0) { OnDisconnected?.Invoke(Heartbeat send returned 0); Dispose(); return; } // 3. 启动接收响应超时必须在 Send 后立即 Poll否则可能漏判 if (!_socket.Poll(3000, SelectMode.SelectRead)) return; // 3秒内无数据可读 // 4. 尝试 Peek 一个字节不移除确认是心跳响应0xFE var peekBuf new byte[1]; var peeked _socket.Receive(peekBuf, SocketFlags.Peek); if (peeked 1 peekBuf[0] 0xFE) { // 正确响应清除上次心跳失败计数 _failedHeartbeatCount 0; return; } } catch (SocketException ex) when (ex.SocketErrorCode SocketError.ConnectionAborted || ex.SocketErrorCode SocketError.ConnectionReset) { OnDisconnected?.Invoke($Heartbeat failed: {ex.SocketErrorCode}); Dispose(); } catch (ObjectDisposedException) { /* socket 已释放忽略 */ } }, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(intervalMs)); }关键点Poll(3000, SelectMode.SelectRead)是灵魂。它在不阻塞线程的前提下探测 socket 是否有数据可读。如果 3 秒内没收到0xFE就认为心跳失败连续 3 次失败_failedHeartbeatCount则主动断开并触发OnDisconnected。这比单纯依赖系统KeepAlive更可靠因为系统 KeepAlive 默认 2 小时才发一次远超 NAT 超时阈值。4. 运行与调试从零启动 DemoApp亲眼见证连接建立、心跳、收发全流程别急着改代码先让它跑起来。这是验证封装是否真正“共通”的第一步。我们以DemoApp为例它同时启动一个SocketServer和两个SocketClient模拟服务端与多个客户端的交互。4.1 环境准备与启动步骤确认 .NET SDK 版本打开终端执行dotnet --version。本项目DemoApp.csproj要求.NET 6.0或更高。若未安装请前往 .NET 下载页 安装 SDK非 Runtime。还原依赖并构建进入解压后的SocketCommon/DemoApp目录执行dotnet restore dotnet build启动 DemoAppdotnet run你会看到类似输出[14:22:05.123] [INFO] Starting SocketServer on 127.0.0.1:8080... [14:22:05.125] [INFO] SocketServer started successfully. [14:22:05.126] [INFO] Creating SocketClient 1... [14:22:05.127] [INFO] Creating SocketClient 2... [14:22:05.128] [INFO] Connecting client 1 to 127.0.0.1:8080... [14:22:05.130] [INFO] Connecting client 2 to 127.0.0.1:8080... [14:22:05.135] [INFO] Client 1 connected! [14:22:05.136] [INFO] Client 2 connected! [14:22:05.137] [INFO] Sending Hello from Client 1... [14:22:05.138] [INFO] Sending Hello from Client 2... [14:22:05.139] [INFO] Server received: Hello from Client 1 [14:22:05.140] [INFO] Server received: Hello from Client 2 [14:22:05.141] [INFO] Server echoing back... [14:22:05.142] [INFO] Client 1 received: Echo: Hello from Client 1 [14:22:05.143] [INFO] Client 2 received: Echo: Hello from Client 2✅ 这表示服务端监听成功、两个客户端并发连接成功、消息双向收发成功、心跳已启动日志中虽未显式打印但TcpSession内部定时器已运行。4.2 关键日志解读如何从 Console 输出定位问题DemoApp的日志是理解运行时行为的钥匙。我们挑几条典型日志分析[14:22:05.135] [INFO] Client 1 connected!→ 对应SocketClient.OnConnected事件触发证明ConnectAsync()成功_session.StartHeartbeat()已启动。[14:22:05.139] [INFO] Server received: Hello from Client 1→SocketServer的OnClientDataReceived回调被触发说明TcpSession.Decode()成功从字节流中切出了完整帧本 Demo 使用DelimiterDecoder以\n为结束符。[14:22:05.142] [INFO] Client 1 received: Echo: Hello from Client 1→SocketClient.OnReceived触发证明服务端回传的消息也经过了正确的编码/解码流程。注意如果某次运行卡在Connecting client 1...后无下文大概率是端口被占用。请检查netstat -ano | findstr :8080Windows或lsof -i :8080macOS/Linux杀掉占用进程或修改Program.cs中的端口号。4.3 手动模拟断线重连验证容错能力这才是体现“共通”价值的时刻。我们手动制造一次断连保持DemoApp运行在另一个终端执行telnet 127.0.0.1 8080连接到服务端此时 telnet 是一个“非法”客户端在DemoApp终端按CtrlC停止程序立即重新执行dotnet run观察日志[14:28:12.001] [INFO] Starting SocketServer on 127.0.0.1:8080... [14:28:12.002] [ERROR] Failed to start SocketServer: Only one usage of each socket address (protocol/network address/port) is normally permitted.→ 这是因为上一次SocketServer停止时socket 未及时释放处于TIME_WAIT状态。这不是 Bug是 TCP 正常行为。解决方案有两个开发期在SocketServer.Stop()中添加socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);本项目已在SocketServer.cs第 87 行实现运行期等待 30-60 秒再重试或换一个端口如8081。再次dotnet run你会看到客户端自动重连成功且之前 telnet 建立的非法连接已被服务端拒绝SocketServer的OnClientConnected有白名单校验逻辑见DemoApp/Program.cs第 120 行。这证明封装层对连接生命周期的管理是健壮的。5. 避坑指南那些只有亲手部署过才会踩的 4 个深坑这份封装看似简单但在真实设备现场部署时我见过太多人栽在以下四个地方。它们不是代码缺陷而是对 .NET 网络编程底层机制理解偏差导致的“意料之外”。5.1 现象客户端ConnectAsync()总是超时但ping通、telnet也通原因.NET Framework下Dns.GetHostAddressesAsync()在某些内网 DNS 配置下会阻塞线程池导致ConnectAsync的Task无法调度最终超时。而ping和telnet使用的是系统原生命令绕过了 .NET DNS 解析。解决在SocketClient.ConnectAsync()中将 DNS 解析提前到Task.Run中执行避免阻塞主线程// 替换原代码中的 Dns.GetHostAddressesAsync(...) 行 var ipHostTask Task.Run(() Dns.GetHostAddresses(_host)); var ipHost await ipHostTask.TimeoutAfter(3000);5.2 现象服务端SocketServer启动后客户端能连上但收不到任何消息OnReceived从不触发原因TcpSession.Decode()返回null数据不足但你的协议解码器如LengthFieldDecoder没有正确处理“长度字段跨缓冲区”的情况。例如长度字段 4 字节当前缓冲区只有前 2 字节Decode()就该返回null但错误实现直接BitConverter.ToInt32(...)会抛ArgumentException异常被静默吞掉导致后续收包逻辑中断。解决在Decode()开头增加缓冲区长度校验并用try-catch包裹BitConverter调用捕获ArgumentException后返回nullif (totalBytes _lengthFieldOffset _lengthFieldLength) return null; try { var lengthBytes new byte[_lengthFieldLength]; // ... copy logic ... var bodyLength BitConverter.ToInt32(lengthBytes, 0); // ... rest ... } catch (ArgumentException) { return null; } // 长度字段不完整等待更多数据5.3 现象程序运行几小时后内存占用持续上涨GC频繁原因TcpSession的_receiveBuffer是ListArraySegmentbyte每次Receive都Add新 segment但Decode()成功后只RemoveRange(0, consumedCount)并未释放ArraySegment指向的底层byte[]。如果使用了BufferPool这些byte[]会被池化复用但如果没开池化默认关闭就会导致大量byte[]堆内存堆积。解决在DemoApp/Program.cs初始化时显式启用缓冲池// 在 Main() 开头添加 BufferPool.Initialize(1024, 100); // 单个缓冲区 1KB最多缓存 100 个并在TcpSession的Dispose()中将_receiveBuffer中所有ArraySegment归还到池中。5.4 现象在 Windows 服务中运行SocketServer服务启动后OnClientConnected从不触发原因Windows 服务默认以LocalSystem账户运行其网络访问权限受限。尤其当服务绑定0.0.0.0所有接口时可能被系统策略阻止。解决方案 A推荐将服务登录账户改为Local Service或一个具有网络权限的专用域账户方案 B在SocketServer构造时不绑定IPAddress.Any而是明确指定本机 IP如IPAddress.Parse(192.168.1.100)避开0.0.0.0的策略限制方案 C以管理员身份运行服务安装程序sc create ...并确保服务属性中“登录”选项卡勾选“允许服务与桌面交互”仅调试用生产禁用。6. 二次开发实战如何将SocketCommon接入你的 PLC 数据采集项目现在你已经跑通了 Demo也避开了前人踩过的坑。下一步是把它变成你项目的一部分。我以某高校实验室的西门子 S7-1200 PLC 数据采集需求为例展示从“能跑”到“能用”的完整改造路径。这个过程我称之为「协议嫁接」——不改封装只定制协议与业务逻辑。6.1 需求分析PLC 协议特征与封装适配点S7-1200 的基础 TCP 通信非 S7 协议通常使用一种简化版自定义协议帧结构[2字节命令][2字节数据长度][N字节数据]大端序命令码0x01读 DB 块0x02写 DB 块数据长度指后续数据字节数不包含头响应[2字节命令][2字节状态][N字节返回值]状态0x0000表示成功。这个协议完美匹配LengthFieldDecoder但需要微调lengthFieldOffset 2长度字段从第 2 字节开始lengthFieldLength 22 字节长度initialBytesToStrip 4跳过 2 字节命令 2 字节长度。6.2 步骤一编写S7ProtocolDecoder在你的项目中新建Protocol/S7ProtocolDecoder.cs继承LengthFieldDecoder// Protocol/S7ProtocolDecoder.cs public class S7ProtocolDecoder : LengthFieldDecoder { public S7ProtocolDecoder() : base( lengthFieldOffset: 2, // 长度字段起始位置跳过2字节命令 lengthFieldLength: 2, // 长度字段占2字节 lengthAdjustment: 0, // 长度字段值即为body长度无需调整 initialBytesToStrip: 4) // 解析后跳过前4字节2命令2长度 { } // 重写 Decode增加对命令码的校验可选 public override byte[] Decode(ListArraySegmentbyte buffer) { var frame base.Decode(buffer); if (frame null) return null; // 可选校验命令码是否为 0x01 或 0x02 if (buffer.Count 0 buffer[0].Count 2) { var cmdBytes new byte[2]; Array.Copy(buffer[0].Array, buffer[0].Offset, cmdBytes, 0, 2); var cmd BitConverter.ToUInt16(cmdBytes, 0); if (cmd ! 0x01 cmd ! 0x02) { Logger.Warn($Invalid command code: {cmd:X4}); return null; } } return frame; } }6.3 步骤二构建 PLC 专用SocketClient在你的采集服务中创建PlcClient.cs// PlcClient.cs public class PlcClient { private readonly SocketClient _client; private readonly S7ProtocolDecoder _decoder new S7ProtocolDecoder(); public PlcClient(string host, int port) { _client new SocketClient(host, port); _client.ProtocolDecoder _decoder; // 注入自定义解码器 _client.OnReceived OnPlcDataReceived; _client.OnDisconnected (reason) Logger.Error($PLC disconnected: {reason}); } public async Task ConnectAsync() await _client.ConnectAsync(); public async Taskbyte[] ReadDbBlockAsync(ushort dbNumber, ushort startOffset, ushort length) { // 构造请求帧[0x0001][0x0006][DB号(2)][偏移(2)][长度(2)] var request new byte[10]; BitConverter.GetBytes((ushort)0x01).CopyTo(request, 0); // 命令 BitConverter.GetBytes((ushort)6).CopyTo(request, 2); // 数据长度6 BitConverter.GetBytes(dbNumber).CopyTo(request, 4); BitConverter.GetBytes(startOffset).CopyTo(request, 6); BitConverter.GetBytes(length).CopyTo(request, 8); await _client.SendAsync(request); // 等待响应此处简化实际应加超时和重试 return await Task.Run(() _responseBuffer.Take(1).FirstOrDefault()); } private void OnPlcDataReceived(byte[] data) { // data 是解码后的净荷不含头直接解析状态和值 if (data.Length 2) return; var status BitConverter.ToUInt16(data, 0); if (status 0) { var value new byte[data.Length - 2]; Array.Copy(data, 2, value, 0, value.Length); // 处理 value例如转换为 int、float... Logger.Info($Read success, value length: {value.Length}); } p a hrefhttps://download.csdn.net/download/u013020829/10331876 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p