一、项目整体架构与核心流程(必考大题)
1.1 项目架构
本项目为Socket-TCP服务端 + 串口SerialPort 双向转发网关,实现 客户端 ↔ 服务器 ↔ 硬件设备 数据透传。
服务器:TCP多客户端服务端 + 串口通信,核心转发中间层
1.2 标准四层转发流程(背诵满分)
Socket服务器接收客户端帧,原样转发给串口硬件设备
服务器将响应帧精准转发给对应请求客户端(一对一应答,非广播)
普通聊天室:服务端广播给所有客户端
融合: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刷新、资源完整释放,实现网络与硬件设备的稳定数据交互。