Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 11 Min.

SignalFTP als Lösung für SignalRC

SignalFTP zeigt gut, wie man mit den Einschränkungen einer mobilen Verbindung umgehen kann, ohne das Architekturprinzip aufzugeben, das SignalRC zusammenhält: Der Server ist ein Proxy.
© ©Sofija De Mitri, Patrizio De Mitri, Event Wave

Heute geht es um ein Thema, das auf den ersten Blick fast zu simpel klingt: Dateien auf das Fahrzeug zu übertragen, das wir als DDC-Truck im Artikel „SignalRC – in Echtzeit ans Steuer“ [1] ausführlich vorgestellt haben. Eigentlich macht jeder von uns so etwas täglich: Datei auswählen, irgendwo hochladen, fertig. 

Wenn das Ziel aber nun kein Cloud-Server ist, sondern ein Raspberry Pi, der in einem fahrenden Truck steckt und nur über eine LTE-Verbindung erreichbar ist, dann wird es interessanter. Das Zielgerät ist mobil, die Bandbreite knapp und die Verbindung eher instabil.

Wie SignalRC das Ganze löst und dabei seinem Proxy-Prinzip treu bleibt, sehen wir uns jetzt an.

Warum nicht einfach SCP?

Die naheliegendste Idee wäre: SSH-Verbindung auf, SCP drauf, fertig. Das war auch mein erster Gedanke. Aber das setzt voraus, dass der Raspberry Pi direkt erreichbar ist. Und genau da liegt das Problem. Hinter einem LTE-Netz gibt es kein Port Forwarding, keine feste IP und kein offenes Netz. Der Mobilfunkanbieter lässt dich von außen schlicht nicht ran. Das ist übrigens kein SignalRC-spezifisches Problem, das betrifft so ziemlich jedes IoT-Gerät im Mobilfunknetz. 

Die Lösung ist deshalb oft dieselbe: Das Gerät baut die Verbindung auf, nicht umgekehrt. Der Onboard-Client verbindet sich zum Server, hält die Verbindung offen und wartet auf Anweisungen. Das ist das Grundprinzip von SignalRC, und es zieht sich durch alles, auch durch den Dateitransfer.

Der Server als reiner Proxy

Bevor wir tiefer in den Dateitransfer einsteigen, lohnt es sich, kurz über ein Architekturprinzip zu sprechen, das in SignalRC konsequent durchgezogen wird: Der Server ist ein Proxy. Er vermittelt, leitet weiter, formt um. Das klingt erst mal naheliegend, ist es aber nicht. In vielen IoT-Architekturen liegt die Logik auf dem Server. Der Server entscheidet, ob ein Befehl ausgeführt wird, der Server validiert Berechtigungen, der Server kennt den Zustand des Geräts. In SignalRC ist das bewusst anders.

Wenn der Browser einen Steuerbefehl schickt, leitet der Server ihn ans Fahrzeug weiter. Das Fahrzeug prüft die Session und entscheidet, ob es den Befehl ausführt. Wenn der Browser eine Challenge für die Authentifizierung anfragt, generiert das Fahrzeug diese Challenge, nicht der Server. So können Fahrzeug und Client alles unter sich ausmachen. Das ist einer üblichen RC-Fernsteuerung nachempfunden. Der Server ist nur das Transportmedium.

Man sieht das sehr schön im CarControlHub, wo quasi jede Methode demselben Muster folgt:

 

// Verbindung zum Fahrzeug nachschlagen
 if (!_connectionMap.TryGetByKey(carId.ToString(), out var carClientId))
     return null;
 // Anfrage weiterleiten, Antwort zurückgeben
 return await Clients.Client(carClientId).DoSomething(sessionId, ...);

 

Der Server schaut in seiner Connection Map nach, ob das Auto überhaupt verbunden ist. Wenn ja, leitet er die Anfrage einfach weiter. Was am anderen Ende damit passiert, ist Sache des Onboard-Clients. Warum? Weil der Server den Zustand des Fahrzeugs nicht kennen muss. Er muss nicht wissen, ob gerade gefahren wird, ob der Akku voll ist oder ob die Session noch gültig ist. All das weiß das Fahrzeug selbst. Und alles, was das Fahrzeug selbst weiß, muss nicht synchronisiert werden. Das spart Komplexität, reduziert Fehlerquellen und macht das ganze System stabiler. Falls der Server neu gestartet wird, verliert er keinen Fahrzeugzustand, denn er hatte nie einen.

