C# TCP - 串口转发1.0
C# TCP - 串口转发服务器完整代码笔记
一、项目整体架构
该项目是一个基于 C# WinForms 的双向数据转发服务器,核心作用是搭建 “TCP 客户端 ↔ 服务器 ↔ 串口设备” 的通信桥梁,适用于工业控制、物联网设备远程监控等场景。项目包含 3 个核心文件,职责分工明确:
文件路径 | 类名 | 核心功能 |
---|---|---|
服务器 / ServerFrm.cs | ServerFrm | 主窗体,实现 TCP 服务器、串口通信、数据转发 |
服务器 / Models/Client.cs | Client | 客户端模型,存储客户端网络端点与 Socket 对象 |
_服务器 / Utility/FileIni.cs | FileIni | INI 文件工具类,实现配置读写(依赖 Windows API) |
二、核心模块解析(按文件划分)
模块 1:主窗体类(ServerFrm.cs)
1.1 成员变量定义
存储服务器运行状态、网络 / 串口对象、任务控制与客户端列表,是整个程序的 “数据中枢”:
// 服务器启动状态(true=运行,false=停止) bool IsStart = false; // TCP服务器Socket(负责监听客户端连接) Socket serverSocket = null; // 串口通信对象(负责与硬件设备交互) SerialPort serialPort = null; // 任务取消令牌源(安全终止异步任务,避免资源泄漏) CancellationTokenSource cts = null; // 客户端列表(存储所有已连接的客户端信息) List<Client> ClientList = new List<Client>();
1.2 初始化配置(BindConfig 方法)
窗体加载时自动执行,完成网络、串口参数的初始配置,减少手动输入操作:
private void BindConfig() {// 1. 自动获取本机IPv4地址(过滤IPv6)string hostName = Dns.GetHostName(); // 获取主机名IPAddress[] addresses = Dns.GetHostAddresses(hostName); // 获取所有IPforeach (IPAddress address in addresses){if (address.AddressFamily == AddressFamily.InterNetwork) // 筛选IPv4txtIP.Text = address.ToString();} // 2. 默认TCP端口(9999,常用非特权端口)txtPort.Text = 9999.ToString(); // 3. 加载可用串口号(自动检测电脑已连接的串口)cbbPortName.DataSource = SerialPort.GetPortNames(); // 4. 初始化波特率(1200→57600,覆盖常见硬件通信速率)int baudRateStart = 1200;cbbBaudRate.Items.Add(baudRateStart);for (int i = 1; i < 5; i++){if (i != 3) // 跳过3倍速,按2倍递增:1200→2400→4800→9600→57600cbbBaudRate.Items.Add(baudRateStart * (i * 2));}cbbBaudRate.Items.Add(57600);cbbBaudRate.SelectedIndex = 0; // 默认选中第一个(1200) // 5. 数据位、停止位、校验位默认值(需与硬件设备配置一致)cbbDataBit.SelectedIndex = 0; // 默认数据位(通常为8位)cbbStopBit.SelectedIndex = 0; // 默认停止位(通常为1位)cbbParity.SelectedIndex = 0; // 默认校验位(通常为无校验) }
1.3 服务器启停控制(btnStart_Click + 辅助方法)
通过按钮触发,实现服务器 “启动→运行→停止” 的完整生命周期管理,核心是 资源的创建与释放:
1.3.1 启动服务器(StartServer 方法)
private void StartServer() {// 步骤1:启动TCP服务器serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);serverSocket.Bind(new IPEndPoint(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text))); // 绑定IP+端口serverSocket.Listen(100); // 最大监听队列(支持100个客户端排队连接)panelNetwork.BackColor = Color.Green; // 网络状态灯:绿色=运行 // 步骤2:启动串口(与硬件设备通信)serialPort = new SerialPort();// 配置串口参数(需与硬件一致,否则通信失败)serialPort.PortName = cbbPortName.SelectedItem.ToString(); // 串口号(如COM3)serialPort.BaudRate = int.Parse(cbbBaudRate.SelectedItem.ToString()); // 波特率serialPort.DataBits = int.Parse(cbbDataBit.SelectedItem.ToString()); // 数据位serialPort.StopBits = (StopBits)Enum.Parse(typeof(StopBits), cbbStopBit.SelectedItem.ToString()); // 停止位(枚举转换)serialPort.Parity = (Parity)Enum.Parse(typeof(Parity), cbbParity.SelectedItem.ToString()); // 校验位(枚举转换)if (!serialPort.IsOpen) serialPort.Open(); // 打开串口 // 步骤3:绑定串口数据接收事件(硬件发数据时触发)serialPort.DataReceived += SerialPort_DataReceived;panelSerialPort.BackColor = Color.Green; // 串口状态灯:绿色=运行 // 步骤4:更新UI状态(防止重复配置)btnStart.Text = "关闭服务器";IsStart = true;// 禁用配置输入框(运行中不允许修改参数)txtIP.Enabled = txtPort.Enabled = cbbPortName.Enabled = cbbBaudRate.Enabled = cbbDataBit.Enabled = cbbStopBit.Enabled = cbbParity.Enabled = false; }
1.3.2 停止服务器(StopServer 方法)
private void StopServer() {// 步骤1:终止所有异步任务(避免线程泄漏)cts?.Cancel(); // 步骤2:断开所有客户端连接foreach (var item in ClientList){Socket client = item.Socket;if (client != null && client.Connected) client.Disconnect(false); // false=不允许后续重用Socket} // 步骤3:清空客户端列表ClientList = new List<Client>(); // 步骤4:释放串口资源(必须关闭,否则其他程序无法占用)if (serialPort != null && serialPort.IsOpen) serialPort.Close(); // 步骤5:停止TCP服务器(释放端口)serverSocket?.Close();serverSocket = null; // 置空,避免重复关闭 // 步骤6:恢复UI状态txtIP.Enabled = txtPort.Enabled = cbbPortName.Enabled = cbbBaudRate.Enabled = cbbDataBit.Enabled = cbbStopBit.Enabled = cbbParity.Enabled = true;panelSerialPort.BackColor = Color.Red; // 串口状态灯:红色=停止panelNetwork.BackColor = Color.Red; // 网络状态灯:红色=停止btnStart.Text = "启动服务器";IsStart = false; }
1.4 客户端连接与数据处理
实现 “接收客户端连接→接收客户端数据→转发到串口” 的完整流程,核心是 异步处理(避免 UI 卡顿):
1.4.1 监听客户端连接(Acceprequest + AccepClient)
// 启动客户端连接监听任务(独立线程,不阻塞UI) private void Acceprequest() {cts = new CancellationTokenSource();Task task = new Task(AccepClient, cts.Token); // 绑定取消令牌task.Start(); } // 异步接收客户端连接(循环执行,直到任务取消) private async void AccepClient() {while (!cts.IsCancellationRequested){try{// 等待客户端连接(异步方法,不阻塞线程)Socket client = await serverSocket.AcceptAsync();string clientEndPoint = client.RemoteEndPoint.ToString(); // 客户端标识(IP:端口) // 去重:移除相同端点的旧连接(避免重复存储)int index = ClientList.FindIndex(c => c.EndPoint == clientEndPoint);if (index != -1) ClientList.RemoveAt(index); // 添加新客户端到列表ClientList.Add(new Client() { EndPoint = clientEndPoint, Socket = client }); // 启动该客户端的数据接收任务(每个客户端独立处理)ReceiveData(client, cts);}catch { /* 捕获取消任务异常,无需处理 */ }} }
1.4.2 接收客户端数据并转发到串口(ReceiveData)
private void ReceiveData(Socket client, CancellationTokenSource cts) {Task task = new Task(async () =>{while (!cts.IsCancellationRequested){try{// 步骤1:创建缓冲区(长度=客户端待接收数据量,减少内存浪费)byte[] buffer = new byte[client.Available]; // 步骤2:异步接收客户端数据(返回实际接收字节数)int count = await client.ReceiveAsync(new ArraySegment<byte>(buffer), SocketFlags.None); if (count > 0) // 有数据才处理{// 步骤3:状态灯闪烁(提示数据活动,增强可视化)panelNetwork.BackColor = Color.Yellow;await Task.Delay(50);panelNetwork.BackColor = Color.Gray;await Task.Delay(50);panelNetwork.BackColor = Color.Green; // 步骤4:转发数据到串口(客户端→服务器→硬件)serialPort.Write(buffer, 0, buffer.Length); // 步骤5:串口状态灯闪烁(提示串口发送)panelSerialPort.BackColor = Color.Yellow;await Task.Delay(50);panelSerialPort.BackColor = Color.Gray;await Task.Delay(50);panelSerialPort.BackColor = Color.Green; // 步骤6:更新接收数据量统计(跨线程UI操作,必须用Invoke)Invoke(new Action(() =>{int oldReceiveCount = int.Parse(txtReceiveCount.Text);txtReceiveCount.Text = (oldReceiveCount + count).ToString();}));}}catch { /* 捕获客户端断开异常,无需处理 */ }}}, cts.Token);task.Start(); }
1.5 串口数据接收与转发到客户端(SerialPort_DataReceived)
硬件设备通过串口发数据时触发,实现 “硬件→服务器→所有客户端” 的广播转发:
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) {// 步骤1:读取串口数据(硬件响应)SerialPort sp = (SerialPort)sender;byte[] buffer = new byte[sp.BytesToRead]; // 缓冲区长度=待读取数据量int count = sp.Read(buffer, 0, buffer.Length); // 读取数据 if (count == 0) return; // 无数据则返回 // 步骤2:转发到所有已连接客户端(广播)foreach (var item in ClientList){Socket client = item.Socket;if (!(client != null && client.Connected)) return; // 跳过断开的客户端 // 发送数据到客户端client.Send(buffer); // 步骤3:更新发送数据量统计(跨线程UI操作)Invoke(new Action(() =>{int oldSendCount = int.Parse(txtSendCount.Text);txtSendCount.Text = (oldSendCount + count).ToString();}));} }
模块 2:客户端模型类(Models/Client.cs)
极简模型类,仅存储客户端核心信息,用于管理多个客户端连接:
namespace 服务器.Models {public class Client{// 客户端网络端点(格式:IP:端口,如192.168.1.100:54321)public string EndPoint { get; set; }// 客户端对应的Socket对象(用于发送/接收数据)public Socket Socket { get; set; }} }
模块 3:INI 配置工具类(Utility/FileIni.cs)
通过调用 Windows 内核 API(kernel32.dll) 实现 INI 文件的读写,适用于需要持久化配置的场景(如记住上次的串口参数):
3.1 核心原理
INI 文件是 Windows 传统配置文件,格式为[节点] 键=值
,需通过系统 API 操作。该类封装了 API 调用,提供简洁的静态方法供外部使用。
3.2 关键代码
namespace _服务器.Utility {internal class FileIni{// 1. 从App.config读取INI文件路径(需提前配置)private static string filePath = ConfigurationManager.AppSettings["InIFilePath"].ToString();// 2. 导入Windows API(写入INI文件)[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)][return: MarshalAs(UnmanagedType.Bool)]private static extern bool WritePrivateProfileString(string lpAppName, // 节点名称(如[SerialConfig])string lpKeyName, // 键(如BaudRate)string lpString, // 值(如9600)string lpFileName // INI文件路径);// 3. 导入Windows API(读取INI文件)[DllImport("kernel32.dll", CharSet = CharSet.Auto)]private static extern uint GetPrivateProfileString(string lpAppName, // 节点名称string lpKeyName, // 键string lpDefault, // 默认值(键不存在时返回)StringBuilder lpReturnedString, // 接收结果的字符串uint nSize, // 接收字符串的最大长度string lpFileName // INI文件路径);// 4. 自定义写入方法(外部调用)public static void Write(string section, string key, string value){WritePrivateProfileString(section, key, value, filePath);}// 5. 自定义读取方法(外部调用)public static string Read(string section, string key){StringBuilder sb = new StringBuilder(); // 存储读取结果GetPrivateProfileString(section, key, "", sb, 255, filePath); // 最大读取255字节return sb.ToString();}} }
3.3 使用前提
需在项目的App.config
中配置 INI 文件路径,示例:
<configuration><appSettings><!-- INI文件路径(可绝对路径或相对路径) --><add key="InIFilePath" value="ServerConfig.ini" /></appSettings> </configuration>
三、关键技术点与注意事项
1. 线程安全(跨线程 UI 操作)
问题:异步任务(如
ReceiveData
)在后台线程执行,直接修改 UI 控件会报错解决方案:使用
Invoke(new Action(() => { /* UI操作代码 */ }))
,将 UI 操作委托到主线程执行
2. 串口通信注意事项
串口参数(波特率、数据位等)必须与硬件设备完全一致,否则会出现 “能连接但收不到数据” 的问题
串口打开后必须关闭(
serialPort.Close()
),否则其他程序无法占用该串口避免频繁创建
SerialPort
对象,建议复用已创建的对象
3. TCP 服务器注意事项
端口号需选择 1024-65535 之间的非特权端口(0-1023 需管理员权限)
serverSocket.Listen(100)
中的 100 是 “等待连接队列长度”,不是最大客户端数客户端断开后需从
ClientList
中移除,避免转发数据时出错
4. 资源释放(防止内存泄漏)
必须通过
CancellationTokenSource.Cancel()
终止异步任务关闭服务器时需断开所有客户端连接、关闭串口和 TCP 服务器
避免使用
Task.Run
时忽略取消令牌,否则任务无法终止
四、扩展建议(基于现有代码)
配置持久化:结合
FileIni
类,在BindConfig
中读取 INI 配置,在StopServer
中保存配置,避免每次启动重新设置客户端管理:添加
ListBox
显示已连接客户端,支持手动断开指定客户端数据日志:添加
TextBox
记录收发数据的时间、内容(如十六进制格式),便于调试异常处理增强:在
catch
块中记录详细异常信息(如时间、错误类型),而非空 catch自动重连:串口断开后自动重试连接,提升稳定性
数据过滤:支持按特定协议(如 Modbus)解析数据,过滤
C# Modbus TCP 客户端完整代码笔记
一、项目整体架构
该项目是一个基于 C# WinForms 的 Modbus TCP 客户端工具,核心功能是与前文的 “TCP - 串口转发服务器” 通信,通过 Modbus-RTU 协议(功能码 03) 读取串口设备的寄存器数据,适用于工业传感器、控制器等设备的远程数据采集场景。
项目包含 2 个核心模块,职责分工清晰:
文件路径 | 类名 / 工具类 | 核心功能 |
---|---|---|
客户端 / ClientFrm.cs | ClientFrm | 主窗体,实现 TCP 连接、数据接收、实时读取 |
客户端 / Helpers/ | CRCHelper + DecHexSendHelper | Modbus 协议工具,含 CRC16 校验、报文构造 |
二、核心模块解析(按功能划分)
模块 1:主窗体类(ClientFrm.cs)
1.1 成员变量定义
存储客户端核心对象,管理连接状态、任务控制与定时任务:
// TCP客户端Socket(与服务器通信) Socket client = null; // 任务取消令牌源(安全终止数据接收任务) CancellationTokenSource cts = null; // Windows Forms定时器(实现实时读取功能,1秒/次) TimerForms timer = null;
1.2 TCP 连接管理(ConnServer + DisConn)
实现与服务器的 “连接→断开” 完整流程,核心是 异步连接避免 UI 卡顿:
1.2.1 连接服务器(ConnServer 方法)
private async Task ConnServer() {// 1. 初始化TCP Socket(IPv4、流式传输、TCP协议,符合Modbus TCP底层要求)client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);// 2. 异步连接服务器(解析IP和端口,避免阻塞UI线程)// 注意:需确保txtServerIP和txtServerPort输入的是有效IP(如192.168.1.100)和端口(如9999)await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(txtServerIP.Text), int.Parse(txtServerPort.Text)));// 3. 连接成功后更新UI状态btnConn.Text = "断开"; // 按钮文本切换为“断开”,提示当前连接状态 }
1.2.2 断开服务器(DisConn 方法)
private void DisConn() {// 1. 断开连接并释放Socket资源(防止资源泄漏)client?.Disconnect(false); // false = 不允许后续重用该Socketclient?.Close(); // 关闭Socket,释放底层网络资源// 2. 终止数据接收任务(避免后台线程继续运行)cts?.Cancel();// 3. 停止实时读取定时器(若已启动)timer?.Stop();// 4. 恢复UI状态btnConn.Text = "连接";btnRealTimeRead.Text = "开始实时读取"; // 重置实时读取按钮 }
1.3 数据接收与解析(AccepData 方法)
后台线程持续接收服务器转发的设备数据(Modbus 响应报文),并解析显示到 UI:
private void AccepData() {// 1. 初始化取消令牌源(用于后续终止任务)cts = new CancellationTokenSource();// 2. 启动后台任务(独立线程,不阻塞UI)Task.Run(() =>{// 循环接收数据,直到任务被取消while (!cts.IsCancellationRequested){try{// 3. 创建缓冲区(长度=当前待接收数据量,减少内存浪费)byte[] buffer = new byte[client.Available];// 4. 接收服务器数据(返回实际接收字节数)int count = client.Receive(buffer);if (count == 0) continue; // 无数据则跳过,避免无效解析// 5. 解析Modbus响应报文并更新UI(跨线程操作必须用Invoke)// 报文格式示例:0x01 0x03 0x08 00 6F 00 DE 01 4D 01 BC 33 08// 解析规则:从第4字节(索引3)开始,每2字节代表1个16位寄存器值Invoke(new Action(() =>{// 寄存器1:buffer[3](高8位)*256 + buffer[4](低8位)txtData1.Text = (buffer[3] * 256 + buffer[4]).ToString();// 寄存器2:buffer[5](高8位)*256 + buffer[6](低8位)txtData2.Text = (buffer[5] * 256 + buffer[6]).ToString();// 寄存器3:buffer[7](高8位)*256 + buffer[8](低8位)txtData3.Text = (buffer[7] * 256 + buffer[8]).ToString();// 寄存器4:buffer[9](高8位)*256 + buffer[10](低8位)txtData4.Text = (buffer[9] * 256 + buffer[10]).ToString();}));}catch{// 捕获异常(如服务器断开连接),无需处理(循环会因cts取消或Socket关闭退出)}}}, cts.Token); // 绑定取消令牌,支持任务终止 }
1.4 数据发送校验(CheckData 方法)
发送 Modbus 指令前,校验连接状态与输入合法性,避免无效请求:
private bool CheckData(out string errorMessage) {// 1. 校验是否已连接服务器if (!(client != null && client.Connected)){errorMessage = "请先连接服务器,再发送";return false;}// 2. 校验从站地址(Slave ID)是否为空if (string.IsNullOrEmpty(txtSlaveId.Text)){errorMessage = "Salve地址不能为空";return false;}// 3. 校验从站地址是否为有效数字if (!int.TryParse(txtSlaveId.Text, out int SlavedId)){errorMessage = "Salve地址必须是数字";return false;}// 4. 校验从站地址范围(Modbus协议规定:1-254为有效地址,0为广播地址,255保留)if (!(SlavedId > 0 && SlavedId <= 254)){errorMessage = "Salve地址必须再[1,254之间]";return false;}// 5. 校验起始地址(txtStartAddress)和数据长度(txtDataLength)(若需严格校验可补充)// 示例:校验起始地址是否为数字且非负if (!int.TryParse(txtStartAddress.Text, out int startAddr) || startAddr < 0){errorMessage = "起始地址必须是非负整数";return false;}if (!int.TryParse(txtDataLength.Text, out int dataLen) || dataLen <= 0 || dataLen > 125){errorMessage = "数据长度必须在[1,125]之间(Modbus功能码03最大读取125个寄存器)";return false;}// 所有校验通过errorMessage = string.Empty;return true; }
1.5 手动发送与实时读取
支持 “手动单次发送” 和 “定时实时读取” 两种数据采集模式:
1.5.1 手动发送(btnSend_Click)
private void btnSend_Click(object sender, EventArgs e) {// 1. 先校验输入与连接状态if (!CheckData(out string errorMessage)){MessageBox.Show(errorMessage);return;}// 2. 调用工具类发送Modbus读指令(功能码03)// 参数:客户端Socket、从站地址、功能码、起始地址、读取寄存器数量DecHexSendHelper.SendData(client,ushort.Parse(txtSlaveId.Text), // 从站地址(1-254)ushort.Parse("3"), // 功能码(3=读保持寄存器)ushort.Parse(txtStartAddress.Text),// 起始寄存器地址(如0)ushort.Parse(txtDataLength.Text) // 读取寄存器数量(如4)); }
1.5.2 实时读取(btnRealTimeRead_Click + Timer_Tick)
private void btnRealTimeRead_Click(object sender, EventArgs e) {if (btnRealTimeRead.Text == "开始实时读取"){// 1. 校验输入与连接状态if (!CheckData(out string errorMessage)){MessageBox.Show(errorMessage);return;}// 2. 初始化定时器(1秒触发1次,符合实时采集常见频率)timer = new TimerForms();timer.Interval = 1000; // 时间间隔(毫秒)timer.Tick += Timer_Tick; // 绑定定时器触发事件timer.Start(); // 启动定时器// 3. 更新UI状态btnRealTimeRead.Text = "关闭实时读取";}else{// 4. 关闭实时读取timer.Stop(); // 停止定时器btnRealTimeRead.Text = "开始实时读取"; // 重置按钮文本} }// 定时器触发事件(每1秒发送1次Modbus读指令) private void Timer_Tick(object sender, EventArgs e) {DecHexSendHelper.SendData(client,ushort.Parse(txtSlaveId.Text),ushort.Parse("3"),ushort.Parse(txtStartAddress.Text),ushort.Parse(txtDataLength.Text)); }
模块 2:Modbus 协议工具类(Helpers 文件夹)
2.1 CRC16 校验工具(CRCHelper.cs)
Modbus-RTU 协议必须的校验算法,确保报文在传输过程中无差错(多项式0xA001
,初始值0xFFFF
):
namespace 客户端.Helpers {public class CRCHelper{public static byte[] CRC16(byte[] data){int crc = 0xffff; // 初始值:0xFFFF(Modbus标准)// 1. 遍历数据字节,计算CRCfor (int i = 0; i < data.Length; i++){crc = crc ^ data[i]; // 当前CRC与数据字节异或// 2. 每字节循环8位(处理每一位)for (int j = 0; j < 8; j++){int temp = crc & 1; // 取CRC最低位crc = crc >> 1; // CRC右移1位crc = crc & 0x7fff; // 清除最高位(确保16位)// 若最低位为1,与多项式0xA001异或if (temp == 1)crc = crc ^ 0xa001;}crc = crc & 0xffff; // 确保CRC始终为16位}// 3. CRC高低位互换(Modbus-RTU要求小端序:低字节在前,高字节在后)byte[] crc16 = new byte[2];crc16[1] = (byte)((crc >> 8) & 0xff); // 高8位(放在第二个字节)crc16[0] = (byte)(crc & 0xff); // 低8位(放在第一个字节)return crc16;}} }
2.2 Modbus 报文构造工具(DecHexSendHelper.cs)
将十进制参数(从站地址、起始地址等)转换为 Modbus-RTU 标准报文,并添加 CRC 校验:
namespace 客户端.Helpers {public class DecHexSendHelper{// 发送Modbus读指令(功能码03)public static void SendData(Socket client, // TCP客户端Socketushort slaveId, // 从站地址(1-254)ushort functionCode, // 功能码(3=读保持寄存器)ushort startAddress, // 起始寄存器地址ushort dataLength // 读取寄存器数量){// 1. 构造Modbus核心报文(6字节:从站地址+功能码+起始地址+读取数量)byte[] buffer = new byte[6];buffer[0] = (byte)slaveId; // 字节0:从站地址buffer[1] = (byte)functionCode; // 字节1:功能码buffer[2] = (byte)(startAddress / 256); // 字节2:起始地址高8位buffer[3] = (byte)(startAddress % 256); // 字节3:起始地址低8位buffer[4] = (byte)(dataLength / 256); // 字节4:读取数量高8位buffer[5] = (byte)(dataLength % 256); // 字节5:读取数量低8位// 2. 计算CRC16校验(2字节)byte[] crc16 = CRCHelper.CRC16(buffer);// 3. 组装完整报文(6字节核心 + 2字节CRC = 8字节)byte[] data = new byte[8];Array.Copy(buffer, 0, data, 0, buffer.Length); // 复制核心报文Array.Copy(crc16, 0, data, buffer.Length, crc16.Length); // 复制CRC校验// 4. 发送报文到服务器(由服务器转发给串口设备)client.Send(data);}} }
三、关键技术点与注意事项
1. 跨线程 UI 操作
问题:数据接收任务在后台线程执行,直接修改
txtData1
等 UI 控件会抛出 “跨线程操作异常”解决方案:使用
Invoke(new Action(() => { /* UI操作代码 */ }))
,将 UI 更新委托到主线程执行
2. Modbus 协议细节
从站地址:必须在
1-254
之间(0 为广播地址,255 保留),否则设备无法响应功能码 03:最大读取
125个寄存器
(每个寄存器 16 位),超过会导致报文无效CRC 校验:Modbus-RTU 报文末尾必须添加 2 字节 CRC,否则设备会丢弃报文(无响应)
报文解析:响应报文的第 3 字节是 “字节数”(如
0x08
表示后续 8 字节数据,对应 4 个 16 位寄存器),解析时需注意偏移
3. 资源释放
Socket 释放:断开连接时必须调用
Close()
,否则会导致 Socket 资源泄漏,影响后续连接任务终止:数据接收任务需通过
cts.Cancel()
终止,避免后台线程持续运行消耗 CPU定时器停止:关闭实时读取时必须调用
timer.Stop()
,否则定时器会继续触发发送任务
4. 异常处理
连接异常:需捕获
FormatException
(IP / 端口格式错误)、SocketException
(连接失败、服务器断开)等数据解析异常:若设备响应报文长度不符合预期(如不足 11 字节),解析时会报错,建议添加报文长度校验(如
if (count < 11) continue;
)
四、扩展建议(基于现有代码)
添加报文日志:记录发送 / 接收的原始报文(如
01 03 00 00 00 04 44 69
),便于调试协议问题支持更多功能码:扩展
DecHexSendHelper
,添加功能码 06(写单个寄存器)、16(批量写寄存器)数据格式转换:支持十进制 / 十六进制显示切换,添加数据单位(如℃、kPa)配置
断线重连:添加自动重连机制,服务器断开后无需手动重新连接
报警功能:设置数据阈值(如温度超过 50℃),触发弹窗或声音报警
数据导出:将采集到的数据导出为 Excel 或 TXT 文件,便于后续分析