【.NET6+Modbus】Modbus TCP协议解析、仿真环境以及基于.NET实现基础通信

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 随着工业化的发展,目前越来越多的开发,从互联网走向传统行业。其中,工业领域也是其中之一,包括各大厂也都在陆陆续续加入工业4.0的进程当中。

 

前言:随着工业化的发展,目前越来越多的开发,从互联网走向传统行业。其中,工业领域也是其中之一,包括各大厂也都在陆陆续续加入工业4.0的进程当中。


工业领域,最核心的基础设施,应该是与下位硬件设备或程序进行通信有关的了,而下位机市场基本上是PLC的天下。而PLC产品就像编程语言一样,类型繁多,协议也多种多样。例如,西门子PLC最常用的S7协议、施耐德PLC最常用的Modbus协议、以及标准工业通信协议CIP协议等等。而多种通信协议里面,基于以太网通信的居多。以太网通信的里面,通用协议除了CIP协议,就属于Modbus TCP协议了。

接下来的内容,我会以从头开发一个简单的基于modbus tcp通信的案例,来实现一个基础的通信功能。

 

有关环境:

开发环境: VS 2022企业版

运行环境: Win 10 专业版

.NET 环境版本: .NET 6

 

【备注】 源码在文末

 

1、新建一个基于.NET 6带控制器的webapi项目,以及一个类库项目。如下图所示,新建以后的项目目录结构。

1995789-20220409102247470-105032793.png


2、由于modbus tcp通信实际上就是一个socket通信,所以在类库项目下,先创建了一个Modbus服务类,并且提供一个基于socket通信连接的方法。socket连接以后,需要返回socket实例拿来使用。

1995789-20220409102300442-1169750593.png

 

3、为了方便一点,再新增一个通用的返回信息类,用于存储一些返回信息使用。

 1995789-20220409102337799-701808259.png

 

4、基于以上的返回信息类,咱对连接方法进行稍微改造一下,让它看起来更方便一点。这样可以用来验证连接是否正常,以及返回对应的异常信息,好做进一步处理。

 1995789-20220409102346323-1975057986.png

 

5、Modbus TCP请求的报文规则,一些解析信息如下:

站地址:默认0x01, 除非PLC告诉我们其他站地址。

功能码:代表读写数据时候指定的读写方法等。例如读取线圈的功能码是0x01。


地址和读取长度:地址目前个人在施耐德物理的PLC环境上,不能超过30000。同时,单次读写长度不能超过248个byte,否则PLC可能会飘。当然,也可能将来一些PLC可以支持更长的批量数据读写,目前在施耐德PLC环境下不支持(具体型号忘记了,有点久了,当前身边没得PLC了,等下会使用仿真工具来做环境)。

头部校验(消息唯一识别码):0~65535,用于PLC服务端进行区分不同的客户端而使用的一组数据标识,不同的客户端必须保证标识码不重合。例如多个客户端同时存在时候,发起的通信请求,必须保持不一样的识别码,否则Modbus服务端有可能会因为不知是哪个客户端发起的请求而导致信息乱了。

无(协议标识):默认0,代表是Modbus协议。

数据长度:发送的报文的长度,刚好是6位,所以可以写成固定值0x06。(写入的规则不一样,此处固定值只当作读取时候使用)

 

 

6、根据协议的一些具体内容,写一个存储功能码和异常返回码的数据类,用于后期做通信时候传参和通信数据验证使用。有关协议具体内容,如下代码所示。

1995789-20220409102428692-2081936433.png

 

7、由于异常码是byte数据,直接验证可能会麻烦一点,为了可以直观一些,此处再新增一个用于解析Modbus返回的异常信息的方法,用于备用。

1995789-20220409102438449-941967295.png

 

8、根据协议规则,提供一些参数,并先搭建一个简单的方法框架,用来可以进行读取线圈的功能。包含简单的报文数据拆分以及报文发送和接收。由于发送报文长度不能超过248byte(1 bool大小 == 1 byte,如果是其他类型,需要做其他长度换算),所以当长度超过时候,做个简单的算法进行拆分再发送,防止发生不必要的异常。以下做一个读取线圈(Bool类型数据)的简单方法。

 1995789-20220409102520566-2102397816.png

 

9、根据上方提供的协议报文组装规则,进行开发一个通用的报文组织方法。有高低位之分,所以对于占用2byte的数据,需要进行"倒装"。

 1995789-20220409102534334-1825920685.png

 

