一、项目整体架构(大题必考)
1.1 通信架构
本项目为UDP-Socket 网络 ↔ SerialPort 硬件串口双向转发网关
-
客户端:UDP客户端,定时自动下发 Modbus 03功能码读取帧
-
服务端:UDP服务端 + 串口服务,核心数据转发中间层
-
下位机:串口Modbus RTU设备
1.2 标准四层转发流程(背诵满分)
-
UDP客户端发送 Modbus 请求帧(带CRC16校验)
-
UDP服务端接收帧,原样转发给串口硬件设备,并将请求客户端终端入队
-
硬件设备返回 Modbus 响应帧给服务端串口
-
服务端从队列取出对应客户端,一对一精准回发响应帧
1.3 核心区别(TCP网关 VS UDP网关)
-
TCP网关:基于连接,存Socket对象
-
UDP网关:无连接,只存 EndPoint 终端地址 + 队列匹配请求
二、服务端全局变量含义(考点)
cs
Socket socketSocket; // UDP服务端套接字
SerialPort serialPort; // 串口设备通信对象
CancellationTokenSource cts; // 异步线程取消令牌
List<EndPoint> clients = new List<EndPoint>(); // 在线客户端终端集合
ConcurrentQueue<EndPoint> cq; // 线程安全队列:匹配请求客户端
重点:ConcurrentQueue 解决多线程并发请求,实现请求-响应一对一绑定
三、串口参数初始化 BindConfig(知识点+完整代码)
3.1 知识点
自动扫描本机串口、预置标准波特率、默认8N1串口参数(8数据位、无校验、1停止位)
3.2 对应代码
cs
public void BindConfig()
{
// 扫描本机所有串口
cobPortName.DataSource = SerialPort.GetPortNames();
cobPortName.SelectedIndex = 0;
// 预置常用波特率
int botev = 1200;
cobBaudRate.Items.Add(botev);
for (int i = 1; i < 5; i++)
{
if (i != 3)
{
cobBaudRate.Items.Add(botev * i * 2);
}
}
cobBaudRate.SelectedIndex = 3;
// 默认串口参数 8N1
cobDataBits.Items.Add(8);
cobDataBits.SelectedIndex = 0;
cobStopBits.Items.Add("One");
cobStopBits.SelectedIndex = 0;
cobParity.Items.Add("None");
cobParity.SelectedIndex = 0;
}
四、服务端启动核心 StartServer(高频考点)
4.1 核心考点
-
UDP套接字固定参数:Dgram 数据报模式
-
自动获取本机IPv4地址
-
同时启动 UDP网络服务 + 串口硬件服务
-
启动后锁定串口配置控件,防止运行中修改
4.2 完整代码
cs
public void StartServer()
{
string IP = "";
// 创建UDP服务端套接字
socketSocket=new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
// 自动获取本机IPv4
IPAddress[] addresses = Dns.GetHostAddresses(Dns.GetHostName());
foreach (IPAddress ip in addresses)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
IP = ip.ToString();
}
}
// 绑定固定端口9999
socketSocket.Bind(new IPEndPoint(IPAddress.Parse(IP),9999));
panel2.BackColor = Color.Green;
// 初始化并打开串口
serialPort = new SerialPort();
serialPort.BaudRate=int.Parse(cobBaudRate.SelectedItem.ToString());
serialPort.PortName=cobPortName.Text;
serialPort.DataBits=int.Parse(cobDataBits.SelectedItem.ToString());
serialPort.Parity=(Parity)Enum.Parse(typeof(Parity), cobParity.SelectedItem.ToString());
serialPort.StopBits=(StopBits)Enum.Parse(typeof(StopBits),cobStopBits.SelectedItem.ToString());
if (!serialPort.IsOpen)
{
serialPort.Open();
}
// 绑定串口数据接收事件
serialPort.DataReceived += SerialPort_DataReceived;
panel1.BackColor = Color.Green;
// 修改界面状态
btnStart.Text = "停止";
foreach (Control item in groupBox1.Controls)
{
if (item is ComboBox)
{
((ComboBox)item).Enabled = false;
}
}
toolStripStatusLabel1.Text = "服务器正在运行";
}
五、UDP服务端接收+入队机制(核心难点)
5.1 原理
客户端发请求 → 服务端接收 → 客户端终端入队 → 数据转发串口设备
UDP无连接,依靠 EndPoint队列 保证响应帧返回给正确客户端
5.2 完整代码
cs
public void AcceptQuestion()
{
cts=new CancellationTokenSource();
Task.Run(async () =>
{
while (!cts.IsCancellationRequested)
{
try
{
if (socketSocket.Available == 0)
{
continue;
}
byte[] buffer = new byte[socketSocket.Available];
EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0);
// 异步UDP接收,获取请求客户端终端
var result=await socketSocket.ReceiveFromAsync(new ArraySegment<byte>(buffer),SocketFlags.None, endPoint);
int count= result.ReceivedBytes;
EndPoint actPoint = result.RemoteEndPoint;
// 客户端去重
int index = clients.FindIndex(c => c.ToString()==actPoint.ToString());
if (index != -1) { clients.RemoveAt(index); }
clients.Add(actPoint);
// 有效请求入队 + 转发串口
if (count>0 && serialPort.IsOpen && serialPort!=null)
{
cq.Enqueue(actPoint);
Invoke(new Action(() =>
{
serialPort.Write(buffer, 0, count);
int oldCount=int.Parse(textBox2.Text);
textBox2.Text=(oldCount+ count).ToString();
}));
}
}
catch (Exception)
{
Console.WriteLine("sss");
}
}
}, cts.Token);
}
六、串口返回数据、UDP精准回发(必考)
6.1 原理
设备返回响应帧 → 从队列取出对应请求客户端 → UDP SendTo 一对一发送
6.2 完整代码
cs
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (serialPort.BytesToRead==0)
{
return;
}
byte[] buffer=new byte[serialPort.BytesToRead];
int count=serialPort.Read(buffer,0,buffer.Length);
// 队列匹配客户端,精准回发
if (cq.TryDequeue(out EndPoint s1))
{
if (clients.Any(x=>s1==x))
{
socketSocket.SendTo(buffer, s1);
}
}
// 统计接收字节
Invoke(new Action(() =>
{
textBox1.Text = (int.Parse(textBox1.Text) + count).ToString();
}));
}
七、服务端停止释放资源代码
cs
public void StopServer()
{
cts?.Cancel();
clients.Clear();
panel2.BackColor = Color.Gray;
serialPort?.Close();
socketSocket?.Close();
btnStart.Text = "启动";
toolStripStatusLabel1.Text = "服务器未启动";
panel1.BackColor = Color.Gray;
// 解锁串口配置控件
foreach (Control item in groupBox1.Controls)
{
if (item is ComboBox)
{
((ComboBox)item).Enabled = true;
}
}
}
八、UDP客户端全套知识点+代码
8.1 客户端连接(UDP无连接特性)
UDP客户端无需Connect,直接指定服务端EndPoint即可通信
cs
Socket client;
EndPoint serverPoint;
CancellationTokenSource cts;
public void ConnectServer()
{
client=new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
serverPoint=new IPEndPoint(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text));
btnConnect.Text = "断开";
txtIP.Enabled=txtPort.Enabled=false;
}
8.2 客户端接收并解析Modbus响应帧
考点:Modbus 03功能码响应帧,高8位*256+低8位 拼接16位寄存器值
cs
public void AcceptData()
{
cts= new CancellationTokenSource();
Task.Run(async () =>
{
while (!cts.IsCancellationRequested)
{
try
{
byte[] buffer = new byte[1024];
EndPoint endpoint= new IPEndPoint(IPAddress.Any,0);
var result=await client.ReceiveFromAsync(new ArraySegment<byte>(buffer), SocketFlags.None, endpoint);
int count=result.ReceivedBytes;
serverPoint = result.RemoteEndPoint;
// 过滤无效短帧
if (count < 13) { continue; }
// 解析4组寄存器数据
Invoke(new Action(() =>
{
int data1 = buffer[3] * 256 + buffer[4];
txtShuju1.Text=data1.ToString();
int data2 = buffer[5] * 256 + buffer[6];
txtShuju2.Text = data2.ToString();
int data3 = buffer[7] * 256 + buffer[8];
txtShuju3.Text = data3.ToString();
int data4 = buffer[9] * 256 + buffer[10];
txtShuju4.Text = data4.ToString();
}));
}
catch (Exception)
{
Console.WriteLine("ss");
}
}
},cts.Token);
}
8.3 客户端定时轮询读取
定时器每隔500ms自动下发Modbus读取请求帧,实现实时采集
cs
System.Timers.Timer timer1;
private void btnRead_Click(object sender, EventArgs e)
{
if (btnRead.Text == "实时读取")
{
if (client == null || serverPoint == null)
{
MessageBox.Show("请先连接服务器");
return;
}
if (timer1 == null)
{
timer1 = new System.Timers.Timer(500);
timer1.Elapsed += (s, e1) =>
{
// 动态传入从站ID、功能码、起始地址、长度
SendDataHelper.SendData(client, serverPoint,
ushort.Parse(textBox1.Text),
3,
ushort.Parse(textBox2.Text),
ushort.Parse(textBox3.Text));
};
}
timer1.Start();
btnRead.Text = "停止读取";
}
else
{
timer1.Stop();
btnRead.Text = "实时读取";
}
}
8.4 客户端断开资源释放
cs
public void DisConnect()
{
// 停止定时读取
if (btnRead.Text=="停止读取")
{
btnRead.PerformClick();
}
cts?.Cancel();
client?.Close();
client = null;
serverPoint = null;
txtIP.Enabled=txtPort.Enabled=true;
btnConnect.Text = "连接";
}
九、Modbus帧组装工具类(必考代码)
9.1 帧结构
从站ID(1) + 功能码(1) + 起始地址高/低(2) + 长度高/低(2) + CRC16校验(2) = 8字节标准帧
9.2 完整代码
cs
public class SendDataHelper
{
public static void SendData(Socket s,EndPoint desPoint,ushort slaveId,ushort functionCode, ushort startAddress,ushort datalength)
{
byte[] data = new byte[8];
data[0] = (byte)slaveId;
data[1] = (byte)functionCode;
// 起始地址高低位拆分
data[2] = (byte)(startAddress /256);
data[3] = (byte)(startAddress % 256);
// 数据长度高低位拆分
data[4] = (byte)(datalength / 256);
data[5] = (byte)(datalength % 256);
// CRC16校验
byte[] crc = CRCHelper.CRC16(data);
data[6] = crc[0];
data[7] = crc[1];
// UDP定点发送
s.SendTo(data,desPoint);
}
}
十、完整 CRC16 工具类(可直接编译,考试必写)
10.1 知识点
Modbus RTU 协议必须附带CRC16校验码 ,校验码占2字节,低字节在前、高字节在后,防止传输过程中数据丢包、错乱。
10.2 完整工具类代码
cs
/// <summary>
/// Modbus CRC16 校验工具类(标准RTU算法)
/// </summary>
public static class CRCHelper
{
/// <summary>
/// 计算Modbus CRC16校验码
/// </summary>
/// <param name="data">待校验的原始指令帧(不含CRC)</param>
/// <returns>CRC16结果数组[0]=低字节,[1]=高字节(Modbus标准顺序)</returns>
public static byte[] CRC16(byte[] data)
{
// 初始寄存器 0xFFFF
ushort crc = 0xFFFF;
// 多项式 0xA001(Modbus标准)
foreach (byte b in data)
{
crc ^= b;
for (int i = 0; i < 8; i++)
{
// 判断最低位是否为1
if ((crc & 0x0001) != 0)
{
crc = (ushort)((crc >> 1) ^ 0xA001);
}
else
{
crc >>= 1;
}
}
}
// 返回 低字节在前、高字节在后,符合Modbus协议规范
return new byte[] { (byte)(crc & 0x00FF), (byte)(crc >> 8) };
}
}
10.3 重点考点
-
初始值:0xFFFF
-
多项式:0xA001
-
输出顺序:低字节在前,高字节在后
-
作用:保证Modbus指令帧完整性,设备才可识别指令
十一、终极考点总结(背诵版)
11.1 UDP网关核心考点
-
UDP无连接:无需Listen、Accept、Connect,只用SendTo/ReceiveFrom
-
使用 ConcurrentQueue 解决多客户端并发请求匹配问题
-
服务端只保存 EndPoint 终端,不保存Socket对象
-
双向透传:网络帧→串口设备、设备帧→对应网络客户端
11.2 Modbus协议考点
-
03功能码:读取寄存器数据
-
16位寄存器 = 高8位*256 + 低8位
-
必须CRC16校验,保证帧合法
11.3 线程与UI考点
-
异步Task接收数据,子线程更新UI必须 Invoke
-
CancellationTokenSource 安全终止线程,防止内存泄漏
-
ConcurrentQueue 线程安全,多线程入队出队不冲突
十二、简答题满分模版
本项目基于UDP无连接数据报协议与SerialPort串口实现Modbus设备转发网关。服务端启动UDP数据报套接字监听网络客户端,同时开启串口与硬件设备通信,通过线程安全队列存储请求客户端终端地址,实现客户端Modbus请求帧转发至串口设备、设备响应帧精准回发对应客户端的一对一透传功能。客户端通过定时器定时自动下发带CRC16校验的标准Modbus读取指令,解析设备返回的高低位寄存器数据。程序实现参数初始化、异步通信、队列匹配、资源完整释放,完成网络终端与串口硬件设备的稳定数据交互。