Dieses Prinzip zieht sich auch komplett durch den Dateitransfer, und es erklärt einige Designentscheidungen, die sonst vielleicht überflüssig wirken würden.

Der Ablauf

Der Dateitransfer in SignalRC läuft über mehrere Schritte. Das Ganze ist bewusst asynchron und in Phasen aufgeteilt, damit jeder Schritt einzeln fehlschlagen und wiederholt werden kann. Kein einzelner Schritt ist so groß, dass ein Abbruch katastrophal wäre. 

 

1. Genehmigung einholen

Bevor irgendetwas hochgeladen wird, fragt der Browser beim Fahrzeug an, ob der Upload überhaupt erlaubt ist. Der Browser ruft RequestFileUpload am CarControlHub auf. Der Hub schaut in der Connection Map nach dem Fahrzeug und reicht die Frage direkt an den Onboard-Client durch:

 

var approved = await Clients.Client(carClientId).ApproveFileUpload(sessionId, filePath);
 if (!approved)
     return null;

 

Der Onboard-Client, also der ControlService auf dem Raspberry Pi, bekommt die Anfrage und prüft als Erstes, ob die mitgelieferte Session-ID mit der aktiven Fahrersession übereinstimmt. Keine gültige Session – kein Upload.

 

public Task<bool> ApproveFileUpload(string sessionId)
 {
     if (_sessionId != sessionId)
         return Task.FromResult(false);
     return Task.FromResult(true);
 }

 

Erst wenn das Fahrzeug zugestimmt hat, wird auf dem Server ein Transfer-Datensatz in der Datenbank angelegt. Dieser enthält einen zufällig generierten Download-Token. Es ist eine einfache GUID ohne Bindestriche. Dieser Token ist ab jetzt der Schlüssel für den gesamten weiteren Ablauf. Wer den Token hat, kann die Datei hochladen und herunterladen. Der Token allein reicht. Er ist entsprechend kurzlebig und kann nur für einen Upload verwendet werden.

 

2. Upload zum Server

Mit dem Token kann der Browser die Datei mit einem simplen HTTP-POST an den Server schicken. Wir benutzen hier bewusst nicht SignalR, denn SignalR eignet zwar ganz hervorragend für kleine, schnelle Echtzeitnachrichten, Steueranweisungen von ein paar Bytes und Telemetrie-Updates. Für einen 500-MB-Video-Blob dagegen eher nicht! Das ist, wofür HTTP erfunden worden ist. Multipart Uploads, Content-Length Header, Range Requests, etabliertes Fehlerhandling. Nutze das richtige Werkzeug für den Job.

Hierfür brauchen wir den Upload-Endpunkt:

 

[HttpPost("{token}")]
 [RequestSizeLimit(1024 * 1024 * 1024)]
 public async Task<IActionResult> Upload(string token, IFormFile file)

 

Das Request-Size-Limit wird aktiv für den einen Endpunkt auf 1 GiB gesetzt. Zum einen übersteuern wir damit eventuell eingestellte niedrigere Limits und begrenzen es auf der anderen Seite. Es gibt noch zusätzlich die konfigurierbare Einstellung MaxFileSizeMB, die standardmäßig auf 100 MB steht, damit wir das Limit ohne Code-Änderung anpassen können. Ich wüsste jetzt nicht, was ich auf mein RC-Fahrzeug kopieren sollte, das größer als 1 GB wird, und ich habe schon Bildschirme drauf verbaut.

Während die Datei auf die Festplatte geschrieben wird, berechnet der Server direkt einen SHA-256-Hash mit. Das passiert über einen CryptoStream, der sich zwischen den eingehenden Datenstrom und die Datei auf der Platte hängt. Jedes Byte, das durchfließt, wird gleichzeitig gehasht. So muss die Datei nicht zweimal gelesen werden, was bei großen Dateien durchaus relevant ist.

 

