|
| 1 | +@page "/modbus-factory" |
| 2 | +@inject IStringLocalizer<ModbusFactories> Localizer |
| 3 | + |
| 4 | +<h3>Modbus 串行通讯服务 <code>IModbusFactory</code></h3> |
| 5 | +<h4>组件库内置了 Modbus 串行通讯服务</h4> |
| 6 | + |
| 7 | +<PackageTips Name="Longbow.Modbus" /> |
| 8 | + |
| 9 | +<p class="code-label">1. 服务注入</p> |
| 10 | + |
| 11 | +<Pre>services.AddModbusFactory();</Pre> |
| 12 | + |
| 13 | +<p class="code-label">2. 使用服务</p> |
| 14 | +<p>调用 <code>ModbusFactory</code> 实例方法 <code>GetOrCreateTcpMaster</code> 即可得到一个 <code>IModbusClient</code> 实例。内部提供复用机制,调用两次得到的 <code>IModbusClient</code> 为同一对象</p> |
| 15 | + |
| 16 | +<Pre>[Inject] |
| 17 | +[NotNull] |
| 18 | +private IModbusFactory? ModbusFactory { get; set; }</Pre> |
| 19 | + |
| 20 | +<Pre>var client = ModbusFactory.GetOrCreateTcpMaster("bb", options => |
| 21 | +{ |
| 22 | + options.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); |
| 23 | +});</Pre> |
| 24 | + |
| 25 | +<p class="code-label">3. 使用方法</p> |
| 26 | + |
| 27 | +<ul class="ul-demo"> |
| 28 | + <li>通过 <code>ITcpSocketClient</code> 实例方法 <code>ConnectAsync</code> 连接远端节点</li> |
| 29 | + <li>通过 <code>ITcpSocketClient</code> 实例方法 <code>SendAsync</code> 发送协议数据</li> |
| 30 | + <li>通过 <code>ITcpSocketClient</code> 实例方法 <code>Close</code> 关闭连接</li> |
| 31 | + <li>通过 <code>ITcpSocketClient</code> 实例方法 <code>SetDataHandler</code> 方法设置数据处理器</li> |
| 32 | + <li>通过 <code>ITcpSocketClient</code> 实例属性 <code>ReceivedCallBack</code> 方法设置接收数据处理器(注意:此回调未做任何数据处理为原始数据)</li> |
| 33 | +</ul> |
| 34 | + |
| 35 | +<p class="code-label">4. 数据处理器</p> |
| 36 | + |
| 37 | +<p>在我们实际应用中,建立套接字连接后就会进行数据通信,数据通信不会是杂乱无章的随机数据,在应用中都是有双方遵守的规约简称通讯协议,在通讯协议的约束下,发送方与接收方均根据通讯协议进行编码或解码工作,将数据有条不紊的传输</p> |
| 38 | + |
| 39 | +<p>数据处理器设计初衷就是为了契合通讯协议大大简化我们开发逻辑,我们已通讯协议每次通讯电文均为 <b>4</b> 位定长举例说明,在实际的通讯过程中,我们接收到的通讯数据存在粘包或者分包的现象</p> |
| 40 | + |
| 41 | +<ul class="ul-demo"> |
| 42 | + <li><b>粘包</b>:比如我们期望收到 <b>1234</b> 四个字符,实际上我们接收到的是 <b>123412</b> 多出来的 <b>12</b> 其实是下一个数据包的内容,我们需要截取前 4 位数据作为一个数据包才能正确处理数据,这种相邻两个通讯数据包的粘连称为<b>粘包</b></li> |
| 43 | + <li><b>分包</b>:比如我们期望收到 <b>1234</b> 四个字符,实际上我们可能分两次接收到,分别是 <b>12</b> 和 <b>34</b>,我们需要将两个数据包拼接成一个才能正确的处理数据。这种情况称为<b>分包</b></li> |
| 44 | +</ul> |
| 45 | + |
| 46 | +<p>我们内置了一些常用的数据处理类 <code>IDataPackageHandler</code> 接口为数据包处理接口,虚类 <code>DataPackageHandlerBase</code> 作为数据处理器基类已经内置了 <b>粘包</b> <b>分包</b> 的逻辑,继承此类后专注自己处理的业务即可</p> |
| 47 | + |
| 48 | +<p>使用方法如下:</p> |
| 49 | + |
| 50 | +<Pre>[Inject] |
| 51 | +[NotNull] |
| 52 | +private ITcpSocketFactory? TcpSocketFactory { get; set; } |
| 53 | + |
| 54 | +private async Task CreateClient() |
| 55 | +{ |
| 56 | + // 创建 ITcpSocketClient 实例 |
| 57 | + var client = TcpSocketFactory.GetOrCreate("localhost", 0); |
| 58 | + |
| 59 | + // 设置数据适配器 使用 FixLengthDataPackageHandler 数据处理器处理数据定长 4 的数据 |
| 60 | + var adapter = new DataPackageAdapter |
| 61 | + { |
| 62 | + DataPackageHandler = new FixLengthDataPackageHandler(4) |
| 63 | + }; |
| 64 | + |
| 65 | + // 如果 client 不销毁切记使用 RemoveDataPackageAdapter 移除回调委托防止内存泄露 |
| 66 | + client.AddDataPackageAdapter(adapter, buffer => |
| 67 | + { |
| 68 | + // buffer 即是接收到的数据 |
| 69 | + return ValueTask.CompletedTask; |
| 70 | + }); |
| 71 | + |
| 72 | + // 连接远端节点 连接成功后自动开始接收数据 |
| 73 | + var connected = await client.ConnectAsync("192.168.10.100", 6688); |
| 74 | +} |
| 75 | +</Pre> |
| 76 | + |
| 77 | +<p>内置数据处理器</p> |
| 78 | + |
| 79 | +<ul class="ul-demo"> |
| 80 | + <li><code>FixLengthDataPackageHandler</code> <b>固定长度数据处理器</b> 即每个通讯包都是固定长度</li> |
| 81 | + <li><code>DelimiterDataPackageHandler</code> <b>分隔符数据处理器</b> 即通讯包以特定一个或一组字节分割</li> |
| 82 | +</ul> |
| 83 | + |
| 84 | +<p class="code-label">5. 数据适配器</p> |
| 85 | + |
| 86 | +<p>在我们实际应用中,接收到数据包后(已经过数据处理器)大多情况下是需要将电文转化为应用中的具体数据类型 <code>Class</code> 或 <code>Struct</code>。将原始数据包转化为类或者结构体的过程由我们的数据适配器来实现</p> |
| 87 | + |
| 88 | +<p>数据适配器设计思路如下</p> |
| 89 | + |
| 90 | +<ol class="ul-demo"> |
| 91 | + <li>使用 <code>DataTypeConverterAttribute</code> 标签约定通讯数据使用那个转换类型进行转换 指定类型需继承 <code>IDataConverter</code> |
| 92 | + 接口 |
| 93 | + </li> |
| 94 | + <li>使用 <code>DataPropertyConverterAttribute</code> 标签约定如何转换数据类型 (Property) 属性值</li> |
| 95 | +</ol> |
| 96 | + |
| 97 | +<Pre>[DataTypeConverter(Type = typeof(DataConverter<MockEntity>))] |
| 98 | +class MockEntity |
| 99 | +{ |
| 100 | + [DataPropertyConverter(Type = typeof(byte[]), Offset = 0, Length = 5)] |
| 101 | + public byte[]? Header { get; set; } |
| 102 | + |
| 103 | + [DataPropertyConverter(Type = typeof(byte[]), Offset = 5, Length = 2)] |
| 104 | + public byte[]? Body { get; set; } |
| 105 | + |
| 106 | + [DataPropertyConverter(Type = typeof(Foo), Offset = 7, Length = 1, ConverterType = typeof(FooConverter), ConverterParameters = ["test"])] |
| 107 | + public string? Value1 { get; set; } |
| 108 | +}</Pre> |
| 109 | + |
| 110 | +<Pre>class FooConverter(string name) : IDataPropertyConverter |
| 111 | +{ |
| 112 | + public object? Convert(ReadOnlyMemory<byte> data) |
| 113 | + { |
| 114 | + return new Foo() { Id = data.Span[0], Name = name }; |
| 115 | + } |
| 116 | +}</Pre> |
| 117 | + |
| 118 | +<p class="code-label">针对第三方程序集的数据类型解决方案如下:</p> |
| 119 | +<p>使用 <code></code></p> |
| 120 | + |
| 121 | +<Pre>builder.Services.ConfigureDataConverters(options => |
| 122 | +{ |
| 123 | + options.AddTypeConverter<MockEntity>(); |
| 124 | + options.AddPropertyConverter<MockEntity>(entity => entity.Header, new DataPropertyConverterAttribute() |
| 125 | + { |
| 126 | + Offset = 0, |
| 127 | + Length = 5 |
| 128 | + }); |
| 129 | + options.AddPropertyConverter<MockEntity>(entity => entity.Body, new DataPropertyConverterAttribute() |
| 130 | + { |
| 131 | + Offset = 5, |
| 132 | + Length = 2 |
| 133 | + }); |
| 134 | +}); |
| 135 | +</Pre> |
0 commit comments