news 2026/9/9 3:57:49

C# Socket+串口Modbus设备通信 全套知识点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# Socket+串口Modbus设备通信 全套知识点

一、项目整体架构与核心流程(必考大题)

1.1 项目架构

本项目为Socket-TCP服务端 + 串口SerialPort 双向转发网关,实现 客户端 ↔ 服务器 ↔ 硬件设备 数据透传。

  • 服务器:TCP多客户端服务端 + 串口通信,核心转发中间层

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

  1. Socket服务器接收客户端帧,原样转发给串口硬件设备

  2. 服务器将响应帧精准转发给对应请求客户端(一对一应答,非广播)

  • 普通聊天室:服务端广播给所有客户端

  • 融合:TCP网络通信 + 串口硬件通信 + Modbus协议 + CRC16校验 + INI配置持久化

2.1 知识点

实现参数持久化,程序重启保留:IP、端口、串口号、波特率。

2.2 窗体加载读取配置代码

private void Form1_Load(object sender, EventArgs e) { BindConfig(); // 加载本地ini配置文件 if (File.Exists("config.ini")) { txtIP.Text = FileIni.Read("Config","ip"); txtPort.Text = FileIni.Read("Config", "port"); cbbPortNames.Text = FileIni.Read("Config", "com"); cbbBaudRate.Text = FileIni.Read("Config", "botelv"); } }

2.3 保存配置代码

private void btnSave_Click(object sender, EventArgs e) { FileIni.Write("Config","ip",txtIP.Text); FileIni.Write("Config", "port", txtPort.Text); FileIni.Write("Config", "com", cbbPortNames.SelectedItem.ToString()); FileIni.Write("Config", "botelv", cbbBaudRate.SelectedItem.ToString()); }

三、初始化绑定配置 BindConfig(知识点+代码)

自动获取本机IPv4、初始化默认端口、自动扫描串口、预置常用波特率/数据位/停止位/校验位。

3.2 对应完整代码

public void BindConfig() { // 1.自动获取本机IPv4 IPAddress[] address = Dns.GetHostAddresses(Dns.GetHostName()); foreach (IPAddress ip in address) { if (ip.AddressFamily == AddressFamily.InterNetwork) { txtIP.Text = ip.ToString(); } } // 2.默认端口9999 txtPort.Text = 9999.ToString(); // 3.扫描本机所有串口 cbbPortNames.DataSource = SerialPort.GetPortNames(); cbbPortNames.SelectedIndex = 0; // 4.预置波特率 int botelv = 1200; cbbBaudRate.Items.Add(botelv); for (int i = 1; i < 5; i++) { if (i != 3) { cbbBaudRate.Items.Add(botelv * i * 2); } } cbbBaudRate.SelectedIndex = 3; // 5.串口默认参数 cbbDataBits.Items.Add(8); cbbDataBits.SelectedIndex = 0; cbbStopBits.Items.Add("One"); cbbStopBits.SelectedIndex = 0; cbbParity.Items.Add("None"); cbbParity.SelectedIndex = 0; }

四、服务端启动核心(Socket+串口双启动)

4.1 知识点

点击启动同时完成:TCP服务端监听启动 + 串口设备连接启动,双通道同时就绪。

4.2 启动服务代码

public void ConnectServer() { // 1.启动TCP Socket服务端 serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); serverSocket.Bind(new IPEndPoint(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text))); serverSocket.Listen(100); userControl13.BackColor = Color.Green; // 2.启动串口,绑定参数 serialport = new SerialPort(); serialport.BaudRate = int.Parse(cbbBaudRate.SelectedItem.ToString()); serialport.PortName = cbbPortNames.SelectedItem.ToString(); serialport.DataBits = int.Parse(cbbDataBits.SelectedItem.ToString()); serialport.Parity = (Parity)Enum.Parse(typeof(Parity), cbbParity.SelectedItem.ToString()); serialport.StopBits = (StopBits)Enum.Parse(typeof(StopBits), cbbStopBits.SelectedItem.ToString()); if (!serialport.IsOpen) serialport.Open(); // 绑定串口数据接收事件 serialport.DataReceived += Serialport_DataReceived; userControl11.BackColor = Color.Green; // 3.锁定配置控件,修改按钮状态 btnStart.Text = "停止"; isStart = true; txtIP.Enabled = txtPort.Enabled = cbbBaudRate.Enabled = cbbDataBits.Enabled = cbbParity.Enabled = cbbPortNames.Enabled = cbbStopBits.Enabled = false; }

五、多客户端监听机制(知识点+代码)

  • 使用List<Client>存储所有在线客户端

  • 重复终端自动去重,防止客户端重复连接残留

5.2 客户端实体类

public class Client { public EndPoint EndPoint { get; set; } // 客户端唯一终端标识 public Socket Socket { get; set; } // 客户端套接字对象 }

5.3 监听客户端连接代码

List<Client> clients = new List<Client>(); CancellationTokenSource cts; public void AcceptQuestion() { cts = new CancellationTokenSource(); Task task = new Task(AcceptClient, cts.Token); task.Start(); } public async void AcceptClient() { while (!cts.IsCancellationRequested) { try { // 异步接收客户端连接 Socket 客户端 = await serverSocket.AcceptAsync(); // 重复客户端去重 int index = clients.FindIndex(c => c.EndPoint == 客户端.RemoteEndPoint); if (index != -1) { clients.RemoveAt(index); } // 添加新客户端 clients.Add(new Client() { EndPoint = 客户端.RemoteEndPoint, Socket = 客户端 }); // 开启当前客户端消息监听 ReceiveMessage(客户端); } catch (Exception) { } } }

六、核心队列转发机制(考试最大难点)

使用ConcurrentQueue 线程安全队列存储请求客户端,实现请求-响应一对一匹配

6.2 客户端消息接收+入队代码

ConcurrentQueue<Socket> cq = new ConcurrentQueue<Socket>(); public void ReceiveMessage(Socket c1) { cts = new CancellationTokenSource(); Task task = new Task(async () => { while (!cts.IsCancellationRequested) { try { byte[] buffer = new byte[c1.Available]; int count = await c1.ReceiveAsync(new ArraySegment<byte>(buffer), SocketFlags.None); if (count > 0) { // 请求客户端入队 cq.Enqueue(c1); // 状态灯动画刷新 userControl13.BackColor = Color.Yellow; await Task.Delay(50); userControl13.BackColor = Color.Gray; await Task.Delay(50); userControl13.BackColor = Color.Green; // 转发原始Modbus帧到串口设备 serialport.Write(buffer, 0, buffer.Length); userControl11.BackColor = Color.Yellow; await Task.Delay(50); userControl11.BackColor = Color.Gray; await Task.Delay(50); userControl11.BackColor = Color.Green; // 统计接收字节数 Invoke(new Action(() => { int oldCount = int.Parse(txtReceiveByte.Text); txtReceiveByte.Text = (oldCount + count).ToString(); })); } } catch (Exception) { } } }, cts.Token); task.Start(); }

6.3 串口数据返回、精准回发客户端代码

private void Serialport_DataReceived(object sender, SerialDataReceivedEventArgs e) { byte[] buffer = new byte[serialport.BytesToRead]; int count = serialport.Read(buffer, 0, buffer.Length); if (count == 0) return; // 队列取出请求客户端,一对一响应 if (cq.TryDequeue(out Socket s1)) { // 校验客户端在线,防止离线残留 if (clients.Any(v => v.Socket == s1)) { s1.Send(buffer); } } // 统计发送字节 Invoke(new Action(() => { txtSendByte.Text = (int.Parse(txtSendByte.Text) + count).ToString(); })); }

七、服务端关闭释放资源(知识点+代码)

关闭必须同时释放:线程令牌、所有客户端、串口资源、服务端Socket、重置UI状态。

7.2 关闭方法代码

public void DisConnect() { // 终止所有后台线程 cts?.Cancel(); // 断开所有在线客户端 foreach (var item in clients) { Socket s1 = item.Socket; if (s1 != null && s1.Connected) { s1.Disconnect(false); } } clients.Clear(); // 关闭串口 if (serialport != null && serialport.IsOpen) serialport.Close(); // 关闭服务端 serverSocket?.Close(); serverSocket = null; // 重置UI btnStart.Text = "启动"; isStart = false; txtIP.Enabled = txtPort.Enabled = cbbBaudRate.Enabled = cbbDataBits.Enabled = cbbParity.Enabled = cbbPortNames.Enabled = cbbStopBits.Enabled = true; }

八、客户端核心功能知识点+代码

8.1 客户端连接与断开

Socket client; CancellationTokenSource cts = null; public async void ConnectServer() { client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text))); btnConnOrClose.Text = "断开"; } public void DisConnect() { cts?.Cancel(); client?.Disconnect(false); client?.Close(); btnConnOrClose.Text = "连接"; }

8.2 客户端接收解析Modbus响应帧

知识点:解析03功能码读寄存器响应帧,拆分高低位拼接真实数值

public void AcceptData() { cts = new CancellationTokenSource(); Task.Run(() => { while (!cts.IsCancellationRequested) { byte[] buffer = new byte[client.Available]; int count = client.Receive(buffer); if (count == 0) continue; Invoke(new Action(() => { // 解析03读寄存器响应 if (buffer[1] == 0x03) { // 高低8位拼接16位寄存器数值 txtReadData1.Text = (buffer[3] * 256 + buffer[4]).ToString(); txtReadData2.Text = (buffer[5] * 256 + buffer[6]).ToString(); } })); } }, cts.Token); }

8.3 定时自动轮询(定时器知识点)

System.Timers.Timer t1; private void cbRealTimeRead_CheckedChanged(object sender, EventArgs e) { if (cbRealTimeRead.Checked) { if (client != null && client.Connected) { // 1秒定时下发读取指令 t1 = new System.Timers.Timer(1000); t1.Start(); t1.Elapsed += (s, e1) => { // 定时发送Modbus读取帧 SendDataHelper.SendData(client, 1, 3, 0, 2); }; } } else { t1.Stop(); txtReadData1.Text = ""; txtReadData2.Text = ""; } }

九、Modbus帧组装 + CRC16校验(超级重点)

标准Modbus RTU帧结构:从站ID + 功能码 + 起始地址(2字节) + 数据长度(2字节) + CRC16校验(2字节),固定8字节。

9.2 帧组装工具类代码

public class SendDataHelper { public static void SendData(Socket s, ushort slaveId, ushort functionCode, ushort startAddress, ushort datalength) { byte[] data = new byte[8]; data[0] = (byte)slaveId; data[1] = (byte)functionCode; // 起始地址 高8位、低8位 data[2] = (byte)(startAddress / 256); data[3] = (byte)(startAddress % 256); // 数据长度 高8位、低8位 data[4] = (byte)(datalength / 256); data[5] = (byte)(datalength % 256); // CRC16校验 byte[] crc = CRCHelper.CRC16(data); data[6] = crc[0]; data[7] = crc[1]; s.Send(data); } }

9.3 标准Modbus CRC16算法(考试必背完整代码)

public static class CRCHelper { // Modbus标准CRC16校验(低字节在前、高字节在后) public static byte[] CRC16(byte[] data) { ushort crc = 0xFFFF; foreach (byte b in data) { crc ^= b; for (int i = 0; i < 8; i++) { if ((crc & 0x0001) != 0) { crc = (ushort)((crc >> 1) ^ 0xA001); } else { crc >>= 1; } } } // 返回低字节在前、高字节在后(Modbus标准) return new byte[] { (byte)(crc & 0xff), (byte)(crc >> 8) }; } }

十、所有高频考点汇总(背诵版)

  • 网关作用:TCP网络帧 ↔ 串口设备帧 双向透传转发

  • INI配置:实现参数持久化,无需每次重启手动输入

10.2 Modbus协议考点

  • 06功能码:写入单个寄存器数据

  • CRC16为Modbus必须校验,防止数据传输错误

  • 子线程接收数据、串口事件,更新UI必须Invoke

  • CancellationTokenSource安全终止后台任务,防止内存泄漏

十一、终结

本项目为TCP-Socket与串口结合的Modbus设备转发网关。服务端启动后同时监听TCP客户端连接与串口设备,通过泛型集合管理多客户端,利用线程安全队列缓存请求客户端,实现客户端请求帧转发至硬件设备、设备响应帧精准回发对应客户端的一对一透传功能。客户端通过定时器定时自动下发带CRC16校验的标准Modbus读写指令,解析设备返回的高低位寄存器数据。程序通过INI文件实现配置持久化,全程异步线程通信、跨线程UI刷新、资源完整释放,实现网络与硬件设备的稳定数据交互。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 3:57:25

多目标规划序贯算法:从原理到Matlab实战,解决目标冲突问题

1. 项目概述&#xff1a;当数学建模遇上多目标难题 在数学建模竞赛和实际的科研、工程问题里&#xff0c;我们常常会遇到一个让人头疼的局面&#xff1a;目标不止一个&#xff0c;而且它们之间还经常“打架”。比如&#xff0c;在设计一个产品时&#xff0c;我们既希望它的成本…

作者头像 李华
网站建设 2026/8/30 15:30:15

Scratch国赛真题“河马带球”深度解析:事件驱动与碰撞检测实战

1. 项目概述&#xff1a;从一道国赛真题看Scratch编程的核心能力最近在整理历年蓝桥杯Scratch国赛的真题&#xff0c;发现“河马带球”这道题非常有意思&#xff0c;它不仅是检验孩子编程逻辑的试金石&#xff0c;更是理解事件驱动、坐标控制和条件判断等核心概念的绝佳案例。很…

作者头像 李华
网站建设 2026/8/30 17:13:54

面向AI Agent的搜索API:从选型到接入的完整指南

最近在关注 AI Agent 工具链的时候&#xff0c;看到 Show HN 上有个项目叫 Keenable&#xff0c;定位写得很直接&#xff1a;A different web search API for AI agents。简单说&#xff0c;它想做的不是又一个通用搜索接口&#xff0c;而是让搜索 API 返回的内容更适合 AI Age…

作者头像 李华
网站建设 2026/8/31 12:23:14

数学建模实战:回归分析高级应用与模型诊断优化指南

1. 项目概述&#xff1a;回归分析在数学建模中的核心地位 备战数学建模&#xff0c;尤其是像国赛、美赛这类高强度竞赛&#xff0c;时间紧、任务重&#xff0c;最怕的就是拿到题目后对着数据发呆&#xff0c;不知道从何下手。我参加过几次比赛&#xff0c;也带过不少队伍&#…

作者头像 李华
网站建设 2026/8/31 10:03:18

深度学习期末复习指南:从神经网络基础到前沿模型实战

1. 期末复习的“道”与“术”&#xff1a;从知识罗列到体系构建又到了学期末&#xff0c;看着“深度学习”这门课厚厚一摞的PPT和笔记&#xff0c;是不是感觉头大如斗&#xff1f;公式、模型、算法、代码……知识点像散落一地的珍珠&#xff0c;捡起这颗&#xff0c;又丢了那颗…

作者头像 李华
网站建设 2026/8/30 14:51:40

CSP-S 2022策略游戏 题解

题目大意 给定数组 A 长度 n&#xff0c;数组 B 长度 m&#xff0c;矩阵C{i,j}Ai*Bj。 每轮查询给出l1,r1,l2,r2&#xff1a; 小L选x[l1,r1] 小Q看到x之后选y[l2,r2] 小L希望得分 C{x,y}尽可能大 小Q希望得分尽可能小 两人都采取最优策略&#xff0c;求最终得分。 暴力核心思路…

作者头像 李华