10、发送报文以后,返回的报文含有校验信息:发送的数据报文的第7位的数据,加上 0x80 以后,跟返回的报文的第7位byte数据如果一致,则代表当前通信上可能有异常。异常码在接收的响应报文的第8位。

所以可以继续写一个验证是否成功的校验方法:

1995789-20220409102543123-1186243455.png

 

11、由于返回的数据也都是byte数据,以上读取的线圈值(布尔值),就需要提供一个数据类型转换的功能用于把byte数组转换为bool数组。

1995789-20220409102552083-201805246.png

 

12、对读取线圈的最开始的方法,进行一些完善以后的代码如下。响应报文长度是 发送数据长度*2+9 。

1995789-20220409102601391-1273380654.png


13、接下来做一个简单的测试。准备一下仿真环境,进行本地的测试,看看是否可以连通。先准备两个工具,一个是 modbus poll,另一个是modbus slave。一个用来模拟服务端环境,另一个可用来模拟数据收发验证。


备注】:由于网上存在很多爬虫爬取博客文章到各个地方的,所以如果有需要这俩工具的小伙伴,可以点击该文章的 原文链接:【https://www.cnblogs.com/weskynet/p/16121383.html】的最下方的QQ群号进行加群进行获取,或者在文章最后提供的个人微信号,加我个人微信私发也可以。

1995789-20220409102633588-839628730.png

 

14、两边都设置为读写单个线圈的功能,用于测试以上线圈读取的代码的功能。

1995789-20220409102643857-1256904453.png


15、两边都设置为modbus tcp连接方式。Slave站点启动以后,默认为本地,poll工具上的IP地址选择本地即可。如果是真实PLC环境,则填写真实PLC地址。

 1995789-20220409102654134-1145344133.png

 

16、测试两边是否通信上。给任意一个地址写入一个true,可以看到另一边也同步更新,说明通信是通的了。

【注意】modbus工具,poll和slave工具默认占用了消息唯一标识码,大概是1~5左右的固定值,所以使用该工具期间,建议程序上的唯一消息识别码设置为5以上,以防止通信干扰。

 1995789-20220409102730837-1352594324.png

17、接下来就可以继续完善代码进行验证了。先新增ModbusService的接口IModbusService,用于实现依赖注入。然后在program.cs文件里面进行服务注册。

 

 

18、新建一个控制器,用来进行模拟实验。有关代码和注释如图所示。

 

 

19、进行读取一个长度试试效果。结果是数据不支持,说明报文有问题。

 

 

20、通过断点,找到问题所在,上面的代码里面,length经过简单算法计算以后等于0,此处需要用的应该是newLength变量的值。

 

 

21、再次测试,地址从1开始,读取两个地址,结果符合预期。

1995789-20220409102829254-132826018.png

 

22、再测试一下,从0开始读取30个,并随即设置若干个是True的值。

1995789-20220409103521105-688297732.png

 

23、其他的写入、以及其他类型读写,基本类似。由于篇幅有限,就不继续进行一步一步操作的截图了。读取的,选好类型,报文格式都是一样的,唯一有差别的是写入的报文。下面是写入单个线圈值的报文。线圈当前仅支持一个一个写入。

1995789-20220409103540076-572943093.png

 

24、写入寄存器的规则会有些偏差,协议规则如下图。

1995789-20220409103551884-1225559310.png


【备注】以上图的标题,我写错了,应该是 “写入寄存器”报文协议,懒得换图了,大佬们看的时候自己辨别哈~

 读取线圈当作引导,其他类型也都异曲同工,大佬们可以自行尝试。

 

另外说点,如果是生产环境下使用,建议把客户端连接做成【长连接】,不然重复创建连接比较耗费资源,耗时也会因为新建连接而占用一大半。同时,如果是多线程访问,使用同一个客户端连接,必须加锁,否则会干扰数据;如果是多线程,不同客户端,就要保证每个消息识别码必须不同,如果存在同一个识别码,很容易发生数据异常等情况。

 

有关源码:

ModbusService源码:


