Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
•
Malware / Trojaner / VirenStreaming-Piraterie: Piraten wechseln vom Firestick zu Social Media(23.09.2026 um 19:09 Uhr)
•
KI & AI VideosUsing Claude Opus 5.5 as your daily driver(23.09.2026 um 18:41 Uhr)
•
Sicherheitslücken (CVE)Security Weekly - A CRA Resource: The AI Vulnerability That Isn’t AI(23.09.2026 um 19:00 Uhr)
•
IT Security ToolsGitHub Release: google/clusterfuzz v2.41.0 (23.09.2026)(23.09.2026 um 18:55 Uhr)
•
IT Security ToolsGitHub Release: trufflesecurity/trufflehog v3.97.7 (23.09.2026)(23.09.2026 um 18:56 Uhr)
•
IT Security NachrichtenHackers Say They Stole Thousands of Sensitive F.B.I. Personnel Records(23.09.2026 um 19:33 Uhr)
•
IT Security NachrichtenMikroTrick: Zwei RouterOS-Fehler kombinieren sich zu Admin-Übernahme(23.09.2026 um 18:53 Uhr)
••••
Malware / Trojaner / VirenStreaming-Piraterie: Piraten wechseln vom Firestick zu Social Media(23.09.2026 um 19:09 Uhr)
•
KI & AI VideosUsing Claude Opus 5.5 as your daily driver(23.09.2026 um 18:41 Uhr)
•
Sicherheitslücken (CVE)Security Weekly - A CRA Resource: The AI Vulnerability That Isn’t AI(23.09.2026 um 19:00 Uhr)
•
IT Security ToolsGitHub Release: google/clusterfuzz v2.41.0 (23.09.2026)(23.09.2026 um 18:55 Uhr)
•
IT Security ToolsGitHub Release: trufflesecurity/trufflehog v3.97.7 (23.09.2026)(23.09.2026 um 18:56 Uhr)
•
IT Security NachrichtenHackers Say They Stole Thousands of Sensitive F.B.I. Personnel Records(23.09.2026 um 19:33 Uhr)
•
IT Security NachrichtenMikroTrick: Zwei RouterOS-Fehler kombinieren sich zu Admin-Übernahme(23.09.2026 um 18:53 Uhr)
•••
Intelligence View
⚡ tsecurity.de Intelligence

Building a Custom Modbus TCP Client in .NET: What a Production-Grade Poller Actually Needs

Most Modbus tutorials in .NET end at the same place: install a library, call ReadHoldingRegisters, print a number. That is enough to prove a device is reachable. It is not enough to run anything. The moment you point that code at more…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Most Modbus tutorials in .NET end at the same place: install a library, call ReadHoldingRegisters, print a number. That is enough to prove a device is reachable. It is not enough to run anything.



The moment you point that code at more than one device and leave it running, the questions change. What happens when a device stops responding? What happens when one slow device blocks the others? Why is that float reading 3.4e38 instead of 480? Where does the data actually go?



This article covers the layer that answers those questions: the application code you build around a Modbus library. Everything here runs against a simulated device included in the code, so you can execute the whole thing locally with nothing but the .NET SDK. All values and device names are illustrative.






A short Modbus refresher



Modbus TCP is a request/reply protocol. A client opens a TCP connection (port 502 by convention) and asks a device for data. The device answers. There is no subscription and no push, so the client polls on a schedule.



Data lives in four spaces:

































Space Size Access
Coils 1 bit read/write
Discrete inputs 1 bit read
Input registers 16 bit read
Holding registers 16 bit read/write


Function code 3 reads holding registers and is the one you will use most. A request carries a transaction ID, a unit ID, a starting address, and a register count. The response carries the raw 16-bit words back.



Two properties of that design drive most of what follows. Registers are only 16 bits wide, so anything larger spans several of them. And the wire format is big-endian, which will not match your CPU.






Just use a library, then keep going



Do not reimplement Modbus framing for production. NModbus, FluentModbus, and EasyModbusTCP all handle the wire protocol, and FluentModbus ships a server implementation that is useful for testing.



What none of them give you is a client. They give you a read call. The distance between a read call and something you can deploy is:




  • connection lifecycle across devices that disappear and come back

  • a polling schedule per device that tolerates slow or dead peers

  • decoding raw registers into typed values you can reason about

  • somewhere for the data to go, decoupled from how it was acquired

  • enough visibility to know when a device went quiet



That is the custom part. It is specific to your system, which is exactly why no package solves it for you.



