)
一、项目整体架构必考简答题1.1 通信链路多TCP客户端 → TCP服务端网关→ 串口Modbus RTU设备 → 原路应答返回对应客户端核心模式网络协议 ↔ 串口协议 双向透传网关1.2 项目核心亮点区别基础版请求队列 排他锁解决多客户端并发抢串口乱包问题严格实现一问一答Modbus RTU 自动粘包分包根据帧长度字段自动截取完整帧双模式客户端轮询定时器轮询 Task异步轮询两种实现完整CRC16校验客户端自主组Modbus RTU合法报文配置文件读取参数串口、IP、端口可配置硬编码零侵入数据统计指示灯状态收发字节计数、网口/串口运行指示灯1.3 工程结构服务器窗体TCP监听、串口管理、请求队列、帧解析、数据透传客户端窗体TCP连接、两种轮询方式、数据解析展示辅助工具类IP获取、Modbus组包、拆包、CRC16校验二、全局核心变量解析考试填空/选择// 串口通信对象 SerialPort serialPort; // TCP服务端监听对象 TcpListener tcpListener; // 异步任务取消令牌安全启停后台线程 CancellationTokenSource cts; // 在线客户端字典本项目预留扩展 DictionaryEndPoint, TcpClient clients new DictionaryEndPoint, TcpClient(); // 核心当前正在应答的客户端绑定问答 public TcpClient currentclient; // 请求队列缓存多客户端并发请求串行执行 private ConcurrentQueueRequestItem requestQueue new ConcurrentQueueRequestItem(); // 指令处理标记防止串口并发下发 public bool isProcessing false; // 线程排他锁保证队列操作原子性 private readonly object processLock new object(); // 串口数据缓冲区解决粘包分包 public Listbyte list new Listbyte();核心变量考点ConcurrentQueue线程安全队列多线程入队无冲突替代普通QueueisProcessing lock锁网关核心防抖机制保证串口同一时刻只处理1条指令currentclient记录请求来源实现应答原路返回多客户端不乱包三、服务端模块拆分逐段代码考点3.1 窗体加载 串口初始化读取配置文件参数自动加载串口列表、波特率、数据位、停止位、校验位private void ServerFrm_Load(object sender, EventArgs e) { InitSerialPort(); } // 初始化串口下拉框读取配置文件默认值 private void InitSerialPort() { // 加载本机所有串口 cbbPortName.DataSource SerialPort.GetPortNames(); // 读取App.config配置 cbbPortName.SelectedItem ConfigurationManager.AppSettings[PortName]; cbbBaudRate.SelectedItem ConfigurationManager.AppSettings[BaudRate]; cbbDataBit.SelectedItem ConfigurationManager.AppSettings[DataBit]; cbbStopBit.SelectedItem ConfigurationManager.AppSettings[StopBit]; cbbParity.SelectedItem ConfigurationManager.AppSettings[Parity]; }3.2 服务启停按钮逻辑private void btnStart_Click(object sender, EventArgs e) { try { if (btnStart.Text 启动) { StartServer(); // 初始化串口TCP服务 AcceptData(); // 开启客户端监听与数据接收 } else { StopServer(); // 释放所有资源、停止服务 } } catch (Exception ex) { MessageBox.Show(ex.Message, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } }3.3 服务启动核心方法 StartServer双初始化串口初始化开启 TCP服务端监听开启更新UI状态与指示灯private void StartServer() { try { // 1.初始化并打开串口 serialPort new SerialPort(cbbPortName.SelectedItem.ToString()); 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()); // 绑定串口接收事件 serialPort.DataReceived SerialPort_DataReceived; if (!serialPort.IsOpen) serialPort.Open(); panelSerialPort.BackColor Color.Lime; // 2.初始化TCP服务端 IPAddress address IPHelper.GetLocalIP();//获取本机局域网IP int port int.Parse(ConfigurationManager.AppSettings[port]); tcpListener new TcpListener(address, port); tcpListener.Start(); panelNetwork.BackColor Color.Lime; // 3.锁定配置控件防止运行中修改 btnStart.Text 停止; cbbPortName.Enabled false; cbbBaudRate.Enabled false; cbbDataBit.Enabled false; cbbStopBit.Enabled false; cbbParity.Enabled false; tsslServerStatus.Text $服务器已启动IP{address}Port{port}; } catch (Exception ex) { throw ex; } }3.4 核心机制请求队列串行处理 TryProcessQueue考试重点解决多客户端并发实现串口串行一问一答杜绝指令错乱、覆盖private void TryProcessQueue() { // 排他锁保证同一时刻只有一个线程操作队列 lock (processLock) { // 正在处理指令 → 直接退出不插队 if (isProcessing) return; // 队列为空 → 无请求需要处理 if (requestQueue.IsEmpty) return; // 取出队首最早的一条请求 if (requestQueue.TryDequeue(out var item)) { currentclient item.Client; // 绑定请求客户端 isProcessing true; // 标记忙碌禁止新指令下发 // 串口下发Modbus请求帧 if (serialPort ! null serialPort.IsOpen item.Data ! null) { serialPort.Write(item.Data, 0, item.Data.Length); } } } }机制原理多客户端同时发请求 → 全部入队排队 → 串行依次下发 → 设备应答后再处理下一条工业通信标准流程。3.5 TCP客户端连接监听 数据接收 AcceptData异步监听客户端连接每个客户端独立线程接收数据收到数据入队排队处理private void AcceptData() { cts new CancellationTokenSource(); Task.Run(() { while (!cts.IsCancellationRequested) { // 无新客户端连接循环等待 if (!tcpListener.Pending()) continue; // 接收新客户端 TcpClient tcpClient tcpListener.AcceptTcpClient(); // 每个客户端独立异步接收线程 Task.Run(async () { while (!cts.IsCancellationRequested) { try { // 连接断开/无数据跳过 if (!tcpClient.Connected || tcpClient.Available 0) continue; NetworkStream stream tcpClient.GetStream(); byte[] buffer new byte[tcpClient.Available]; int count await stream.ReadAsync(buffer, 0, buffer.Length); if (count 0) continue; // 核心请求入队绑定客户端不直接下发 requestQueue.Enqueue(new RequestItem() { Client tcpClient, Data buffer }); TryProcessQueue(); // 网口闪烁 发送计数累加 Invoke(new Action(async () { panelNetwork.BackColor Color.Lime; await Task.Delay(50); panelNetwork.BackColor Color.Gray; int oldSendCount int.Parse(txtSendCount.Text); txtSendCount.Text (oldSendCount count).ToString(); })); } catch { // 客户端异常静默处理 Console.WriteLine(客户端数据接收异常); } } }, cts.Token); } }, cts.Token); }3.6 串口数据接收 粘包分包解析最难考点流式串口数据拼接缓冲区根据Modbus RTU帧结构自动截取完整帧原路回传客户端private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { if (!serialPort.IsOpen) return; int length serialPort.BytesToRead; byte[] serialResBuffer new byte[length]; serialPort.Read(serialResBuffer, 0, length); // 追加数据到缓冲区解决粘包、半包 list.AddRange(serialResBuffer); // Modbus最小帧长度5字节不足直接等待 while (list.Count 5) { byte dataLength list[2]; // 整帧长度 地址1功能码1长度1数据NCRC2 int fullFrameLength 3 dataLength 2; // 缓冲区够一帧数据 if (list.Count fullFrameLength) { // 取出完整应答帧 byte[] fullFrame list.Take(fullFrameLength).ToArray(); // 移除已解析数据保留半包 list.RemoveRange(0, fullFrameLength); // 原路回传给请求客户端 if (currentclient ! null currentclient.Connected) { try { NetworkStream stream currentclient.GetStream(); stream.Write(fullFrame, 0, fullFrame.Length); // 串口指示灯闪烁 接收计数 Invoke(new Action(async () { panelSerialPort.BackColor Color.Lime; await Task.Delay(50); panelSerialPort.BackColor Color.Gray; int oldRecCount int.Parse(txtReceiveCount.Text); txtReceiveCount.Text (oldRecCount fullFrame.Length).ToString(); })); } catch (Exception) { currentclient.Close(); } // 释放忙碌状态允许处理下一条队列请求 lock (processLock) { currentclient null; isProcessing false; } // 递归处理队列下一条 TryProcessQueue(); } } else { // 数据不足一帧退出等待下次拼接 break; } } }分包核心公式必背整帧长度 3(头固定) 数据长度 2(CRC校验)3.7 服务停止资源释放 StopServerprivate void StopServer() { try { cts?.Cancel(); clients new DictionaryEndPoint, TcpClient(); serialPort?.Close(); tcpListener?.Stop(); // 还原UI状态 btnStart.Text 启动; cbbPortName.Enabled true; cbbBaudRate.Enabled true; cbbDataBit.Enabled true; cbbStopBit.Enabled true; cbbParity.Enabled true; tsslServerStatus.Text $服务器已停止; panelNetwork.BackColor Color.Gray; panelSerialPort.BackColor Color.Gray; } catch (Exception ex) { throw ex; } }3.8 队列实体类 RequestItem// 队列存储项绑定【请求客户端请求数据】 public class RequestItem { public TcpClient Client { get; set; } public byte[] Data{ get; set; } }四、客户端模块完整拆解4.1 客户端全局变量TcpClient tcpClient; CancellationTokenSource cts; CancellationTokenSource cts2; // 异步轮询专用4.2 初始化服务器配置private void ClientFrm_Load(object sender, EventArgs e) { InitServerConfig(); } private void InitServerConfig() { txtServerIP.Text ConfigurationManager.AppSettings[ServerIP]; txtServerPort.Text ConfigurationManager.AppSettings[ServerPort]; }4.3 连接/断开服务器private async void btnConn_Click(object sender, EventArgs e) { try { if (btnConn.Text 连接) { await ConnServer(); AcceptData(); } else { DisConnServer(); } } catch (Exception ex) { MessageBox.Show(ex.Message, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } } // 连接服务端 private async Task ConnServer() { tcpClient new TcpClient(); await tcpClient.ConnectAsync(IPAddress.Parse(txtServerIP.Text), int.Parse(txtServerPort.Text)); btnConn.Text 断开; txtServerIP.Enabled false; txtServerPort.Enabled false; } // 断开连接、释放资源 private void DisConnServer() { if (btnRealTimeRead.Text 停止读取) btnRealTimeRead.PerformClick(); cts?.Cancel(); tcpClient?.Close(); tcpClient null; btnConn.Text 连接; txtServerIP.Enabled true; txtServerPort.Enabled true; }4.4 客户端接收服务端应答数据private void AcceptData() { cts new CancellationTokenSource(); Task.Run(async () { while (!cts.IsCancellationRequested) { try { if (!tcpClient.Connected || tcpClient.Available 0) continue; NetworkStream stream tcpClient.GetStream(); byte[] buffer new byte[tcpClient.Available]; int count await stream.ReadAsync(buffer, 0, buffer.Length); if (count 0) continue; // 解析Modbus数据并展示 Invoke(new Action(() { int[] data DataHelper.GetData(buffer); txtData1.Text data[0].ToString(); txtData2.Text data[1].ToString(); txtData3.Text data[2].ToString(); txtData4.Text data[3].ToString(); })); } catch { } } }, cts.Token); }4.5 两种轮询方式考试重点对比方式1定时器轮询private void btnRealTimeRead_Click(object sender, EventArgs e) { if (btnRealTimeRead.Text 实时读取1) { if (tcpClient null || !tcpClient.Connected) { MessageBox.Show(先连接服务器, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } timer1.Enabled true; btnRealTimeRead.Text 停止读取1; } else { timer1.Enabled false; btnRealTimeRead.Text 实时读取1; } } private void timer1_Tick(object sender, EventArgs e) { try { if (tcpClient! null) { byte slaveId byte.Parse(txtSlaveId.Text); byte startAddress byte.Parse(txtStartAddress.Text); byte dataLength byte.Parse(txtDataLength.Text); byte[] buffer DataHelper.SetData(slaveId, 3, startAddress, dataLength); NetworkStream stream tcpClient.GetStream(); stream.Write(buffer, 0, buffer.Length); } } catch { } }方式2Task异步轮询推荐private void btnRealTimeRead2_Click(object sender, EventArgs e) { if (tcpClient null || !tcpClient.Connected) { MessageBox.Show(先连接服务器, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } if (btnRealTimeRead2.Text 实时读取2) { cts2 new CancellationTokenSource(); Task.Run(async () { while (!cts2.IsCancellationRequested) { byte slaveId byte.Parse(txtSlaveId.Text); byte startAddress byte.Parse(txtStartAddress.Text); byte dataLength byte.Parse(txtDataLength.Text); byte[] buffer DataHelper.SetData(slaveId, 3, startAddress, dataLength); NetworkStream stream tcpClient.GetStream(); stream.Write(buffer, 0, buffer.Length); await Task.Delay(1000); } }, cts2.Token); btnRealTimeRead2.Text 停止读取2; } else { cts2?.Cancel(); btnRealTimeRead2.Text 实时读取2; } }五、核心工具类CRC16组包拆包5.1 DataHelper 完整代码namespace 客户端.Helpers { public static class DataHelper { // Modbus RTU 组包生成合法请求帧带CRC校验 public static byte[] SetData(byte slaveAddress, byte funCode, byte startAddress, byte dataLen) { byte[] k new byte[6] { slaveAddress, funCode, GetHigh(startAddress), GetLow(startAddress), GetHigh(dataLen), GetLow(dataLen) }; byte[] r Crc16(k); byte[] buffer new byte[8]; Array.Copy(k, 0, buffer, 0, k.Length); Array.Copy(r, 0, buffer, k.Length, r.Length); return buffer; } // 拆包解析应答帧输出寄存器数值 public static int[] GetData(byte[] buffer) { if (buffer.Length 3) return null; byte count buffer[2]; int[] data new int[count / 2]; for (int i 0; i data.Length; i) { data[i] buffer[i * 2 3] * 256 buffer[i * 2 4]; } return data; } // 取字节高4位 private static byte GetHigh(byte value) { return (byte)(value 4); } // 取字节低4位 private static byte GetLow(byte value) { return (byte)(value 0x0F); } // 标准Modbus CRC16校验算法 private static byte[] Crc16(byte[] data) { int len data.Length; if (len 0) { ushort crc 0xFFFF; for (int i 0; i len; i) { crc (ushort)(crc ^ (data[i])); for (int j 0; j 8; j) { crc (crc 1) ! 0 ? (ushort)((crc 1) ^ 0xA001) : (ushort)(crc 1); } } byte high (byte)((crc 0xFF00) 8); byte low (byte)(crc 0x00FF); return new byte[] { low, high }; } return new byte[] { 0, 0 }; } } }5.2 IPHelper 获取本机局域网IPpublic static class IPHelper { public static IPAddress GetLocalIP() { IPAddress address null; IPHostEntry entry Dns.GetHostEntry(Dns.GetHostName()); foreach (var item in entry.AddressList) { // 筛选IPv4地址 if (item.AddressFamily AddressFamily.InterNetwork) { address item; break; } } return address; } }六、核心重难点总结考试必背6.1 解决的三大工业通信问题多客户端并发乱包ConcurrentQueue队列排他锁串行处理串口粘包分包List缓冲区累加Modbus帧长度精准截取问答不匹配RequestItem绑定客户端与请求应答原路返回6.2 两种轮询优缺点定时器轮询UI线程执行阻塞轻微简单易用Task异步轮询后台线程执行不卡UI稳定高效工业首选6.3 Modbus RTU帧解析公式数据值 高位字节 * 256 低位字节整帧长度 3 数据长度 2(CRC)七、满分简答题模板本项目实现TCP转串口Modbus RTU网关功能服务端搭建TCP监听服务并初始化串口设备通信采用线程安全队列缓存多客户端并发请求通过排他锁实现指令串行下发避免串口并发冲突。通过缓冲区拼接方式解决串口流式数据粘包分包问题依据Modbus帧结构精准截取完整应答帧并原路回传给对应请求客户端。客户端支持定时器与异步Task两种轮询方式通过CRC16算法自主生成合法Modbus请求帧解析设备应答数据并可视化展示完整实现网口与串口设备的双向透传通信。