Socket-TCP 上位机下位机数据交互框架

简介: Socket-TCP 上位机下位机数据交互框架

Socket-TCP 上位机 ↔ 下位机数据交互框架(C# WinForm 版本)
框架特点:

  • 上位机既做“客户端”又可秒切“TCP服务器”,适应不同场景
  • 支持 01 线圈 / 03 保持寄存器 / 04 输入寄存器 的任意地址位配置
  • 实时日志、自动重连、断线检测、CRC16 校验
  • 线程安全:收发线程与 UI 线程解耦,不卡界面
  • 协议扩展:预留“私有协议”接口,可快速改为 Modbus-TCP、自定义帧格式

1. 工程准备

依赖 NuGet 指令
日志 Install-Package NLog
CRC16 Install-Package Crc16

2. 核心代码目录

/FrmMain.cs        // UI
/TcpLink.cs        // 连接管理
/ProtocolCodec.cs  // 协议编解码
/DataStore.cs      // 地址-数据映射(01 03 04)

3. 代码

3.1 连接管理(TcpLink.cs)

public class TcpLink : IDisposable
{
   
    private TcpClient _tcp;
    private NetworkStream _ns;
    private readonly CancellationTokenSource _cts = new();
    public event Action<byte[]> OnReceived;
    public bool IsConnected => _tcp?.Connected == true;

    // 作为客户端连接
    public async Task ConnectAsync(string ip, int port)
    {
   
        _tcp = new TcpClient();
        await _tcp.ConnectAsync(ip, port);
        _ns = _tcp.GetStream();
        _ = Task.Run(() => ReceiveLoop(_cts.Token));
    }

    // 作为服务器监听
    public async Task StartServerAsync(int port)
    {
   
        var listener = new TcpListener(IPAddress.Any, port);
        listener.Start();
        var client = await listener.AcceptTcpClientAsync();
        _tcp = client; _ns = client.GetStream();
        _ = Task.Run(() => ReceiveLoop(_cts.Token));
    }

    public async Task SendAsync(byte[] data)
    {
   
        if (!IsConnected) return;
        await _ns.WriteAsync(data, 0, data.Length);
    }

    private async Task ReceiveLoop(CancellationToken ct)
    {
   
        var buf = new byte[1024];
        while (!ct.IsCancellationRequested)
        {
   
            var len = await _ns.ReadAsync(buf, 0, buf.Length, ct);
            if (len == 0) break; // 断线
            OnReceived?.Invoke(buf.AsSpan(0, len).ToArray());
        }
    }

    public void Dispose()
    {
   
        _cts.Cancel();
        _ns?.Dispose();
        _tcp?.Close();
    }
}

3.2 地址-数据映射(DataStore.cs)

public class DataStore
{
   
    private readonly bool[] _coils = new bool[1000];          // 01
    private readonly ushort[] _holdingRegs = new ushort[1000]; // 03
    private readonly ushort[] _inputRegs = new ushort[1000];   // 04

    public bool[] Coils => _coils;
    public ushort[] HoldingRegs => _holdingRegs;
    public ushort[] InputRegs => _inputRegs;

    // 示例:设置单个线圈
    public void SetCoil(ushort addr, bool value) => _coils[addr] = value;
    public bool GetCoil(ushort addr) => _coils[addr];

    // 示例:设置保持寄存器
    public void SetHolding(ushort addr, ushort value) => _holdingRegs[addr] = value;
    public ushort GetHolding(ushort addr) => _holdingRegs[addr];
}

3.3 Modbus-TCP 协议编解码(ProtocolCodec.cs)

public static class ProtocolCodec
{
   
    public static byte[] BuildReadHolding(ushort addr, ushort qty, byte slaveId)
    {
   
        var ms = new MemoryStream();
        var bw = new BinaryWriter(ms);
        bw.Write((ushort)0x0000);      // 事务标识
        bw.Write((ushort)0x0000);      // 协议标识
        bw.Write((ushort)0x0006);      // 长度
        bw.Write(slaveId);
        bw.Write((byte)0x03);          // 功能码 03
        bw.Write(addr);
        bw.Write(qty);
        return ms.ToArray();
    }

    public static bool TryDecode(byte[] raw, out byte fn, out ushort addr, out ushort[] values)
    {
   
        if (raw.Length < 8) {
    fn = 0; addr = 0; values = null; return false; }
        fn = raw[7];
        addr = (ushort)(raw[8] << 8 | raw[9]);
        var qty = (ushort)(raw[10] << 8 | raw[11]);
        values = new ushort[qty];
        var bytes = raw.AsSpan(9 + 2);
        for (int i = 0; i < qty; i++)
            values[i] = (ushort)(bytes[i * 2] << 8 | bytes[i * 2 + 1]);
        return true;
    }
}

3.4 WinForm 调用(FrmMain.cs)

public partial class FrmMain : Form
{
   
    private readonly TcpLink _link = new();
    private readonly DataStore _store = new();

    public FrmMain()
    {
   
        InitializeComponent();
        _link.OnReceived += bytes =>
        {
   
            if (ProtocolCodec.TryDecode(bytes, out var fn, out var addr, out var vals))
            {
   
                // 更新 UI
                this.Invoke(() => UpdateUI(fn, addr, vals));
            }
        };
    }

    private async void btnConnect_Click(object sender, EventArgs e)
    {
   
        await _link.ConnectAsync(txtIP.Text, int.Parse(txtPort.Text));
        lblStatus.Text = "已连接";
    }

    private async void btnRead_Click(object sender, EventArgs e)
    {
   
        ushort addr = ushort.Parse(txtAddr.Text);
        ushort qty = 1;
        var frame = ProtocolCodec.BuildReadHolding(addr, qty, 1);
        await _link.SendAsync(frame);
    }

    private void UpdateUI(byte fn, ushort addr, ushort[] vals)
    {
   
        if (fn == 0x03) txtValue.Text = vals[0].ToString();
    }
}

4. 运行步骤

  1. 下位机(如STM32)作为 TCP服务器 监听 502 端口,实现 Modbus-TCP 协议。
  2. 上位机输入服务器 IP → 连接 → 点击“读保持寄存器”即可实时获取数据。
  3. 如需私有协议,只需替换 ProtocolCodec 即可;所有通信逻辑复用。

参考代码 soket、tcp上位机与下位机数据交互框架 www.youwenfan.com/contentald/112324.html

5. 可扩展功能

  • 心跳包:定时发送,断线自动重连
  • CRC16 校验:在 ProtocolCodec 中加入 Crc16.ComputeChecksum()
  • 日志:使用 NLog 记录收发帧
  • 多线程:UI 线程与通信线程完全隔离,避免卡顿
相关文章
|
4月前
|
人工智能 算法 NoSQL
LRU算法的Java实现
LRU(Least Recently Used)算法用于淘汰最近最少使用的数据,常应用于内存管理策略中。在Redis中,通过`maxmemory-policy`配置实现不同淘汰策略,如`allkeys-lru`和`volatile-lru`等,采用采样方式近似LRU以优化性能。Java中可通过`LinkedHashMap`轻松实现LRUCache,利用其`accessOrder`特性和`removeEldestEntry`方法完成缓存淘汰逻辑,代码简洁高效。
182 0
|
3月前
|
XML 人工智能 Java
注入Java Bean的方式
本文总结了 Spring Boot 中常见的 Bean 注入方式,包括字段注入(`@Autowired`)、构造器注入(推荐)、Setter 方法注入、`@Resource` 按名称注入、`@Bean` 定义复杂 Bean、`@Value` 注入配置值、XML 配置、`ApplicationContextAware` 手动获取 Bean 以及 JSR-330 的 `@Inject`。同时分析了 Setter 注入逐渐被淡化的原因,强调构造器注入的不可变性和安全性优势,并推荐结合 Lombok 简化代码。文章还提供了按需选择注解的建议和最佳实践,帮助开发者根据场景选择合适的依赖注入方式。
189 49
|
2月前
|
存储 数据管理 Linux
区分Linux中.tar文件与.tar.gz文件的不同。
总之,".tar"文件提供了一种方便的文件整理方式,其归档但不压缩的特点适用于快速打包和解压,而".tar.gz"文件通过额外的压缩步骤,尽管处理时间更长,但可以减小文件尺寸,更适合于需要节约存储空间或进行文件传输的场景。用户在选择时应根据具体需求,考虑两种格式各自的优劣。
375 13
|
2月前
|
编解码 算法 5G
MIMO雷达空间谱估计中Capon算法与MUSIC算法的对比分析及实现
MIMO雷达空间谱估计中Capon算法与MUSIC算法的对比分析及实现
142 2
|
1月前
|
算法 数据处理 定位技术
基于TDOA算法的三维定位
基于TDOA算法的三维定位
163 0
|
4月前
|
Linux
使用CentOS格式化硬盘或U盘为NTFS格式的方法
以上就是整个过程,如同拆解一个复杂的机器,然后按照自己的需要重新装配,你把数据的世界塑造成你需要的样子。你不只是一个简单的用户,你是一个创造者。
452 70
|
4月前
|
存储 项目管理 开发工具
Git常用命令及操作技巧
以上是Git的常用命令及操作技巧,尽管看起来有些繁琐,但实际上只要花费一些时间进行实践,您将很快熟练掌握。随着使用熟练度的提高,您会发现Git对项目管理和协同工作的强大帮助。
127 20
|
网络协议 C语言
C语言 网络编程(十一)TCP通信创建流程---服务端
在服务器流程中,新增了绑定IP地址与端口号、建立监听队列及接受连接并创建新文件描述符等步骤。`bind`函数用于绑定IP地址与端口,`listen`函数建立监听队列并设置监听状态,`accept`函数则接受连接请求并创建新的文件描述符用于数据传输。套接字状态包括关闭(CLOSED)、同步发送(SYN-SENT)、同步接收(SYN-RECEIVE)和已建立连接(ESTABLISHED)。示例代码展示了TCP服务端程序如何初始化socket、绑定地址、监听连接请求以及接收和发送数据。
|
5月前
|
SQL 测试技术
除了postman还有什么接口测试工具
最好还是使用国内的接口测试软件,其实国内替换postman的软件有很多,这里我推荐使用yunedit-post这款接口测试工具来代替postman,因为它除了接口测试功能外,在动态参数的支持、后置处理执行sql语句等支持方面做得比较好。而且还有接口分享功能,可以生成接口文档给团队在线浏览。
227 2
|
11月前
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【10月更文挑战第10天】网络协议定义了机器间通信的标准格式,确保信息准确无损地传输。主要分为两种模型:OSI七层模型与TCP/IP模型。