• IoTClient开发4


    前言

    上篇我们实现了ModBusTcp协议的客户端读写,可是在很多时候编写业务代码之前是没有现场环境的。总不能在客户现场去写代码,或是蒙着眼睛写然后求神拜佛不出错,又或是在办公室部署一套硬件环境。怎么说都感觉不太合适,如果我们能用软件仿真模拟硬件那不就完美了,以后有各种不同的硬件协议接口都模拟出来,而不是每个硬件都买一套回来部署了做测试。
    真要用软件仿真模拟也是可以的,客户端是对协议的请求报文发送和响应报文的解析,服务端其实就是请求报文的接收和响应报文的发送,正好和客户端的动作相反。
    前面我们在写你也可以写个聊天程序 - C# Socket学习1的时候就有写Socket服务端实现,其实这个也差不了多少。

    ModBusTcp报文分析(上篇拷贝过来的,方便下面代码实现的时候做对比)

    协议的理解和实现主要就是要对协议报文理解。(注意:以下报文数据都是十六进制)

    数据【读取-请求报文】:19 B2 00 00 00 06 02 03 00 04 00 01

    • 19 B2 是客户端发的检验信息,随意定义。
    • 00 00 代表是基于tcp/ip协议的modbus
    • 00 06 标识后面还有多长的字节
    • 02 表示站号地址
    • 03 为功能码(读保持寄存器)
    • 00 04 为寄存器地址
    • 00 01 为寄存器的长度(寄存器个数)

    数据【读取-响应报文】(分两次获取)

    第一次获取前八个字节(Map报文头):19 B2 00 00 00 05 02 03 02 00 20

    • 19 B2 检验验证信息(复制的客户端发的,配件检验)
    • 00 00 代表是基于tcp/ip协议的modbus(复制的客户端发的)
    • 00 05 为当前位置到最后的长度
    • 02 表示站号地址(复制的客户端发的)
    • 03 为功能码(复制的客户端发的)

    第二次获取的报文:02 00 20

    • 02 字节个数
    • 00 20 响应的数据

    数据【写入-请求报文】:19 B2 00 00 00 09 02 10 00 04 00 01 02 00 20

    • 19 B2 是客户端发的检验信息,随意定义。
    • 00 00 代表是基于tcp/ip协议的modbus
    • 00 09 从本字节下一个到最后
    • 02 站号
    • 10 功能码(转十进制就是16)
    • 00 04 寄存器地址
    • 00 01 寄存器的长度(寄存器个数)
    • 02 写字节的个数
    • 00 20 要写入的值(转十进制为32)

    数据【写入-响应报文】:19 B2 00 00 00 06 02 10 00 04 00 01

    和请求报文的区别

    • 没有了请求报文的数据值
    • 00 09 变成了00 06 因为报文长度变了
    • 其他的报文意义和请求报文一致

    实现

    //启动服务
    public void Start()
    {
        //1 创建Socket对象
        var socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    
        //2 绑定ip和端口 
        IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, 502);
        socketServer.Bind(ipEndPoint);
    
        //3、开启侦听(等待客户机发出的连接),并设置最大客户端连接数为10
        socketServer.Listen(10);
    
        Task.Run(() => { Accept(socketServer); });
    }
    
    //客户端连接到服务端
    void Accept(Socket socket)
    {
        while (true)
        {
            //阻塞等待客户端连接
            Socket newSocket = socket.Accept();
            Task.Run(() => { Receive(newSocket); });
        }
    }
    

    以上都和我们前面的一样,这了不一样的地方就是对请求报文的解析和响应报文的组装发送

    //接收客户端发送的消息
    void Receive(Socket newSocket)
    {
        while (newSocket.Connected)
        {
            byte[] requetData1 = new byte[8];
            //读取客户端发送报文 报文头
            int readLeng = newSocket.Receive(requetData1, 0, requetData1.Length, SocketFlags.None);
            byte[] requetData2 = new byte[requetData1[5] - 2];
            //读取客户端发送报文 报文数据
            readLeng = newSocket.Receive(requetData2, 0, requetData2.Length, SocketFlags.None);
            var requetData = requetData1.Concat(requetData2).ToArray();
    
            byte[] responseData1 = new byte[8];
            //复制请求报文中的报文头
            Buffer.BlockCopy(requetData, 0, responseData1, 0, responseData1.Length);
            //这里可以自己实现一个对象,用来存储客户端写入的数据(也可以用redis等实现数据的持久化)
            DataPersist data = new DataPersist("");
    
            //根据协议,报文的第八个字节是功能码(前面我们有说过 03:读保持寄存器  16:写多个寄存器)
            switch (requetData[7])
            {
                //读保持寄存器
                case 3:
                    {
                        var value = data.Read(requetData[9]);
                        short.TryParse(value, out short resultValue);
                        var bytes = BitConverter.GetBytes(resultValue);
                        //当前位置到最后的长度
                        responseData1[5] = (byte)(3 + bytes.Length);
                        byte[] responseData2 = new byte[3] { (byte)bytes.Length, bytes[1], bytes[0] };
                        var responseData = responseData1.Concat(responseData2).ToArray();
                        newSocket.Send(responseData);
                    }
                    break;
                //写多个寄存器
                case 16:
                    {
                        data.Write(requetData[9], requetData[requetData.Length - 1].ToString());
                        var responseData = new byte[12];
                        Buffer.BlockCopy(requetData, 0, responseData, 0, responseData.Length);
                        responseData[5] = 6;
                        newSocket.Send(responseData);
                    }
                    break;
            }
        }
    }
    

    这段要点就是根据请求报文获得功能码,然后对报文数据进行读取或写入动作。当然你完全可以对写入的数据进行持久化存储(如用redis),这样在断电或重启后数据依然可以正常读取。
    当然,以上只是个类伪代码,为了减少代码量和方便理解,很多情况和实际可能性没有做对应的处理。

    IoTClient中ModBusTcp协议的使用

    安装

    Nuget安装 Install-Package IoTClient
    或图形化安装

    使用

    //1、实例化客户端 - 输入正确的IP和端口
    ModBusTcpClient client = new ModBusTcpClient("127.0.0.1", 502);
    
    //2、写操作 - 参数依次是:地址 、值 、站号 、功能码
    client.Write("4", (short)33, 2, 16);
    client.Write("4", (short)3344, 2, 16);
    
    //3、读操作 - 参数依次是:地址 、站号 、功能码
    var value = client.ReadInt16("4", 2, 3).Value;
    var value2 = client.ReadInt32("4", 2, 3).Value;
    
    //4、如果没有主动Open,则会每次读写操作的时候自动打开自动和关闭连接,这样会使读写效率大大减低。所以建议手动Open和Close。
    client.Open();
    
    //5、读写操作都会返回操作结果对象Result
    var result = client.ReadInt16("4", 2, 3);
    //5.1 读取是否成功(true或false)
    var isSucceed = result.IsSucceed;
    //5.2 读取失败的异常信息
    var errMsg = result.Err;
    //5.3 读取操作实际发送的请求报文
    var requst  = result.Requst;
    //5.4 读取操作服务端响应的报文
    var response = result.Response;
    //5.5 读取到的值
    var value3 = result.Value;
    

    结束

  • 相关阅读:
    JS模拟出 getElementsByClassName 功能
    如何为PDF文件添加书签
    Linux内核模块学习
    Linux字符设备驱动学习
    第53篇编译线程的初始化
    第51篇SharedRuntime::generate_native_wrapper()生成编译入口
    第50篇调用约定(2)
    第52篇即时编译器
    2021 阿里云容器服务年度盘点:企业级容器应用变化和技术趋势观察
    如何在零停机的情况下迁移 Kubernetes 集群
  • 原文地址:https://www.cnblogs.com/zhaopei/p/11839515.html
Copyright © 2020-2023  润新知