You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter如何用ServerSocket实现Modbus TCP服务端响应客户端读请求

Dart实现Modbus TCP服务端响应异常解决

问题背景

  • 开发基于Modbus协议与PLC通信的桌面端应用时,发现pub.dev现有Modbus插件仅提供客户端实现,不支持服务端能力
  • 查阅插件源码确认客户端基于dart:io的Socket实现,因此尝试通过ServerSocket自行搭建Modbus TCP服务端
  • 测试验证客户端可正常连接自建TCP服务端,服务端可接收到客户端发送的Uint8List格式报文:
    1. 写单个寄存器(向地址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为待写入值
    1. 读保持寄存器(读取地址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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 08:45:34