using var sha256 = SHA256.Create();
 await using var target = new FileStream(fullPath, FileMode.Create, ...);
 await using var hashStream = new CryptoStream(target, sha256, CryptoStreamMode.Write);
 await file.CopyToAsync(hashStream);
 await hashStream.FlushFinalBlockAsync();
 hash = Convert.ToHexString(sha256.Hash!);

 

Der Dateiname auf der Platte des Servers ist übrigens nicht der Originalname, sondern der Token mit der originalen Dateiendung. Das verhindert Namenskonflikte und macht es unmöglich, durch Path Traversing aus dem Speicherverzeichnis auszubrechen. Wir müssen ja nicht zum Gehacktwerden einladen.

 

3. Benachrichtigung ans Fahrzeug

Sobald die Datei vollständig auf dem Server liegt und der Hash berechnet ist, ändert sich der Status des Transfers auf Ready. Jetzt muss das Fahrzeug erfahren, dass es etwas abzuholen gibt.

Dafür schickt der Server eine FileReadyNotification über den CarControlHub an die SignalR-Gruppe des Fahrzeugs. Das ist eine simple SignalR-Nachricht. Der Server wartet nicht auf eine Antwort. Er geht einfach davon aus, dass das Fahrzeug den Download startet.

 

await _controlHub.Clients.Group($"Car-{transfer.CarId}").FileReady(
     new FileReadyNotification
     {
         Token = transfer.DownloadToken,
         FileName = transfer.FileName,
         FileSizeBytes = transfer.FileSizeBytes,
         Sha256Hash = hash
     });

 

Der Hash wird direkt mitgeschickt, damit das Fahrzeug nach dem Download prüfen kann, ob die Datei korrekt angekommen ist. Ebenfalls enthalten ist die Dateigröße, damit das Fahrzeug im Voraus weiß, ob es genug Speicherplatz hat. Der Dateiname wird mitgeschickt, damit es weiß, wo die Datei hinsoll. Alles, was das Fahrzeug für die Download-Entscheidung braucht, steckt also in einer Nachricht.

 

4. Download durch das Fahrzeug

Das Fahrzeug holt sich die Datei über einen HTTP-GET-Endpoint ab. Wieder HTTP, nicht SignalR. Diesmal wegen eines weiteren Features von HTTP: Range Request Support.

 

[HttpGet("{token}/download")]
 public async Task<IActionResult> Download(string token, 
     [FromQuery] int? maxKBytesPerSecond = null)

 

Warum Range Requests? Weil eine LTE-Verbindung notorisch instabil ist. Im Tunnel, in einem Funkloch, bei einem Handover zwischen Funkzellen. Das passiert ständig. Ohne Range Requests müsste der Download bei jedem Abbruch komplett von vorne beginnen. Bei einer 50-MB-Datei über eine 20-KB/s-Verbindung wäre das eine Tortur, die möglicherweise nie endet.

Mit Range Requests fragt das Fahrzeug einfach: „Gib mir die Bytes ab Position 1.048.576“. Der Server springt in der Datei an die richtige Stelle und liefert ab dort weiter. So kann ein Download, der bei 30 Prozent abgebrochen ist, nahtlos bei 30 Prozent fortgesetzt werden. Der Server antwortet dann mit Status 206 Partial Content und dem entsprechenden Content-Range- Header. Standardkonformes HTTP also – jeder gute HTTP-Client kann damit umgehen. 

Eigentlich kann Web-API das schon „out of the box“, aber wir wrappen hier noch den Throttle Stream ums File. Daher lesen wir die Header von Hand:

 

var rangeHeader = Request.GetTypedHeaders().Range;
 if (rangeHeader?.Ranges.Count == 1)
 {
     var range = rangeHeader.Ranges.First();
     rangeStart = range.From ?? 0;
     rangeEnd = range.To ?? fileLength - 1;
 }

 

5. Abschluss und Verifizierung

Hat das Fahrzeug die Datei komplett heruntergeladen, berechnet es den SHA-256-Hash der empfangenen Daten und meldet den Status Completed zusammen mit dem Hash an den Server zurück. Das geschieht über die Methode ReportFileTransferStatus am CarConnectionHub.