public class ModbusService: IModbusService
    {
        public ResultInformation<Socket> ConnectModbusTcpService( IPAddress ip, int port)
        {
            ResultInformation<Socket> client = new();
            try
            {
                client.Result = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                client.Result.Connect(new IPEndPoint(ip, port));
                client.IsSucceed = true;
            }
            catch (Exception ex)
            {
                client.IsSucceed=false;
                client.Message = ex.Message;
            }
            return client;
        }
        /// <summary>
        /// 读取线圈值(Bool)
        /// </summary>
        /// <param name="client">客户端</param>
        /// <param name="headCode">头部标识</param>
        /// <param name="station">站地址</param>
        /// <param name="address">地址</param>
        /// <param name="length">长度</param>
        /// <returns></returns>
        public ResultInformation<bool[]> ReadCoils(Socket client,ushort headCode,byte station, ushort address, ushort length)
        {
            ResultInformation<bool[]> result = new();
            int resultIndex = 0;
            ushort newLength = 0;
            ushort realLength = length;  // 存储实际长度
            try
            {
                List<byte> byteResult = new List<byte>(); // 存储实际读取到的所有有效的byte数据
                while (length > 0)
                {
                    if (length > 248)  // 长度限制,不能超过248
                    {
                        length = (ushort)(length - 248);
                        newLength = 248;
                    }
                    else
                    {
                        newLength = length;
                        length = 0;
                    }
                    resultIndex += newLength;
                    byte[] sendBuffers = BindByteData(headCode,station,FunctionCode.ReadCoil,address,newLength); // 组装报文
                    client.Send(sendBuffers);
                    byte[] receiveBuffers = new byte[newLength * 2 + 9]; 
                    int count = client.Receive(receiveBuffers); // 等待接收报文
                    var checkResult = CheckReceiveBuffer(sendBuffers, receiveBuffers); // 验证消息发送成功与否
                    if (checkResult.IsSucceed)
                    {
                        // 成功,如果长度超出单次读取长度,进行继续读取,然后对数据进行拼接
                        List<byte> byteList = new List<byte>(receiveBuffers);
                        byteList.RemoveRange(0, 9); // 去除前面9个非数据位
                        byteResult.AddRange(byteList); // 读取到的数据进行添加进集合
                        address += newLength; // 下一个起始地址
                    }
                    else
                    {
                        throw new Exception(checkResult.Message);
                    }
                }
                result.IsSucceed = true;
                result.Result = ByteToBoolean(byteResult.ToArray(), realLength);
            }
            catch (Exception ex)
            {
                result.IsSucceed = false;
                result.Result = new bool[0];
                result.Message = ex.Message;
            }
            return result;
        }
        private bool[] ByteToBoolean(byte[] data,int length)
        {
            if (data == null)
            {
                return new bool[0];
            }
            if (length > data.Length * 8) length = data.Length * 8;
            bool[] result = new bool[length];
            for (int i = 0; i < length; i++)
            {
                int index = i / 8;
                int offect = i % 8;
                byte temp = 0;
                switch (offect)
                {
                    case 0: temp = 0x01; break;
                    case 1: temp = 0x02; break;
                    case 2: temp = 0x04; break;
                    case 3: temp = 0x08; break;
                    case 4: temp = 0x10; break;
                    case 5: temp = 0x20; break;
                    case 6: temp = 0x40; break;
                    case 7: temp = 0x80; break;
                    default: break;
                }
                if ((data[index] & temp) == temp)
                {
                    result[i] = true;
                }
            }
            return result;
        }
        private byte[] BindByteData(ushort headCode,byte station,byte functionCode,ushort address, ushort length)
        {
            byte[] head = new byte[6];
            head[0] = station; // 站地址
            head[1] = functionCode; // 功能码
            head[2] = BitConverter.GetBytes(address)[1]; // 起始地址
            head[3] = BitConverter.GetBytes(address)[0];
            head[4] = BitConverter.GetBytes(length)[1]; // 长度
            head[5] = BitConverter.GetBytes(length)[0];
            return GetSocketBytes(headCode,head);
        }
        private byte[] GetSocketBytes(ushort headCode,byte[] head)
        {
            byte[] buffers = new byte[head.Length+6]; 
            buffers[0] = BitConverter.GetBytes(headCode)[1];
            buffers[1] = BitConverter.GetBytes(headCode)[0];
            // 2 和 3位置默认,所以不需要赋值
            buffers[4] = BitConverter.GetBytes(head.Length)[1];
            buffers[5] = BitConverter.GetBytes(head.Length)[0];
            head.CopyTo(buffers, 6);
            return buffers;
        }
        private ResultInformation<string> CheckReceiveBuffer(byte[] send,byte[] receive)
        {
            ResultInformation<string> result = new();
            if ((send[7] + 0x80) == receive[7])
            {
                var str = FunctionCode.GetDescriptionByErrorCode(receive[8]);
                result.IsSucceed = false;
                result.Message = str;
            }
            else
            {
                result.IsSucceed = true;
            }
            return result;
        }
    }