The code below writes the framing directly against System.Net.Sockets so the example has zero dependencies and you can read every byte. In a real build, swap that piece for a library and keep the structure around it.






Component 1: decoding registers into typed values



This is where most first attempts break, so start here.



A 32-bit float occupies two consecutive registers. To reconstruct it you concatenate both words in the order the device sent them and read the result as big-endian. Get the order wrong and you do not get an error, you get a plausible-looking wrong number, or a value like 1.7e38.



Rather than scatter that logic through the codebase, describe the device as data:




enum RegisterType { Float32, UInt16Enum }

record RegisterDefinition(string Name, ushort Address, RegisterType Type, string Unit);

record Reading(string DeviceName, string PointName, object Value, string Unit, DateTime TimestampUtc);

static class RegisterDecoder
{
public static object Decode(RegisterDefinition def, ushort[] regs, int offset)
{
switch (def.Type)
{
case RegisterType.Float32:
Span<byte> buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt16BigEndian(buf.Slice(0, 2), regs[offset]);
BinaryPrimitives.WriteUInt16BigEndian(buf.Slice(2, 2), regs[offset + 1]);
return Math.Round(BinaryPrimitives.ReadSingleBigEndian(buf), 2);

case RegisterType.UInt16Enum:
return regs[offset] switch
{
0 => "Stopped",
1 => "Running",
_ => "Fault"
};

default:
throw new NotSupportedException();
}
}
}






A register map then becomes a list, not code:




var map = new List<RegisterDefinition>
{
new("Voltage", 0, RegisterType.Float32, "V"),
new("Frequency", 2, RegisterType.Float32, "Hz"),
new("Status", 4, RegisterType.UInt16Enum, ""),
};






Two things worth knowing before you fight them. Vendors disagree about word order, so some devices need the two registers swapped before decoding, which is why a WordSwapped flag usually appears in the map eventually. And documentation often numbers registers from 1 while the protocol numbers them from 0, which is a very common source of off-by-one addressing.






Component 2: connection management



BinaryPrimitives handles endianness. The socket handles nothing.



Set explicit timeouts. Without them a silent device leaves you waiting on a read that never returns, and one stalled device can quietly stop your entire pipeline.




public async Task ConnectAsync(CancellationToken ct)
{
_client = new TcpClient { ReceiveTimeout = 1000, SendTimeout = 1000 };
await _client.ConnectAsync(_host, _port, ct);
_stream = _client.GetStream();
}






Also, never assume one ReadAsync returns a whole frame. TCP is a stream, not a message queue. A response can arrive in pieces, and the header tells you how many bytes to expect, so read until you have them all:




private async Task ReadExactAsync(byte[] buffer, CancellationToken ct)
{
int read = 0;
while (read < buffer.Length)
{
int n = await _stream!.ReadAsync(buffer.AsMemory(read, buffer.Length - read), ct);
if (n == 0) throw new IOException("Connection closed by device.");
read += n;
}
}






Skipping that check is the classic bug that works perfectly on localhost and fails intermittently on a real network.






Component 3: the polling engine



Each device gets its own loop and its own interval. Independent loops mean a dead device cannot delay a healthy one.



Failures are normal, not exceptional. Devices reboot, networks drop, technicians unplug things. The loop treats a failure as an expected state and backs off exponentially instead of hammering a device that is down:




catch (Exception ex)
{
_failures++;
conn?.Dispose();
conn = null;

var delay = TimeSpan.FromMilliseconds(
Math.Min(200 * Math.Pow(2, _failures - 1), 3000));

Console.WriteLine($"{_cfg.Name}: read failed ({ex.GetType().Name}), " +
$"retry #{_failures} in {delay.TotalMilliseconds:F0}ms");

await Task.Delay(delay, ct);
}






One detail is easy to get wrong, and I got it wrong first while writing this. Reset the failure counter after a successful read, not a successful connect. If you reset on connect, a device that accepts connections but immediately drops them looks healthy every cycle, the counter returns to zero, and the backoff never grows. You reconnect in a tight loop forever. The captured output below shows the corrected behavior.






Component 4: a pluggable sink



Polling and delivery are separate concerns. Keep them apart behind an interface:




interface IReadingSink
{
Task WriteAsync(IReadOnlyList<Reading> readings, CancellationToken ct);
}






The demo uses a console sink. A real deployment might implement the same interface over Azure Event Hubs, MQTT, Kafka, or a time-series database, and a batching decorator that buffers readings and flushes on an interval slots in without the polling loop knowing about it.