Dann wird geprüft, ob der gemeldete Hash mit dem Hash übereinstimmt, den der Server beim Upload selbst berechnet hat. Beide Seiten haben unabhängig voneinander gehasht. Stimmen die Werte überein, ist die Datei garantiert korrekt übertragen worden und kann weg.

 

if (string.Equals(transfer.Sha256Hash, update.Sha256Hash, 
     StringComparison.OrdinalIgnoreCase))
 {
     File.Delete(transfer.StoragePath);
     dbContext.FileTransfers.Remove(transfer);
     await dbContext.SaveChangesAsync();
 }

 

Bei Übereinstimmung räumt der Server auf. Die temporäre Datei wird von der Platte gelöscht und der Transfer-Datensatz verschwindet ebenfalls aus der Datenbank. Der Server ist wieder sauber, als wäre nie etwas passiert. Das ist wichtig, denn der Server soll schließlich kein Archiv sein. Er war nur die Zwischenstation. Wenn ich wissen möchte, was an Dateien da ist, kann ich das Fahrzeug fragen.

Throttling – Rücksicht auf die Leitung

Eine LTE-Verbindung hat begrenzte Bandbreite. Diese Bandbreite teilt man sich mit der Steuerung, dem Video und der Telemetrie. Wenn jetzt ein 500-MB-Download die gesamte Leitung belegt, reagiert die Steuerung plötzlich verzögert oder das Video reißt ab. Bei einem ferngesteuerten Fahrzeug ist das keine Unannehmlichkeit, es ist gefährlich.

Deshalb wird der Download serverseitig gedrosselt. Der Server liefert die Daten in kleinen 4-KB-Chunks und wartet zwischen jedem Chunk eine berechnete Pause ab. Die Berechnung ist so simpel wie effektiv:

 

var throttleBytesPerSecond = effectiveRate * 1024;
 const int chunkSize = 4096;
 var intervalMs = (int)((double)chunkSize / throttleBytesPerSecond * 1000);
 if (intervalMs < 1) intervalMs = 1;

 

Bei den standardmäßig konfigurierten 20 KB/s bedeutet das: 4-KB-Chunk senden, 200 Millisekunden warten, nächster Chunk. Das klingt langsam, und 20 KB/s sind dies auch. Aber eine 10-MB-Datei ist damit in knapp 9 Minuten da, und die Steuerung bleibt die ganze Zeit flüssig. Das ist meiner Meinung nach der richtige Trade-off. 

Das Fahrzeug kann über den Query-Parameter maxKBytesPerSecond auch selbst das Tempolimit beim Download mitgeben. Der Server verwendet dann den niedrigeren Wert aus den Voreinstellungen für Client-Wunsch und Server-Konfiguration. So kann das Fahrzeug bei guter Verbindung schneller laden, aber nie schneller, als der Server erlaubt.

Konfigurierbar ist das Ganze über die FileTransferConfiguration. Wer sich etwas mutiger fühlt kann das Limit auch höher konfigurieren – ganz nach Wunsch. Ich habe es bei meinen Uploads nicht so eilig.

Dateien auf dem Fahrzeug verwalten

SignalFTP kann nicht nur Dateien hochladen. Über das ICarControlClient-Interface kann der Browser auch das Dateisystem des Fahrzeugs durchstöbern und Dateien löschen. Auch hier gilt wieder: Der Server ist nur das Medium. Er weiß nicht, welche Dateien auf dem Fahrzeug liegen. Er fragt einfach nach und gibt die Antwort weiter.

Auch auf dem Fahrzeug wird sichergestellt, dass nur innerhalb des konfigurierten Basispfads navigiert werden kann. Der Onboard-Client löst den angefragten Pfad auf und prüft, ob er noch im erlaubten Bereich liegt. Doppelt hält besser.

Warum nicht alles über SignalR?

Man könnte sich jetzt fragen: Wenn die SignalR-Verbindung ohnehin steht, warum dann nicht die Datei direkt darüber schicken? Das wäre doch eleganter, alles über einen Kanal.