控制器源码:


[Route("api/[controller]/[action]")]
    [ApiController]
    public class TestModbusController : ControllerBase
    {
        IModbusService _service;
        public TestModbusController(IModbusService modbusService)
        {
            _service = modbusService;
        }
        [HttpPost]
        public IActionResult ReadCoil(ushort address, ushort length)
        {
            var ip = IPAddress.Parse("127.0.0.1"); // ip地址
             int port = 502; // modbus tcp通信,默认端口
            byte station = (byte)((short)1); // 站地址为1
            var connectResult = _service.ConnectModbusTcpService(ip,port);
            if (connectResult.IsSucceed)
            {
                // socket连接创建成功
                var readResult = _service.ReadCoils(connectResult.Result,6,station,address,length);  // 唯一消息码设为6(大于5,且不重复即可)
                if (readResult.IsSucceed)
                {
                    if (readResult.Result.Any())
                    {
                        StringBuilder sb = new StringBuilder();
                        for(int i = 0; i < readResult.Result.Length; i++)
                        {
                            sb.AppendLine($"[{i}]:{readResult.Result[i]}");
                        }
                        return Ok(sb.ToString());
                    }
                }
                else
                {
                    return Ok(readResult.Message);
                }
            }
            else
            {
                return Ok(connectResult.Message);
            }
            return Ok();
        }
    }


功能码和异常码:


public class FunctionCode
    {
        #region 功能码
        public const byte ReadCoil = 0x01; // 读取线圈状态  寄存器PLC地址 00001 - 09999
        public const byte ReadInputDiscrete = 0x02; // 读取 可输入的离散量  寄存器PLC地址 10001 - 19999
        public const byte ReadRegister = 0x03; // 读取 保持寄存器  40001 - 49999
        public const byte ReadInputRegister = 0x04; // 读取 可输入寄存器  30001 - 39999
        public const byte WriteSingleCoil = 0x05; // 写单个 线圈  00001 - 09999
        public const byte WriteSingleRegister = 0x06; // 写单个 保持寄存器  40001 - 49999
        public const byte WriteMultiCoil = 0x0F;  // 写多个 线圈  00001 - 09999
        public const byte WriteMultiRegister = 0x10; // 写多个 保持寄存器  40001 - 49999
        public const byte SelectSlave = 0x11; //  查询从站状态信息  (串口通信使用)
        #endregion
        #region 异常码
        public const byte FunctionCodeNotSupport = 0x01;// 非法功能码
        public const byte DataAddressNotSupport = 0x02;// 非法数据地址
        public const byte DataValueNotSupport = 0x03;// 非法数据值
        public const byte DeviceNotWork = 0x04;// 从站设备异常
        public const byte LongTimeResponse = 0x05;// 请求需要更长时间才能进行处理请求
        public const byte DeviceBusy = 0x06;// 设备繁忙
        public const byte OddEvenError = 0x08;// 奇偶性错误
        public const byte GatewayNotSupport = 0x0A;// 网关错误
        public const byte GatewayDeviceResponseTimeout = 0x0B;// 网关设备响应失败
        #endregion
        public static string GetDescriptionByErrorCode(byte code)
        {
            switch (code)
            {
                case FunctionCodeNotSupport:
                    return "FunctionCodeNotSupport";
                case DataAddressNotSupport:
                    return "DataAddressNotSupport";
                case DataValueNotSupport:
                    return "DataValueNotSupport";
                case DeviceNotWork:
                    return "DeviceNotWork";
                case LongTimeResponse:
                    return "LongTimeResponse";
                case DeviceBusy:
                    return "DeviceBusy";
                case OddEvenError:
                    return "OddEvenError";
                case GatewayNotSupport:
                    return "GatewayNotSupport";
                case GatewayDeviceResponseTimeout:
                    return "GatewayDeviceResponseTimeout";
                default:
                    return "UnknownError";
            }
        }
    }