The payoff is testability. The polling logic never knows where data lands, so you can verify it against a fake sink and a simulator with no cloud account involved.






Running it



The full program starts two simulated devices, points three pollers at them (the third at a port where nothing is listening), and takes one device offline mid-run. Real captured output:




[18:50:21] device-01: connected
[18:50:21] device-02: connected
[18:50:21] device-03: read failed (SocketException), retry #1 in 200ms
[18:50:21] device-01 Voltage = 478.99 V
[18:50:21] device-01 Frequency = 59.92 Hz
[18:50:21] device-01 Status = Running
[18:50:21] device-02 Voltage = 481.08 V
[18:50:21] device-02 Frequency = 59.98 Hz
[18:50:21] device-02 Status = Stopped
[18:50:21] device-03: read failed (SocketException), retry #2 in 400ms
[18:50:22] device-03: read failed (SocketException), retry #3 in 800ms
[18:50:22] device-01 Voltage = 481.09 V
[18:50:22] device-01 Frequency = 60.03 Hz
[18:50:22] device-01 Status = Running
[18:50:22] device-03: read failed (SocketException), retry #4 in 1600ms
[18:50:23] device-02 Voltage = 481.94 V
[18:50:23] device-02 Frequency = 59.92 Hz
[18:50:23] device-02 Status = Stopped
[18:50:24] device-03: read failed (SocketException), retry #5 in 3000ms
[18:50:25] *** simulating device-02 going offline ***
[18:50:25] device-01 Voltage = 479.28 V
[18:50:25] device-01 Frequency = 60.1 Hz
[18:50:25] device-01 Status = Fault
[18:50:27] device-02: read failed (IOException), retry #1 in 200ms
[18:50:27] device-03: read failed (SocketException), retry #6 in 3000ms
[18:50:27] device-01 Voltage = 480.82 V
[18:50:27] device-01 Frequency = 60.04 Hz
[18:50:27] device-01 Status = Fault
[18:50:27] device-02: connected
[18:50:27] device-02: read failed (IOException), retry #2 in 400ms
[18:50:28] device-02: connected
[18:50:28] device-02: read failed (IOException), retry #3 in 800ms
[18:50:28] device-01 Voltage = 478.37 V
[18:50:28] device-01 Frequency = 59.93 Hz
[18:50:28] device-01 Status = Running
[18:50:28] device-02: connected
[18:50:28] device-02: read failed (IOException), retry #4 in 1600ms
Shutdown complete.






The behavior worth noting: device-01 polls on schedule the entire time, unaffected by two failing neighbors. device-03, which never existed, walks its backoff up to the 3 second ceiling instead of spinning. device-02 degrades gracefully when pulled offline, and its retry counter climbs correctly because it only resets on a successful read.






What this buys you



Failure isolation. One bad device costs you that device, not the fleet.



Testability. A simulator plus a fake sink means the whole pipeline runs in CI.



Decoupled evolution. Protocol handling, decoding, and delivery change independently. Swapping Event Hubs for Kafka touches one class.



Configuration over code. New device model, new register map entry, no redeploy of polling logic.



Operability. Per-device failure counts and timestamps tell you which device went quiet and when.






When not to build this



If you have one device, read it occasionally, and can tolerate a crash and restart, use a library directly in twenty lines and stop reading. The structure here earns its cost when you have a fleet, uneven reliability, and a downstream system that expects a steady stream.



If your devices already speak MQTT or OPC UA, prefer those. Modbus is a 1979 protocol with no security model and no push, and you should not add a polling layer you do not need.






Takeaways



The interesting problems in industrial data collection are not in the protocol. They are in everything wrapped around it: endianness, partial reads, timeouts, backoff, isolation, and a clean seam between acquisition and delivery.



Four rules worth keeping:




  1. Treat the register map as configuration, not code.

  2. Assume every read can fail, and back off when it does.

  3. Reset failure counters on successful reads, not successful connects.

  4. Keep the sink behind an interface so polling never knows where data goes.



The complete runnable program, simulator included, is roughly 330 lines and requires only the .NET SDK.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building a Custom Modbus TCP Client in .NET: What a Production-Grade Poller Actually Needs

Thematisch verwandte Begriffe: Building, Custom, Modbus, Client · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-18181 | IBM Financial Transaction Manager (FTM) for RedHat OpenShift could allow…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel • Rechts: nächster Artikel • unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel TTP ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger • Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick