一、项目整体架构(必考简答题)
1.1 通信链路
多TCP客户端 → TCP服务端(网关)→ 串口Modbus RTU设备 → 原路应答返回对应客户端
核心模式:网络协议 ↔ 串口协议 双向透传网关
1.2 项目核心亮点(区别基础版)
-
请求队列 + 排他锁 :解决多客户端并发抢串口乱包问题,严格实现一问一答
-
Modbus RTU 自动粘包分包:根据帧长度字段自动截取完整帧
-
双模式客户端轮询:定时器轮询 + Task异步轮询两种实现
-
完整CRC16校验:客户端自主组Modbus RTU合法报文
-
配置文件读取参数:串口、IP、端口可配置,硬编码零侵入
-
数据统计+指示灯状态:收发字节计数、网口/串口运行指示灯
1.3 工程结构
-
服务器窗体:TCP监听、串口管理、请求队列、帧解析、数据透传
-
客户端窗体:TCP连接、两种轮询方式、数据解析展示
-
辅助工具类:IP获取、Modbus组包、拆包、CRC16校验
二、全局核心变量解析(考试填空/选择)
cs
// 串口通信对象
SerialPort serialPort;
// TCP服务端监听对象
TcpListener tcpListener;
// 异步任务取消令牌(安全启停后台线程)
CancellationTokenSource cts;
// 在线客户端字典(本项目预留扩展)
Dictionary<EndPoint, TcpClient> clients = new Dictionary<EndPoint, TcpClient>();
// 核心:当前正在应答的客户端(绑定问答)
public TcpClient currentclient;
// 请求队列:缓存多客户端并发请求,串行执行
private ConcurrentQueue<RequestItem> requestQueue = new ConcurrentQueue<RequestItem>();
// 指令处理标记:防止串口并发下发
public bool isProcessing = false;
// 线程排他锁,保证队列操作原子性
private readonly object processLock = new object();
// 串口数据缓冲区(解决粘包分包)
public List<byte> list = new List<byte>();
核心变量考点
-
ConcurrentQueue:线程安全队列,多线程入队无冲突,替代普通Queue
-
isProcessing + lock锁:网关核心防抖机制,保证串口同一时刻只处理1条指令
-
currentclient :记录请求来源,实现应答原路返回,多客户端不乱包
三、服务端模块拆分(逐段代码+考点)
3.1 窗体加载 & 串口初始化
读取配置文件参数,自动加载串口列表、波特率、数据位、停止位、校验位
cs
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 服务启停按钮逻辑
cs
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状态与指示灯
cs
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
考试重点:解决多客户端并发,实现串口串行一问一答,杜绝指令错乱、覆盖
cs
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
异步监听客户端连接,每个客户端独立线程接收数据,收到数据入队排队处理
cs
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帧结构自动截取完整帧,原路回传客户端
cs
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+数据N+CRC2
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 服务停止资源释放 StopServer
cs
private void StopServer()
{
try
{
cts?.Cancel();
clients = new Dictionary<EndPoint, 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
cs
// 队列存储项:绑定【请求客户端+请求数据】
public class RequestItem
{
public TcpClient Client { get; set; }
public byte[] Data{ get; set; }
}
四、客户端模块完整拆解
4.1 客户端全局变量
cs
TcpClient tcpClient;
CancellationTokenSource cts;
CancellationTokenSource cts2; // 异步轮询专用
4.2 初始化服务器配置
cs
private void ClientFrm_Load(object sender, EventArgs e)
{
InitServerConfig();
}
private void InitServerConfig()
{
txtServerIP.Text = ConfigurationManager.AppSettings["ServerIP"];
txtServerPort.Text = ConfigurationManager.AppSettings["ServerPort"];
}
4.3 连接/断开服务器
cs
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 客户端接收服务端应答数据
cs
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:定时器轮询
cs
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
{
}
}
方式2:Task异步轮询(推荐)
cs
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 完整代码
cs
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 获取本机局域网IP
cs
public 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请求帧,解析设备应答数据并可视化展示,完整实现网口与串口设备的双向透传通信。