![]() |
Konzept: Netzwerkprotokoll
Hallo zusammen,
ich habe bereits schon einige Entwürfe für ein (auf TCP aufgesetztes) Netzwerkprotokoll angefertigt, bin aber überzeugt, dass noch einiges verbessert werden kann. :?: Hat jemand von euch ein paar gute Konzeptvorschläge für mich? Bin dankbar für alles! Die Hauptfunktion des Protokolls soll es sein, multiple Daten(transfers) über ein einziges Socket ablaufen zu lassen. Beispielsweise 2 Dateiübertragungen und 3 Steuerbefehle. Hierbei soll mindestens eine einfache Priorisierung möglich sein. Sprich: Auch wenn die maximale Anzahl an gleichzeitigen Transfers erreicht ist, sollen die Steuerbefehle trotzdem noch zusätzlich gesendet werden. Folgende Grundfunktionen möchte ich zusätzlich unterstützen: :arrow: Blockweise Übertragung :arrow: Verschlüsselung & Kompression einzelner Blöcke :arrow: Suspend, Resume und Cancel einer Übertragung :arrow: Meta Daten, die zusammen mit dem Info Header sofort geschickt werden Für meine bisherige Implementation verwende ich folgende Header:
Delphi-Quellcode:
Auf Senderseite läuft ein Thread, welcher vorerst inaktiv wartet, bis mindestens eine Übertragung initialisiert wird. Ist dies der Fall, geht der Thread die komplette Liste mit Transfer Objekten durch und sendet in den vorgegebenen Parametern jeweils den nächsten Datenblock. Diese Schleife wird wiederholt, bis keine ausstehende Übertragung mehr vorhanden ist.
type
PdxIDTPMainHeader = ^TdxIDTPMainHeader; TdxIDTPMainHeader = packed record TransferID: Word; // ID der Übertragung PacketSize: Word; // Größe des aktuellen Datenblocks Flags: Byte; // optionale Flags end; TdxIDTPInfolHeader = packed record MetaSize: Word; // Größe der Metadaten DataSize: UInt64; // Größe der gesamt zu übertragenden Daten BlockSize: TdxIDTPBlockSize; // Info: angepeilte Einzelblockgröße Priority: Boolean; // Info: priorisierte Übertragung Encrypted: Boolean; // Info: verschlüsselte Übertragung Compressed: Boolean; // Info: komprimierte Übertragung end;
Delphi-Quellcode:
:arrow: Hier ist auch schon die erste Sache, die mich etwas stört: Der Thread verbraucht, durch die Schleife bedingt, relativ viel CPU Zeit. Das Sleep(1) schafft schon etwas Abhilfe, verringert natürlich aber auch deutlich die Übertragungsgeschwindigkeit.
procedure TdxIDTPSendThread.Execute;
var List: TList; ListCopy: array of TdxIDTPOTransfer; Transfer: TdxIDTPOTransfer; I, NormalCount: Integer; begin while (not Terminated) do begin // Transfer Objekte aus der gesicherten Liste kopieren. Von außerhalb // können danach zwar Übertragungen hinzugefügt oder deren Reihenfolge // geändert werden, was aber keinen Einfluss auf den aktuellen Durchlauf // hat. // ACHTUNG: Niemals Transfer Objekte außerhalb dieses Threads manuell // freigeben! List := FIOHandler.OutgoingTransfers.LockList; try // Abgeschlossene Übertragungen entfernen for I := List.Count -1 downto 0 do begin if (TdxIDTPOTransfer(List.Items[I]).TransferState = tsFinished) then begin TdxIDTPOTransfer(List.Items[I]).Free; List.Delete(I); end; end; // Kopie der Liste anfertigen // TODO: Hier suspendierte Transfers nicht kopieren! // Ausnahme: PendingStatusUpdate = true oder HeaderSent = false SetLength(ListCopy, List.Count); for I := 0 to List.Count - 1 do begin ListCopy[I] := TdxIDTPOTransfer(List.Items[I]); end; finally FIOHandler.OutgoingTransfers.UnlockList; end; // Thread suspendieren, wenn keine Transfers aktiv sind if (Length(ListCopy) = 0) then begin ResetEvent(FWaitEvent); WaitForSingleObject(FWaitEvent, INFINITE); end; if Terminated then begin Break; end; // Transferliste abarbeiten NormalCount := 0; for I := 0 to High(ListCopy) do begin Transfer := ListCopy[I]; // Maximale gleichzeitige Transfer Anzahl erreicht. // Transfer überspringen, wenn es kein priorisierter Transfer ist, // der Info Header schon gesendet wurde und kein Update // des Transfer Status gesendet werden muss if ((NormalCount >= FIOHandler.OutgoingTransferCountLimit) and (not Transfer.Priority)) and (Transfer.HeaderSent) and (not Transfer.PendingStatusUpdate) then Continue; // Transfer ist inaktiv if (Transfer.TransferState = tsSuspended) and (not Transfer.PendingStatusUpdate) then Continue; // Transfer ist abgeschlossen if (Transfer.TransferState = tsFinished) then Continue; // Transferzahl erhöhen if (not Transfer.Priority) then begin Inc(NormalCount); end; // anstehende Daten senden Transfer.SendNextPacketData; end; Sleep(1); end; end; Vor jeder Art von Daten wird ein TdxIDTPMainHeader gesendet, welcher die ID der Übertragung, die Größe des Aktuellen Blocks und weitere Flags beinhaltet. Beim Start eines Transfers enthält der erste Block den TdxIDTPInfoHeader. Dort enthalten sind Informationen, wie beispielsweise die Gesamtgröße der zu übertragenden Daten. Sind Meta Informationen angegeben, so werden auch diese direkt beim Start des Transfers an den Empfänger geschickt. Damit ist die Initialisierung abgeschlossen. Jetzt wird je nach eingestellter Priorisierung und maximaler Transfer Anzahl jeweils der nächste Datenblock gesendet, bis alle Übertragungen abgeschlossen sind. Die Zuordnung auf Empfängerseite erfolgt über die TransferID. Hier habe ich momentan ein statisches Array mit 1024 * 64 Elementen, in dem ich dann jeweils der Transfer ID entsprechend ein Objekt anlege, welches für das Sammeln der Daten zuständig ist. :arrow: Hier ist die zweite unschöne Sache. "Theoretisch" wäre es möglich, dass hier eine Art Überlauf stattfindet, wenn mehr als 2^16 Übertragungen gleichzeitig aktiv sind (oder zumindest in der Liste). Hier könnte ich zwar das Transfer ID Feld auf ein DWord erweitern. Das hätte aber zur Folge, dass bei jedem Datenpaket 2 zusätzliche Bytes gesendet werden und ich außerdem auf der Empfängerseite auf ein dynamisches Array umsteigen müsste (was von der Performance her sehr viel langsamer durchsucht und verwaltet werden kann). :arrow: Das dritte Problem liegt in der Verschlüsselung & Kompression begründet. Sind hier beispielsweise die Schlüssel unterschiedlich, bekommt der Decompressor Daten, mit denen er nichts anfangen kann, was zu einer Exception führt. Meine Frage ist nun, wie ich diese Exception am besten signalisieren soll? Über ein Event vielleicht? Außerdem müsste der Senderseite im Optimalfall gesagt werden, dass der Transfer abzubrechen ist. Ich hoffe ihr habt ein paar nützliche Ideen für mich. Das Protokoll werde ich selbstverständlich hier zur Verfügung stellen, sobald alles zu meiner Zufriedenheit funktioniert. Viele Grüße Zacherl |
AW: Konzept: Netzwerkprotokoll
Zitat:
Die anderen Dinge würde ich wie folgt angehen: Brauchst du wirklich mehr als 2^16 Verbindungen? Schon eine 2^16 Lookup-Table ist imho ziemlich groß. Dann würde ich vorschlagen, dass der Empfänger das erste Paket bestätigen muss ... damit gibt er bekannt, das er genug Ressourcen (und Fähigkeiten*) für diese Verbindung hat und das der Schlüssel richtig ist. Im ersten Paket gibt es bei verschlüsselten Verbindungen eine verschlüsselte Zufallszahl (Challenge), die der Empfänger entschlüsselt** wieder zurücksenden muss. Wichtig wäre dabei: Priorisierte Nachrichten sollten immer eine vorbereitete Verbindung haben. Wenn du die Zuordnungstabelle auf Empfängerseite besser verwalten willst, dann lass den Sender eine eigene Nummer zurücksenden, die der Sender ab jetzt benutzen soll. Der Empfänger meldet sich weiter mit der ursprünglichen ID an den Sender. Wenn der Sender oder Empfänger eine Verbindung (Chancel) abbrechen wollen, schicken sie ein Paket, das auch den Grund nennt (User/Fehler/...). Das kann er auch senden, wenn er die Verbindung nicht haben will. *Das gibt dir die Möglichkeit, eine light-Version des Protokoll zu benutzen, z.B. ohne Kompression. ** Achtung: Gute Verschlüsselung verwenden. |
AW: Konzept: Netzwerkprotokoll
Hallo BUG, danke für deine Antwort.
Zitat:
Zitat:
Zitat:
Zitat:
Zitat:
|
AW: Konzept: Netzwerkprotokoll
Zitat:
Wenn du kleine Datenmengen (HTTP) pro Verbindung gesendet hast, dann würde ich vermuten, das der Aufwand für eine neue Verbindung (Handshake, usw.) mit rein spielen könnte. Aber auch ein Key-Exchange nötigt dir ja mindestens eine Round-Trip-Time als Wartezeit auf. Mal so aus Interesse: Wo genau kommt denn das Protokoll ins Spiel? |
AW: Konzept: Netzwerkprotokoll
Zitat:
:?: Hat vielleicht noch jemand anders hiermit Erfahrungen gemacht? Zitat:
Mein zweiter Ansatz lief damals schon über eine frühe Version meines Protokolls, was ich hier zu optimieren versuche. Hierbei wurden die Websiten in jedem Fall deutlich schneller aufgerufen, als bei der Multi Socket Variante. Dort war aber auch kein Key Exchange enthalten, deshalb habe ich hier wieder keinen direkten Vergleich. |
AW: Konzept: Netzwerkprotokoll
Also ich habe mir dazu auch schon mal Gedanken gemacht und bin auf folgendes Protokoll gekommen:
Delphi-Quellcode:
Damit können bis zu 256 unabhängige Streams über die gleiche TCP-Verbindung gemultiplexed werden.
TMessageHeader = packed record
BlockSize: Word; // Größe des aktuellen Datenblocks (inkl. Header) StreamNo : Byte; // 0=Command Stream, 1..255=Data Streams Payload : Array[0..0] of Byte; // Nutzdaten end; Nach Aufbau der TCP/IP-Verbindung ist nur der Command-Stream (0) offen. Der Client sendet dann z.B. einen Befehl an den Server:
Code:
Der Server antwortet
SENDFILE test.dat
Code:
Die Daten werden dann blockweise mit StreamNo=2 übertragen.
ACK Stream 2
Zum Schluss wird eine Message ohne Payload geschickt um den Stream wieder zu schliesen. Der Charme dieses Protokolls ist seine Einfachheit. (das Protokoll im Command-Stream gehört aber nicht dazu. Das SENDFILE oben war nur ein Anwendungsbeispiel) Man kann es unabhängig vom Einsatzzweck benützen. |
AW: Konzept: Netzwerkprotokoll
Habe noch bis früh heute morgen am Protokoll gearbeitet und einige Ideen von hier wieder verworfen, geändert oder neue Sachen hinzugefügt. Das Senden und Empfangen ansich funktioniert nun schon überraschend gut. Fehlen nur noch einige Events, das automatische Sammeln der Daten, ein paar Fehlerkorrekturen und ausführliche Tests.
Meine Hauptpakete sind folgende:
Delphi-Quellcode:
Der Main Header enthält wie vorher die ID der Übertragung, die aktuelle Blockgröße und den Typ der folgenden Daten.
type
TdxIDTPPacketType = ( ptTransferInfo = 1, ptTransferData = 2, ptTransferStateChanged = 3, ptTransferStateCommand = 4 ); TdxIDTPMainHeader = packed record TransferID: Word; PacketSize: Word; PacketType: TdxIDTPPacketType; end; // Diesem Paket folgen direkt die Meta Daten, fals vorhanden PdxIDTP1Packet = ^TdxIDTP1Packet; TdxIDTP1Packet = packed record Magic: DWord; MetaSize: Word; DataSize: UInt64; BlockSize: TdxIDTPBlockSize; Priority: Boolean; Encrypted: Boolean; Compressed: Boolean; end; PdxIDTP2Packet = ^TdxIDTP2Packet; TdxIDTP2Packet = packed record Magic: DWord; TransferState: TdxIDTPTransferState; Reason: TdxIDTPTransferStateChangeReason; end; :arrow: ptTransferInfo ist praktisch das Initialisierungspaket für einen neuen Transfer. Es enthält das TdxIDTP1Packet gefolgt von eventuellen Meta Daten. Der Empfänger reagiert auf das Paket, indem es ein neues Transfer Objekt anlegt und in der LookupTable einträgt :arrow: ptTransferData enthält jeweils einen Datenblock eines Transfers. Der Empfänger prüft, ob der Transfer in der LookupTable vorhanden ist und akkumuliert die Daten. Wenn die TransferID nicht existiert, wird das Paket schlicht und einfach verworfen. :arrow: ptTransferStateChanged wird vom Sender geschickt, wenn der User eine Übertragung pausiert, fortsetzt oder abbricht. Der Empfänger ändert auf seiner Seite dann ebenfalls den Status des Transfers. Wenn die TransferID nicht existiert, wird auch hier das Paket einfach verworfen. :arrow: ptTransferStateCommand wird vom Empfänger an den Sender geschickt, wenn der User eine Übertragung pausiert, fortsetzt oder abbricht. Der Sender antwortet darauf mit einem ptTransferStateChanged Packet. Das Magic Feld in den Control Paketen beinhaltet einen festen Wert, welcher nach der Entschlüsselung geprüft wird. Ist der Wert falsch, kann davon ausgegangen werden, dass unterschiedliche Verschlüsselungroutinen oder Schlüssel zum Einsatz kommen. Die Pakete werden dann auf Seite des Empfängers verworfen (hier könnte man sich eventuell noch etwas überlegen, um den Sender zu informieren). Die Ping Pong Variante beim Start eines Transfers habe ich komplett verworfen. Ausgehend davon, dass die LookupTabelle auf Sender und Empfängerseite eigentlich immer synchronisiert sein sollte, kann der Sender bereits feststellen, ob bereits 2^16 Transfers laufen. Die Funktion zum Ermitteln der nächsten freien TransferID ist folgender:
Delphi-Quellcode:
Schlägt die Funktion fehl, wird eine Exception geschmissen.
function TdxIDTPIOHandler.SearchNextTransferID(var TransferID: Word): Boolean;
var I: Word; begin Result := false; if (FLastTransferID = MAXWORD) then FLastTransferID := 0; for I := FLastTransferID to High(FOLookupTable) do begin if not Assigned(FOLookupTable[I]) then begin TransferID := I; Result := true; FLastTransferID := TransferID; Exit; end; end; for I := Low(FOLookupTable) to FLastTransferID do begin if not Assigned(FOLookupTable[I]) then begin TransferID := I; Result := true; FLastTransferID := TransferID; Break; end; end; end; :?: Was sagt ihr zur bisherigen Umsetzung? Auf den ersten Blick scheint mir das Protokoll recht stabil zu funktionieren. Seht ihr noch irgendwelche Sachen, die extrem umgeschickt gelöst sind? |
AW: Konzept: Netzwerkprotokoll
Wenn Du mit 2^16 Übertragungen rechnest, solltest Du deine Lookuptabelle nicht als unsortiertes Array konzipieren. Verwende lieber doppelt-verkettete Liste für die freigegebenen Sende-IDs.
Delphi-Quellcode:
So ist der Aufwand immer O(1), anstatt O(n) bei deiner Variante.
Function GetNewID : Integer;
Begin If FreeList.IsEmpty Then begin If HighestID = MAXWORD Then Raise Exception....; HighestID := HighestID + 1; Result := HighestID; End else Begin Result := FreeList.First; FreeList.RemoveFirstElement; End; End; Procedure DiscardUsedID (aIDWhichIsNoLongerInUse : Integer); Begin FreeList.InsertAtFront(aIDWhichIsNoLongerInUse); End; |
AW: Konzept: Netzwerkprotokoll
Auf die Idee eine verkettete Liste zu verwenden, bin ich gar nicht gekommen. Werde ich direkt umsetzen, danke dir :)
|
AW: Konzept: Netzwerkprotokoll
Ach ja, und wenn Du ein Speicherfetischist bist, kannst Du 'HighestID' auch wieder dekrementieren, solange der Wert 'HighestID' in der FreeList enthalten ist. Das allerdings geht auf Kosten der Performance, aber nur im Extremfall, wenn man nämlich sehr viele IDs anfordert und sie in umgekehrter Reihenfolge wieder freigibt.
|
Alle Zeitangaben in WEZ +1. Es ist jetzt 12:57 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz