Skip to content

Option to disable request batching for strict Modbus compliance #118

@cmdjulian

Description

@cmdjulian

Hello,

First, thank you for creating and maintaining this excellent and high-performance Modbus library.

The Issue

I am using the library to communicate with an industrial controller (PLC) that has a very strict implementation of the Modbus/TCP protocol.

When I send multiple requests asynchronously in a rapid succession, the library's underlying Netty channel batches them into the payload of a single TCP packet. My Wireshark captures clearly show multiple Modbus ADUs being sent in one TCP segment.

This behavior violates the official Modbus specification, which states, "A TCP frame must transport only one MODBUS ADU." As a result, the controller considers this a framing error and discards the packet, so I never receive a response.

What I've Tried

  1. TCP_NODELAY: I have confirmed that setting ChannelOption.TCP_NODELAY to true does not solve the problem. The batching appears to happen at the application/Netty level before the data is handed to the OS network stack, so Nagle's algorithm is not the root cause. Or the channel option is not applied properly, I can't really tell.
    What I'm basically doing is the following:
val config = NettyTcpClientTransport.create { cfg ->
    cfg.hostname = uri.host
    cfg.port = uri.port
    cfg.connectPersistent = device.holdConnection
    cfg.connectTimeout = device.timeout
    cfg.bootstrapCustomizer = Consumer<Bootstrap> { it.option(ChannelOption.TCP_NODELAY, config.tcpNoDelay) }
}
  1. Sequential Chaining: The only effective workaround is to manually syncronize requests by guarding the ModbusTcpClient instance with a semaphore and force sequential request / response behaviour.

The Problem with the Workaround

While manually synchronization works for ensuring compliance, it forces a sequential execution model. This makes the application code more complex and negates the throughput benefits of sending multiple independent requests concurrently in an asynchronous manner.
Unfortunately otherwise the controller does not respond in a predictable way... Sometimes I simply does not answer at all.

Is there any similar problem you are aware of? Would it be possible to add a configuration option to the client to handle this common industrial requirement more directly?

A configuration flag—perhaps something like .strictComplianceMode(true) or .flushAfterRequest(true)—would be incredibly helpful. When enabled, this mode would ensure that the library flushes the channel after each request is written, guaranteeing one ADU per TCP packet without forcing the user to implement complex sequential logic.

Or is there anything I'm missing here?

Thank you for your time and consideration.

Spec:
Image

TCP_NODELAY=false
Image
Image

TCP_NODELAY=true
Image

FYI:
I'm using the newest version of the client v2.1.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions