上位机实践1

发布时间:2026/7/31 22:15:55
上位机实践1 1编写Tcp客户端工具要求实现可同步/异步接收Tcp服务端的数据Tcp服务端可使用现有第三方功能2拓展工具功能支持Modbus通信支持基本的参数配置Modbus服务端可使用Modbus Slave;3自行编写Tcp服务端代替之前的第三方Tcp服务端工具要求可支持主动/被动 发送模式4自行编写Modbus服务端代替之前使用的Modbus Slave要求可实现自定义站号/方法/地址等常用服务参数5自定义数据协议且上面的客户端和服务可根据自定义数据协议实现各自完整的通信流程1、tcp客户端/服务端界面Tcp服务端开启监听等待客户端连接这时候需要相同的ipport才能够完成通信和客户端发起连接。客户端主动发送数据后服务端会接收到发送的数据。服务端会主动下发数据到客户端。服务端的被动发送是客户端发送请求后服务端再返回特定的返回数据。1、TcpClientManager.csusingSystem;usingSystem.Net.Sockets;usingSystem.Text;usingSystem.Threading.Tasks;namespaceTcpClientApp{publicclassTcpClientManager{privateSystem.Net.Sockets.TcpClientclient;privateNetworkStreamstream;// 连接客户端publicboolConnect(stringip,intport){try{Disconnect();clientnewSystem.Net.Sockets.TcpClient();client.Connect(ip,port);streamclient.GetStream();returntrue;}catch(Exceptionex){Console.WriteLine(ex.Message);returnfalse;}}//异步发送数据publicasyncTaskSendAsync(stringmsg){if(clientnull||!client.Connected||streamnull)return;byte[]dataEncoding.UTF8.GetBytes(msg);awaitstream.WriteAsync(data,0,data.Length);}//同步发送数据未使用publicstringReceiveSync(){if(streamnull)returnnull;byte[]buffernewbyte[1024];intlengthstream.Read(buffer,0,buffer.Length);returnEncoding.UTF8.GetString(buffer,0,length);}// 异步接收数据publicasyncTaskReceiveAsync(Actionstringcallback){if(streamnull)return;byte[]buffernewbyte[1024];try{while(client!nullclient.Connected){intlenawaitstream.ReadAsync(buffer,0,buffer.Length);if(len0){stringmsgEncoding.UTF8.GetString(buffer,0,len);callback(msg);}else{break;}}}catch{// 忽略断开异常}}// 断开连接publicvoidDisconnect(){try{stream?.Close();client?.Close();streamnull;clientnull;}catch{}}}}知识点1、System.Net.Sockets什么是 SocketSocket套接字 是操作系统提供的一种网络通信抽象机制。实际作用它封装了底层的 TCP/IP、UDP 等协议细节。不管是浏览器请求网页还是游戏连接服务器底层都是通过 Socket建立连接并传送数据的。代码中的 TcpClientSystem.Net.Sockets.TcpClient 是 C# 在低级 Socket类之上封装的高级类专门用来简化 TCP 协议客户端的开发。2、GetStream() 的作用GetStream() 是 TcpClient 的一个方法用于获取网络字节流NetworkStream。TCP 是基于字节流Byte Stream传输的。获得 NetworkStream 后你就可以像对文件流FileStream一样使用 Write / WriteAsync 向服务端发送字节或者用 Read / ReadAsync 从服务端读取字节。3、async Task异步编程用法async 和 Task 是 C# 中实现异步编程Async/Await 模式的核心关键字用来避免阻塞 UI 界面或主线程。单线程阻塞问题如果在主线程调用同步的 stream.Read(…)如果服务器一直没发数据你的软件界面如 WinForms / WPF就会卡死。Task代表一个正在进行或将要完成的操作相当于一个“承诺”或异步任务。async / awaitasync 修饰方法表明该方法内部有异步操作。await 用于暂停当前方法的执行把线程释放回系统处理其他事比如刷新 UI直到异步操作如网络传输完成后再自动接着往下走。4、Action callback回调函数/委托用法Action 是 C# 内置的一种委托Delegate用来把一个方法作为参数传递给另一个方法。5、stream?.Close()空条件运算符与资源释放if (stream ! null){stream.Close();}2、TcpServerManager.csusingSystem;usingSystem.Net;usingSystem.Net.Sockets;usingSystem.Text;usingSystem.Threading.Tasks;namespaceTcpServerApp{publicclassTcpServerManager{privateTcpListenerlistener;privateSystem.Net.Sockets.TcpClientclient;publicboolIsAutoReply{get;set;}false;publicstringAutoReplyContent{get;set;};// 开启监听publicasyncTaskStartAsync(stringip,intport){Stop();listenernewTcpListener(IPAddress.Parse(ip),port);listener.Start();clientawaitlistener.AcceptTcpClientAsync();}// 接收异步传输publicasyncTaskReceiveAsync(ActionstringonMessageReceived){if(clientnull||!client.Connected)return;NetworkStreamstreamclient.GetStream();byte[]buffernewbyte[1024];try{while(client!nullclient.Connected){intlenawaitstream.ReadAsync(buffer,0,buffer.Length);if(len0){stringmsgEncoding.UTF8.GetString(buffer,0,len);onMessageReceived?.Invoke(msg);// 被动自动应答逻辑if(IsAutoReply){stringreplyMsgstring.IsNullOrEmpty(AutoReplyContent)?$[服务端被动回复]: 已成功收到数据 [{msg}]:AutoReplyContent;awaitSendAsync(replyMsg);onMessageReceived?.Invoke($[服务端被动自动回复] ➔{replyMsg});}}else{onMessageReceived?.Invoke([提示] 客户端已断开连接);break;}}}catch(Exceptionex){onMessageReceived?.Invoke($[提示] 连接中断:{ex.Message});}}// 异步发送数据publicasyncTaskSendAsync(stringmsg){if(clientnull||!client.Connected)return;NetworkStreamstreamclient.GetStream();byte[]dataEncoding.UTF8.GetBytes(msg);awaitstream.WriteAsync(data,0,data.Length);}// 断开连接publicvoidStop(){try{client?.Close();listener?.Stop();clientnull;listenernull;}catch{}}}}2、modbus主站从站从站开始监听主站发起来连接也是对应相同的ipport。主站实现了01 到 06功能码只提供了读取寄存器和写入寄存器这两个按钮。可以从起始地址、读取长度、写入值设置寄存器的内容。从站动态改变值。1、ModbusTcpServerusingSystem;usingNModbus;usingTcpClientApp.Config;namespaceTcpClientApp{publicclassModbusTcpClient{privateSystem.Net.Sockets.TcpClienttcpClient;privateIModbusMastermaster;// 主站自动开启publicboolConnect(ModBusConfigconfig){try{tcpClientnewSystem.Net.Sockets.TcpClient(config.Ip,config.Port);varfactorynewModbusFactory();masterfactory.CreateMaster(tcpClient);returntrue;}catch{returnfalse;}}// 读线圈publicbool[]ReadCoils(byteslaveId,ushortstartAddress,ushortcount){returnmaster?.ReadCoils(slaveId,startAddress,count);}// 读保持寄存器publicushort[]ReadHoldingRegisters(byteslaveId,ushortaddress,ushortcount){returnmaster?.ReadHoldingRegisters(slaveId,address,count);}// 读输入寄存器publicushort[]ReadInputRegisters(byteslaveId,ushortaddress,ushortcount){returnmaster?.ReadInputRegisters(slaveId,address,count);}// 写入线圈publicvoidWriteCoil(byteslaveId,ushortaddress,boolvalue){master?.WriteSingleCoil(slaveId,address,value);}// 写入寄存器publicvoidWriteRegister(byteslaveId,ushortaddress,ushortvalue){master?.WriteSingleRegister(slaveId,address,value);}}}2、ModbusTcpClientusingSystem;usingSystem.Net;usingSystem.Net.Sockets;usingSystem.Threading.Tasks;usingNModbus;namespaceTcpServerApp{publicclassModbusTcpServer{privateTcpListener_listener;privateIModbusSlaveNetwork_slaveNetwork;privateIModbusSlave_slave;publicbyteSlaveId{get;privateset;}1;publicboolIsRunning{get;privateset;}false;publiceventActionushort[]OnRegistersUpdated;// 开启从站publicasyncTaskStartAsync(stringip127.0.0.1,intport502,byteslaveId1,ushortstartAddress0,ushort[]initialRegistersnull){if(IsRunning)return;SlaveIdslaveId;IPAddressiPAddressIPAddress.Parse(ip);_listenernewTcpListener(iPAddress,port);_listener.Start();varfactorynewModbusFactory();_slaveNetworkfactory.CreateSlaveNetwork(_listener);_slavefactory.CreateSlave(slaveId);if(initialRegisters!nullinitialRegisters.Length0){_slave.DataStore.HoldingRegisters.WritePoints(startAddress,initialRegisters);}else{ushort[]defaultRegistersnewushort[]{0,0,0,0,0,0,0,0,0,0};_slave.DataStore.HoldingRegisters.WritePoints(0,defaultRegisters);}_slaveNetwork.AddSlave(_slave);IsRunningtrue;// ✨ 开启后台 200ms 实时监听一旦客户端写入修改内存立即自动通知 UI 刷新看板与日志_Task.Run(async(){ushort[]lastValuesnewushort[10];while(IsRunning){awaitTask.Delay(200);if(_slave!null){ushort[]currentGetHoldingRegisters(0,10);boolchangedfalse;for(inti0;icurrent.Length;i){if(current[i]!lastValues[i]){changedtrue;break;}}if(changed){Array.Copy(current,lastValues,current.Length);OnRegistersUpdated?.Invoke(current);}}}});await_slaveNetwork.ListenAsync();}//写入保持寄存器publicvoidSetHoldingRegisters(ushortstartAddress,ushort[]values){if(_slavenull)return;_slave.DataStore.HoldingRegisters.WritePoints(startAddress,values);}//读取保持寄存器publicushort[]GetHoldingRegisters(ushortstartAddress,ushortcount){if(_slavenull)returnnewushort[0];return_slave.DataStore.HoldingRegisters.ReadPoints(startAddress,count);}// 断开连接publicvoidStop(){if(!IsRunning)return;_slaveNetwork?.Dispose();_listener?.Stop();IsRunningfalse;}}}1、为什么变量名开头有一个下划线 _变量名开头的下划线 _ 是一种 C# 代码命名规范Coding Convention用来标识这个变量是类的私有成员变量Private Field。快速区分变量作用域在代码中看到 _listener就能立马知道它是定义在整个类层级的私有字段而如果看到没有下划线的 listener通常代表它是一个局部变量或函数参数。避免与方法参数同名冲突在构造函数或属性赋值时省去了写 this. 的麻烦。publicclassServer{privateTcpListener_listener;// 私有字段// 没有下划线时需要写 this.listener listener;// 有下划线时可以直接写publicServer(TcpListenerlistener){_listenerlistener;}}3、自定义通信协议一、为什么需要自定义通信协议在实际工程中普通 TCP 协议只管发裸数据容易出现**“粘包/断句乱码”**比如发Hello和World接收端可能收到HelloWor和ld且没有数据校验。Modbus TCP 协议功能固定只适合读写 16 位整数寄存器无法高效传输复杂字符串、结构化浮点传感器或大块数据。自定义通信协议兼具防粘包断句、CRC16 防乱码校验、消息自增流水号、结构化浮点传感器等能力是商业设备和硬件通信的标准选择。二、 协议数据帧全景结构图我们协议的数据包在网线上传输时由8 大核心字段紧密排列组成字节位置: [ 0..1 ] [ 2 ] [ 3..4 ] [ 5 ] [ 6..7 ] [ 8 .. 8N ] [ 8N1..8N2 ] [ 8N3..8N4 ] 字段名称: [ 帧头 ] [版本] [消息流水号] [命令字] [数据长度] [ 结构化 Payload ] [ CRC-16 校验码 ] [ 帧尾 ] 对应大小: 2字节 1字节 2字节 1字节 2字节 N 字节 (变长) 2 字节 2 字节 示例数据: AA 55 0x10 0x00 0x01 0x03 0x00 0x0E [ 14字节传感器结构 ] 0x4A 0x2B 0x0D 0x0A帧头 (0xAA 0x55, 2 字节)协议暗号。接收端检测到0xAA 0x55才开始拆包过滤无效杂波。版本号 (0x10, 1 字节)代表 V1.0 协议方便未来软件版本升级兼容。消息流水号SeqID(2 字节)每发送一个数据包序号自动加 11, 2, 3...用于防止丢包、重放攻击与并发匹配。命令字Cmd(1 字节)代表本包数据的功能类型。数据长度 (2 字节)大端序记录后方数据内容 Payload 的实际字节数。数据内容Payload(N 字节)变长数据区域文本、心跳或结构化传感器。CRC-16 校验码 (2 字节)标准的CRC16-Modbus查表法冗余校验拦截网络干扰造成的乱码。帧尾 (0x0D 0x0A, 2 字节)固定换行符\r\n保证整帧数据闭环。三、 支持的 4 大命令字指令集命令字 (Hex)命令名称功能描述传输载荷 (Payload) 示例0x01心跳包 (PING)客户端定期检测服务端是否在线PING0x02文本消息包发送任意长度的 UTF-8 变长文本Hello Protocol0x03高精度传感器包传输结构化多传感器浮点数据设备ID (4B) 温度 Float (4B) 湿度 Float (4B) 电压 UInt16 (2B)0x80ACK 应答包服务端处理成功后的被动确认回复包含原请求SeqID的回复确认字