好了,以上就是该文章的全部内容。如果觉得有帮助,欢迎一键三连啊~~ 如果有兴趣,也可以加我私人微信,欢迎大佬来我微信群做客。私人微信:【WeskyNet001】


目录
相关文章
|
1月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
100 3
|
1月前
使用的是.NET Framework 4.0,并且需要使用SMTP协议发送电子邮件
使用的是.NET Framework 4.0,并且需要使用SMTP协议发送电子邮件
42 1
|
3月前
|
Linux C++ Windows
【Azure 应用服务】Azure App Service(Windows)环境中如何让.NET应用调用SAP NetWeaver RFC函数
【Azure 应用服务】Azure App Service(Windows)环境中如何让.NET应用调用SAP NetWeaver RFC函数
【Azure 应用服务】Azure App Service(Windows)环境中如何让.NET应用调用SAP NetWeaver RFC函数
|
16天前
|
消息中间件 Linux iOS开发
.NET 高性能异步套接字库,支持多协议、跨平台、高并发
【11月更文挑战第3天】本文介绍了高性能异步套接字库在网络编程中的重要性,特别是在处理大量并发连接的应用中。重点讨论了 .NET 中的 Socket.IO 和 SuperSocket 两个库,它们分别在多协议支持、跨平台特性和高并发处理方面表现出色。Socket.IO 基于 WebSocket 协议,支持多种通信协议和跨平台运行,适用于实时通信应用。SuperSocket 则通过事件驱动的异步编程模型,实现了高效的高并发处理,适用于需要自定义协议的场景。这些库各有特点,可根据具体需求选择合适的库。
|
16天前
|
域名解析 缓存 网络协议
浏览器中输入URL返回页面过程(超级详细)、DNS域名解析服务,TCP三次握手、四次挥手
浏览器中输入URL返回页面过程(超级详细)、DNS域名解析服务,TCP三次握手、四次挥手
|
20天前
|
安全 算法 编译器
.NET 9 AOT的突破 - 支持老旧Win7与XP环境
【10月更文挑战第30天】在.NET 9 中,AOT(Ahead-of-Time)编译技术在支持老旧的 Windows 7 和 XP 系统方面取得了显著进展。主要突破包括:性能提升(启动速度加快、执行效率提高)、部署优化(无需安装.NET 运行时、减小应用程序体积)、兼容性保障(编译策略优化、依赖项管理改进)以及安全性增强(代码保护机制)。这些改进使得应用程序在老旧系统上运行更加流畅、高效和安全。
|
20天前
|
XML 安全 API
.NET 9 AOT的突破 - 支持老旧Win7与XP环境
.NET 9开始,AOT支持Win7和XP,不仅仅只支持SP1版本
.NET 9 AOT的突破 - 支持老旧Win7与XP环境
|
27天前
|
测试技术 API 开发者
精通.NET单元测试:MSTest、xUnit、NUnit全面解析
【10月更文挑战第15天】本文介绍了.NET生态系统中最流行的三种单元测试框架:MSTest、xUnit和NUnit。通过示例代码展示了每种框架的基本用法和特点,帮助开发者根据项目需求和个人偏好选择合适的测试工具。
37 3
|
1月前
|
SQL 开发框架 .NET
ASP.NET连接SQL数据库:实现过程与关键细节解析an3.021-6232.com
随着互联网技术的快速发展,ASP.NET作为一种广泛使用的服务器端开发技术,其与数据库的交互操作成为了应用开发中的重要环节。本文将详细介绍在ASP.NET中如何连接SQL数据库,包括连接的基本概念、实现步骤、关键代码示例以及常见问题的解决方案。由于篇幅限制,本文不能保证达到完整的2000字,但会确保
|
2月前
|
监控 网络协议 API
.NET WebSocket 技术深入解析,你学会了吗?
【9月更文挑战第4天】WebSocket 作为一种全双工协议,凭借低延迟和高性能特点,成为实时应用的首选技术。.NET 框架提供了强大的 WebSocket 支持,使实时通信变得简单。本文介绍 WebSocket 的基本概念、.NET 中的使用方法及编程模型,并探讨其在实时聊天、监控、在线游戏和协同编辑等场景的应用,同时分享最佳实践,帮助开发者构建高效实时应用。
139 12

推荐镜像

更多