Die Antwort ist pragmatisch. SignalR ist auf kleine Nachrichten optimiert. Die Serialisierung mit MessagePack, die SignalRC nutzt, ist extrem effizient für strukturierte Daten. Einen Binär-Blob von 50 MB durch MessagePack zu schieben und als SignalR-Nachricht zu verschicken ist keine gute Idee. Zum einen bläht das die Nachricht unnötig auf, und zum anderen müssten wir das Chunking und Fehlerhandling komplett selbst designen.

HTTP dagegen bietet von Haus aus alles, was man für Dateitransfer braucht: Multipart Uploads, Content-Length, Range Requests, Streaming und ein über Jahrzehnte gereiftes Fehlerhandling für große Payloads. Die Kombination aus SignalR für die Koordination und HTTP für den eigentlichen Transport holt das Beste aus beiden Welten heraus. 

Was kommt noch?

  • Fortschrittsanzeige: Bei größeren Dateien über eine gedrosselte Leitung wäre es schön, im Browser einen Fortschrittsbalken zu sehen. Die Infrastruktur dafür ist im Grunde schon da. Der Server kennt die Dateigröße, der Download-Status lässt sich tracken, und über den bestehenden SignalR-Kanal könnte man regelmäßig den aktuellen Fortschritt an den Browser pushen.
  • Automatische Updates: Die spannendste Anwendung für die Zukunft: Softwareupdates auf das Fahrzeug übertragen, ohne physisch vor Ort sein zu müssen. Neue Firmware, geänderte Channel Maps oder aktualisierte Konfigurationen, alles über SignalFTP. Das Fahrzeug könnte nach einem erfolgreichen Download sogar selbstständig einen Neustart triggern und die neue Version laden. Ich sehe schon die Over-the-Air-Updates für den DDC-Truck.

Fazit

SignalFTP zeigt gut, wie man mit den Einschränkungen einer mobilen Verbindung umgehen kann, ohne das Architekturprinzip aufzugeben, das SignalRC zusammenhält: Der Server ist ein Proxy. Er vermittelt, er speichert temporär, er drosselt den Durchsatz. Die Entscheidungen trifft das Fahrzeug aber allein: Ob ein Upload erlaubt ist, ob eine Session gültig ist, welche Dateien sichtbar sind. 

SignalR übernimmt die Koordination in Echtzeit, HTTP den schwerlastigen Transport. Das Ergebnis ist ein robuster Dateitransfer, der Verbindungsabbrüche verkraftet und die Bandbreite berücksichtigt. Vielleicht nicht direkt FTP, aber ein guter Workaround für unsere Lösung.

 

[1] Georg Poweleit, SignalRC – in Echtzeit ans Steuer, dotnetpro 10-11/2025, Seite 46 ff.

Neueste Beiträge

00:00
KI und Security: Aufrüsten auf beiden Seiten - Ein Interview mit Christian Wenz, Track Chair Software Security der DWX 2026
KI übernimmt knifflige Aufgaben - wie das Suchen von Sicherheitslücken. Die Erkenntnisse darüber können aber von den Guten und den Bösen verwendet werden.
3. Mär 2026
Die ganze Welt der Softwareentwicklung
Ein riesiges Angebot an Wissen, das von Expert:innen lebendig vermittelt wird, gewürzt mit Kontakt zu Gleichdenkenden – das ist der Kern der DWX.
6 Minuten
19. Feb 2026

Das könnte Dich auch interessieren

Batterie-Management mit SignalRC - Der DDC-Truck, Teil 4
Das Batterie-Management-System (BMS) von RC-Modellen benötigt verlässliche Telemetrie.
6 Minuten
12. Feb 2026
Version 30 von List & Label mit neuen Cloud-Funktionen - Reportgenerator
Die neue Version des Reporting-Tools List & Label unterstützt Entwickler:innen mit neuen Features, die speziell für Cloud- und Webumgebungen konzipiert sind.
2 Minuten
21. Okt 2024
SignalRC WebRTC - Der DDC-Truck, Teil 3
WebRTC ist als Tool ideal geeignet, um Videodaten von RC-Modellen in Echtzeit zu übertragen.
7 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige