I'm trying to learn about MODBUS as a free-time project. It is a long standing desire to write my own driver in Golang. That being said, I'm now trying to design the concurrency model for device connections.
I'm using the MODBUS specifications from modbus.org as guidance. Specifically, my question arises after reading the MODBUS Messaging on TCP/IP Implementation Guide V1.0b section 4.2 TCP CONNECTION MANAGEMENT:
Implementation Rules :
- Without explicit user requirement, it is recommended to implement the automatic TCP connection management
- It is recommended to keep the TCP connection opened with a remote device and not to open and close it for each MODBUS/TCP transaction, Remark: However the MODBUS client must be capable of accepting a close requestfrom the server and closing the connection. The connection can be reopened when required.
- It is recommended for a MODBUS Client to open a minimum of TCP connections with a remote MODBUS server (with the same IP address). One connection per application could be a good choice.
- Several MODBUS transactions can be activated simultaneously on the same TCP Connection. Remark: If this is done then the MODBUS transaction identifier must be used to uniquely identify the matching requests and responses.
- In case of a bi-directional communication between two remote MODBUS entities (each of them is client and server), it is necessary to open separate connections for the client data flow and for the server data flow.
- A TCP frame must transport only one MODBUS ADU. It is advised against sending multiple MODBUS requests or responses on the same TCP PDU.
1. What I normally would do
Use a connection pool. Each request takes a connection out of the pool and define a maximum amount of concurrent connections. If there are no connections available in the pool and N < Max
create a new one. Idle connections close after a timeout. Closed and errored connections will be redialed as required. Requests can be done from multiple Go routines.
A Send()
method will build and APU and Write()
it on an acquired connection. It then attempts to Read()
. Until the result comes back, this routine is blocked. On success the resulting APU is parsed into a PDU and returned to the caller. The connection is Put()
back into the pool. In case of a connection error during Write()
or Read()
, the error is returned and the closed & connection is discarded. (Perhaps I'll implement some retrying later)
This method keeps a perfect relation requests, results and potential errors. Its a common used pattern if Go networking application like SQL drivers, HTTP, gRPC and more.
2. If I would follow the spec's advice
Use a single connection. Request PDUs are passed through a channel to a handler. The channel sequences the requests in a concurrent safe way. A handler runs in an infinite loop over the channel. For every PDU it builds an APU, which contains an incremental Transaction ID, as defined by the spec. This ID needs to be stored in a concurrent safe way into the connection object, probably with a return channel attached to it. For example a map[uint16]chan struct{pdu, error}
, protected by a mutex.
A second loop iterates over Read()
. When a reply arrives, it reads the header and decodes the transaction ID. The return channel is than obtained from the map, over which it sends back the PDU.
In this situation connection error handling and propagation becomes a bit more vague. In case of any error I would somehow lock the Write()
loop until redial logic completes. But how to deal with the callers that are still waiting for their Read()
results on a channel? It can be considered that their result will never arrive. Should the error be send to all channels currently waiting? Optionally, I can store the original request in the same map and re-transmit.
In any case, this approach feels fragile at best.
My questions
Which way should I go? My preference is clear (1). Will this lead to future problems on embedded devices? Unfortunately I don't have devices to work with yet, nor the experience with them.