C# UDP+串口Modbus转发网关

一、项目整体架构(大题必考)

1.1 通信架构

本项目为UDP-Socket 网络 ↔ SerialPort 硬件串口双向转发网关

  • 客户端:UDP客户端,定时自动下发 Modbus 03功能码读取帧

  • 服务端:UDP服务端 + 串口服务,核心数据转发中间层

  • 下位机:串口Modbus RTU设备

1.2 标准四层转发流程(背诵满分)

  1. UDP客户端发送 Modbus 请求帧(带CRC16校验)

  2. UDP服务端接收帧,原样转发给串口硬件设备,并将请求客户端终端入队

  3. 硬件设备返回 Modbus 响应帧给服务端串口

  4. 服务端从队列取出对应客户端,一对一精准回发响应帧

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读取指令,解析设备返回的高低位寄存器数据。程序实现参数初始化、异步通信、队列匹配、资源完整释放,完成网络终端与串口硬件设备的稳定数据交互。

相关推荐
北冥you鱼2 小时前
HTTPS 推送失败?改用 SSH 一键搞定
网络协议·https·ssh
2401_868534787 小时前
指令集-最常用 Linux 命令速查手册(整合版)
linux·网络协议
dog25010 小时前
再论 TCP 的流式传输
网络·网络协议·tcp/ip
fox081511 小时前
栈空间带来的大问题
单片机·嵌入式硬件
wuyk55512 小时前
从零吃透Modbus通信|第3章:STM32 Modbus RTU主机
服务器·开发语言·网络·数据结构·stm32·单片机·嵌入式硬件
喜欢吃燃面13 小时前
HTTP协议深度解析:从请求响应到状态管理与安全传输
linux·网络协议·学习
恒锐丰小瑞13 小时前
率能SS6285L 18V/5A/单通道直流电机驱动IC,超低导通电阻(58mΩ)与多重保护,用于电子锁/玩具/机器人
单片机·嵌入式硬件·机器人
傻啦嘿哟14 小时前
某宝商品爬虫:破解反爬的终极方案(含IP代理池+User-Agent轮换)
爬虫·网络协议·tcp/ip
天空'之城14 小时前
单片机基础核心知识点汇总(二十)
单片机·嵌入式硬件