文章目录
- C# 工业设备通讯系列技术博文(持续更新...)
- 第一篇:【C# 工业通讯】封装一个健壮的 TCP Socket 客户端基类(心跳与异常处理)
- 核心痛点
- 解决方案:通用客户端基类设计
- 关键技术点:
- 核心代码实现
- 设计要点总结
- 总结
C# 工业设备通讯系列技术博文(持续更新…)
系列说明: 本系列基于实际工业机械臂控制项目,分享 C# 底层 Socket 通讯封装、设计模式应用、高频数据异步读取以及单通道通讯设计的实战经验。
第一篇:【C# 工业通讯】封装一个健壮的 TCP Socket 客户端基类(心跳与异常处理)
在工业自动化开发中,与机械臂、PLC 等设备的 TCP 通讯是基础。本文分享一个基于 C# Socket 封装的通用客户端基类,重点解决了连接状态探测(Poll)、KeepAlive 心跳保活以及异常断开的统一处理。
核心痛点
在使用原生 Socket 开发工业设备通讯时,我们经常遇到以下问题:
•假连接: socket.Connected 属性并不总是准确的,网线被拔了可能还是返回 true。
•心跳保活: 长时间无数据传输,路由器或防火墙可能会切断连接,导致对端认为你已离线。
•代码复用: 每个设备都写一套连接、断开、发送逻辑,维护成本高,且容易出现遗漏。
解决方案:通用客户端基类设计
我设计了一个抽象类作为底层通讯基类,将通用的连接管理逻辑抽离出来,子类只需关注具体的业务协议。
关键技术点:
•精准的状态检测: 使用 Poll 方法配合 SelectMode.SelectRead 来探测连接是否真实有效。
•底层 KeepAlive: 通过 IOControl 设置 TCP 层的保活机制,防止连接被静默断开。
•事件驱动: 定义 NetworkErrorEvent,将网络异常统一抛给上层处理。
核心代码实现
usingSystem;usingSystem.Net;usingSystem.Net.Sockets;usingSystem.Runtime.InteropServices;usingSystem.Text;namespaceJupiter.Device{/// <summary>/// 设备客户端基类 - 封装底层 Socket 通讯细节/// 所有机械臂/PLC等设备的通讯类均应继承此类/// </summary>publicabstractclassDeviceClient{privateSocketmSocketClient=newSocket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);publicstringIP{get;privateset;}publicintPort{get;privateset;}/// <summary>/// 连接设备(入口方法,处理重复连接和地址变更)/// </summary>publicboolConnect(stringstrIp,intiPort){// 如果已连接且地址未变,直接返回,避免重复建连if(null!=mSocketClient&&IsConnected()){if(strIp!=this.IP||iPort!=this.Port){this.Disconnect();}else{returntrue;}}returnthis.ConnectDeviceServer(strIp,iPort);}/// <summary>/// 真实连接状态检测/// 解决 Connected 属性不准的问题:网线拔了可能还是 true/// </summary>publicboolIsConnected(){try{if(mSocketClient==null||!mSocketClient.Connected)returnfalse;// 核心:使用 Poll 探测连接状态// 如果 SelectRead 为 true 且 Available 为 0,说明连接已被对端关闭boolstateChanged=mSocketClient.Poll(1000,SelectMode.SelectRead);if(stateChanged&&mSocketClient.Available==0){returnfalse;}returntrue;}catch(Exceptionex){LogHelper.Error("DeviceClient",ex,"连接检测异常");returnfalse;}}/// <summary>/// 建立物理连接并开启 KeepAlive/// </summary>privateboolConnectDeviceServer(stringstrIp,intiPort){try{this.IP=strIp;this.Port=iPort;mSocketClient=newSocket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);mSocketClient.Connect(newIPEndPoint(IPAddress.Parse(strIp),iPort));// 【重点】开启 TCP KeepAlive 防止连接被静默断开// 参数说明:启用(1) + 首次探测时间(5000ms) + 探测间隔(500ms)uintdummy=0;byte[]optVal=newbyte[Marshal.SizeOf(dummy)*3];BitConverter.GetBytes((uint)1).CopyTo(optVal,0);BitConverter.GetBytes((uint)5000).CopyTo(optVal,Marshal.SizeOf(dummy));BitConverter.GetBytes((uint)500).CopyTo(optVal,Marshal.SizeOf(dummy)*2);mSocketClient.IOControl(IOControlCode.KeepAliveValues,optVal,null);OnConnected(mSocketClient);returntrue;}catch(Exceptionex){LogHelper.Error("DeviceClient",ex,"连接失败");returnfalse;}}/// <summary>/// 发送数据(UTF-8 文本协议)/// </summary>protectedboolSendData(stringstr){try{byte[]data=Encoding.UTF8.GetBytes(str);return(mSocketClient.Send(data)==data.Length);}catch(SocketExceptionex){// 发送失败且连接断开,触发网络错误事件if(!IsConnected())NetworkErrorEvent?.Invoke(this,ex.SocketErrorCode);returnfalse;}}/// <summary>/// 断开连接/// </summary>publicvoidDisconnect(){try{if(mSocketClient!=null&&mSocketClient.Connected){mSocketClient.Shutdown(SocketShutdown.Both);mSocketClient.Close();}}catch(Exceptionex){LogHelper.Error("DeviceClient",ex,"断开连接异常");}finally{OnDisconnected();}}/// <summary>/// 网络异常事件,供上层订阅/// </summary>publicdelegatevoidOnNetworkError(DeviceClientsender,SocketErroriErrCode);publiceventOnNetworkErrorNetworkErrorEvent;// 抽象方法,强制子类实现连接成功/断开后的业务逻辑protectedabstractvoidOnConnected(Socketsock);protectedabstractvoidOnDisconnected();}}设计要点总结
| 设计点 | 说明 |
|---|---|
| IsConnected() 重写 | 用 Poll + Available 替代简单的 Connected 属性判断,能准确检测网线拔出等物理断开 |
| TCP KeepAlive | 通过 IOControl 设置底层保活参数,首次探测 5s 后开始,间隔 500ms,有效防止路由器/防火墙切断空闲连接 |
| 抽象方法隔离 | OnConnected / OnDisconnected 交由子类实现,基类只负责连接生命周期管理 |
| 事件驱动异常 | NetworkErrorEvent 让上层决定如何处理异常(重试、告警、降级等),基类不耦合具体策略 |
总结
通过封装这个通用客户端基类,我们将繁琐的 Socket 状态管理、字节流转换和异常捕获下沉到了基类。后续开发具体的机械臂控制类时,只需要继承它并实现 OnConnected 即可,大大提升了代码的可维护性。