Flutter如何用ServerSocket实现Modbus TCP服务端响应客户端读请求
Dart实现Modbus TCP服务端响应异常解决
问题背景
- 开发基于Modbus协议与PLC通信的桌面端应用时,发现pub.dev现有Modbus插件仅提供客户端实现,不支持服务端能力
- 查阅插件源码确认客户端基于
dart:io的Socket实现,因此尝试通过ServerSocket自行搭建Modbus TCP服务端 - 测试验证客户端可正常连接自建TCP服务端,服务端可接收到客户端发送的
Uint8List格式报文:- 写单个寄存器(向地址4写入值5)时收到报文:
报文字段说明:[0,1,0,0,0,6,10,6,0,4,0,5]- 第2位的
1为事务序列号 - 第8位的
6为功能码,对应write single register操作 - 第10位的
4为目标寄存器地址 - 第12位的
5为待写入值
- 读保持寄存器(读取地址4开始的5个寄存器)时收到报文:
报文字段说明:[0,1,0,0,0,6,10,3,0,4,0,5]- 第2位的
1为事务序列号 - 第8位的
3为功能码,对应read holding registers操作 - 第10位的
4为寄存器起始地址 - 第12位的
5为待读取寄存器数量
异常复现
初始服务端实现代码:
void createServer(){ ServerSocket.bind("127.0.0.1", 4000) .then((server) { server.listen((socket) { socket.listen((event) { print(event); // 尝试向客户端返回数据时触发异常 socket.write(Uint16List.view(event.buffer,0,1)); // 报错信息:Unhandled Exception: RangeError (byteOffset): Invalid value: Not in inclusive range 0..3: 4 }); }); }); }
客户端读寄存器调用代码:
Future<void>read(modbus.ModbusClient client,int address, int amount)async{ Uint16List data = await client.readHoldingRegisters(address, amount); print(data); }
核心异常表现:客户端发起readHoldingRegisters请求时,无法通过ServerSocket构造符合Modbus TCP规范的响应返回,直接调用socket.write/socket.add方法会触发RangeError异常。
原因分析
- RangeError触发原因:
Uint16List.view是基于现有缓冲区的视图构造方法,要求传入的byteOffset必须是2的整数倍,且不能超出缓冲区有效范围。socket.listen回调中的event是Uint8List类型,它本身往往是底层更大缓冲区的一个片段,直接访问event.buffer拿到的是完整底层缓冲区,而非当前报文对应的有效字节段,基于这个buffer构造视图时偏移计算错误,就会触发越界。 - 响应解析失败原因:
socket.write方法用于发送字符串内容,会将传入对象做字符串编码后输出,根本不适合发送Modbus要求的二进制报文;同时Modbus TCP响应有严格的格式规范,必须按协议组装完整字节流后才能被客户端正确解析。
解决方案
Modbus TCP读保持寄存器响应格式规范
响应报文总长度固定为 9 + 2*寄存器数量 字节,各段定义如下:
- 0-1字节:事务序列号,必须和请求报文中的事务ID完全一致
- 2-3字节:协议标识,Modbus TCP固定为
0x0000 - 4-5字节:后续字节长度,值为
3 + 2*寄存器数量 - 6字节:单元标识符,和请求报文中的单元ID一致
- 7字节:功能码,读保持寄存器固定为
0x03 - 8字节:返回数据字节长度,值为
2*寄存器数量 - 9字节及之后:寄存器实际值,每个寄存器占2字节,大端序排列
修正后服务端代码
import 'dart:io'; import 'dart:typed_data'; void createServer() { // 模拟寄存器存储,key为寄存器地址,value为寄存器值 final Map<int, int> registers = {4: 5, 5: 6, 6: 7, 7: 8, 8: 9}; ServerSocket.bind("127.0.0.1", 4000).then((server) { server.listen((socket) { socket.listen((event) { // 解析请求公共字段 final int transactionId = (event[0] << 8) | event[1]; final int unitId = event[6]; final int functionCode = event[7]; if (functionCode == 3) { // 处理读保持寄存器请求 final int startAddr = (event[9] << 8) | event[10]; final int quantity = (event[11] << 8) | event[12]; final int byteCount = quantity * 2; // 按协议构造响应字节流 final BytesBuilder builder = BytesBuilder(); builder.add([(transactionId >> 8) & 0xFF, transactionId & 0xFF]); // 事务ID builder.add([0, 0]); // 协议标识 builder.add([0, 3 + byteCount]); // 后续报文长度 builder.add([unitId]); // 单元ID builder.add([functionCode]); // 功能码 builder.add([byteCount]); // 数据字节长度 // 写入寄存器值 for (int i = 0; i < quantity; i++) { int addr = startAddr + i; int value = registers[addr] ?? 0; builder.add([(value >> 8) & 0xFF, value & 0xFF]); } // *二进制数据必须使用socket.add发送,不能用socket.write* socket.add(builder.toBytes()); } else if (functionCode == 6) { // 处理写单个寄存器请求,直接回显请求报文即可符合协议要求 socket.add(event); } }); }); }); }
关键注意点
- 不要直接通过
Uint8List.buffer获取底层缓冲区构造视图,若需要截取片段使用Uint8List.sublistView,或通过BytesBuilder从零构造新的字节数组 - 多字节字段(事务ID、寄存器地址、数值等)采用大端序排列,高低位顺序不能错
- 不存在的寄存器地址默认返回0即可,可根据业务需求自定义异常返回码
内容的提问来源于stack exchange,提问作者Miftakhul Arzak
相关产品推荐
相关产品推荐

