C# TCP转串口Modbus网关终极完整版笔记(队列防抖+一问一答+帧解析+双客户端轮询)

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

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 解决的三大工业通信问题

  1. 多客户端并发乱包:ConcurrentQueue队列+排他锁串行处理

  2. 串口粘包分包:List缓冲区累加+Modbus帧长度精准截取

  3. 问答不匹配:RequestItem绑定客户端与请求,应答原路返回

6.2 两种轮询优缺点

  • 定时器轮询:UI线程执行,阻塞轻微,简单易用

  • Task异步轮询:后台线程执行,不卡UI,稳定高效,工业首选

6.3 Modbus RTU帧解析公式

数据值 = 高位字节 * 256 + 低位字节

整帧长度 = 3 + 数据长度 + 2(CRC)

七、满分简答题模板

本项目实现TCP转串口Modbus RTU网关功能,服务端搭建TCP监听服务并初始化串口设备通信,采用线程安全队列缓存多客户端并发请求,通过排他锁实现指令串行下发,避免串口并发冲突。通过缓冲区拼接方式解决串口流式数据粘包分包问题,依据Modbus帧结构精准截取完整应答帧,并原路回传给对应请求客户端。客户端支持定时器与异步Task两种轮询方式,通过CRC16算法自主生成合法Modbus请求帧,解析设备应答数据并可视化展示,完整实现网口与串口设备的双向透传通信。

相关推荐
L@ncor13 小时前
第五章 基于低代码平台的智能体搭建 · 学习笔记(Coze / Dify / FastGPT / n8n)
笔记·学习·低代码·agent·prompt工程
知潮网13 小时前
被嫌弃的五仁月饼,一吨千元在养猪场翻了身
笔记
2601_9606312313 小时前
中药前处理及提取的工艺环节与技术要点
笔记
2601_9653842613 小时前
口腔黏膜清洁中的几何约束与流体动力学边界层问题分析
经验分享·笔记
XiaoMaqqqq13 小时前
目前知名的IP驱动产业新场景新工具有哪些
网络·python·网络协议·tcp/ip
一米阳光866114 小时前
软考高级【信息系统项目管理师】高项重要考点(9)组织 助你顺利上岸!
笔记·职场发展·软考·高项·信息系统项目管理师·高级职称
白杨尚青14 小时前
C++入门篇(十):string(上)——认识string:构造与三大遍历(一条龙讲透operator[]、迭代器、auto、范围for)
java·开发语言·c++·笔记·stl
Chen—LSN14 小时前
C语言——⽂件操作(1)
c语言·开发语言·c++·经验分享·笔记·算法·链表
Chen—LSN15 小时前
C语言——文件操作(2)
c语言·开发语言·c++·经验分享·笔记·算法·c#