Heiner Kücker

Remote Protocol

Home

Java-Seite

   ASM Improved

   heterogene
   Map, HMap

   Constraint
   Code Generator

   JSP WorkFlow
   PageFlow FlowControl
   Page Flow Engine
   Web Flow Engine
   Control_and_Command

   JSP_Spreadsheet

   Code-Generator
   für Option-Either-Stil
   in Java

   verbesserter
   Comparator

   Fluent-Interface
   Code-Generator
   auf Basis
   einer Grammatik

   Visitor mit Multidispatch

   for-Schleife mit
   yield-return

   Kognitions-Maschine
   semantisches Netz

   Domain Parser

   Codegenerator_für
   hierarchische
   Datenstrukturen

   Expression_Engine
   Formula_Parser

   Thread Preprocessor

   State Transition Engine

   AspectJ

   Java_Explorer

   DBF_Library

   Kalender_Applet

   SetGetGen

   BeanSetGet

   CheckPackage

   LineNumbers

   GradDms

   Excel-Export

   StringTokenizer

   JspDoc

   JspCheck

   JSP-Schulung
   Java Server Pages
   Struts

   Ascii-Tabellen-
   Layouter

   Ascii-Baum-
   Layouter

   Ascii-Art-Fluss-
   Diagramm-
   Parser

   AsciiArt
   AssignmentMatrix
   Layouter

   StringSerial

   Silbentrennung

   JDBC_Schlüssel-
   Generierung

   bidirektional/
   unidirektional
   gelinkte Liste

   Java_Sitemap
   Generator

   XmlBuilder

   RangeMap

   StringFormatter

   VersionSafe
   XCopy

   JTextField

   CommandLine-
   ParamReader

   Bitmap-Grafik

   MultiMarkable-
   Buffered-
   InputStream

   JavaCache

   JdomUtil

   CollectionUtil

   XML Really
   Pull Parser

   Log-Filter

   Remote-Protokoll

   Sudoku-Generator

   Delegation statt
   Mehrfachvererbung

   Disjunct
   Interval Set

   WebCam_Demo

   Weiterentwicklung_Java

Alaska-XBase++-Seite

Projekte

Philosophien
Techniken


Konzepte

Sudoku

Kontakt /
Impressum


Links

SiteMap





Letzte Aktualisierung:
10.05.2007
Remote Protocol Serialisierung und Deserialisierung von Java-Objekten

Serialisierung und Deserialisierung von Java-Objekten zur Übertragung über Socket mit Streams oder Java-NIO.
Das Remote Client-Server-Protokoll
----------------------------------

Ziel ist es, ein performantes Übertragungsprotokoll zwischen dem
einem Remote Client und Server festzulegen. Dabei soll auch ein
Server, der in PHP geschrieben wurde, unterstützt werden.

Allgemeines
-----------

Die Standard-Serialisierung/-Deserialsierung in Java erzeugt sehr
grosse Datenmengen. So wird zum Beispiel ein java.util.Date in 82
Byte serialsiert, wobei nur 8 Byte (das interne long time)
Nutzdaten sind.

Durch das hier gezeigte Protokoll soll diese Datenmenge reduziert
werden, zum Beispiel soll ein java.util.Date nur zu seinen 8 Byte
Nutzlast und zusätzlich einem Byte für die Erkennung des
Datentyps serialsiert werden.

TODO

Ein Datum ohne Zeitangabe könnte sogar in 3 Byte Nutzlast
serialsiert werden, 5 Bit für den Tag im Monat (1 bis 31), 4 Bit
für den Monat (1 bis 12). Dann bleiben noch 15 Bit für das Jahr,
was den Bereich von 0 bis 32768 bzw von -16383 bis -16384
erlaubt. Dies sollte für alle kaufmännischen Probleme reichen.
Ich verstehe also das Jahr-2000-Problem nicht, es hätte nicht
auftreten müssen.

Zum Test des Protokolls habe ich einen Java-NIO-Server
(ProtocolServerNonBlocked) und einen Client (ProtocolClient)
geschrieben.

Dieser Code kam nie produktiv zum Einsatz und wurde nie intensiv
getestet.


Der Protokoll-Umschlag
----------------------

Da das TCP/IP-Protokoll keine Information liefert,
wann ein gesendeter Datenblock vollständig ist,
muss eine Längen-Information mitgesendet werden.

Die ersten vier Byte der Nachricht stellen die Längen-Information
dar. Das höchstwertigste Byte wird als erstes gesendet.

Dadurch ist die Grösse eines Datenblockes auf 4 GigaByte
begrenzt, was ausreichen sollte.

Nach den vier Byte für die Länge werden zwei
Byte für die gewünschte Operation gesendet.

Diese beiden Bytes gehören schon zur Gesamt-Information
und werden bei der Länge mitgezählt.

Das erlaubt es 65536 Operationen zu definieren.

In den Client- und Server-Programmen werden diese
Nummern auf benannte Operationen gemappt.

Nach dem Operations-Code folgt ein Byte mit der
Anzahl der Parameter.

Dadurch ist die Anzahl der Parameter auf 255 beschränkt.

Durch die Angabe der Parameter-Anzahl ist es möglich,
die gleiche Operation mit unterschiedlichen Parametern
aufzurufen.

Werden mehr Parameter benötigt, müssen diese in
Arrays verpackt werden.

Achtung: das höchstwertigste Bit der Parameter-Anzahl wird als
Signal benutzt, ob die gesamten Parameter komprimiert sind (GZIP).
Negative Parameter-Anzahlen gelten als komprimiert.
Dadurch reduziert sich die Anzahl möglicher Parameter auf 127.
Diese Lösung ist notwendig, weil sich mit einem GZIP-Umschlag nur
ein einzelner Parameter komprimieren lässt.

Danach folgen die Parameter der Operation.

Beim Response wird der Operations-Code zurückgesendet.
Dies ist zwar redundant, wird aber bei evtl. später
zu implementierenden asynchronen Antworten benötigt.

Die Response enthält ebenfalls ein Byte für die Anzahl der
Rückgabe-Werte mit dem höchstwertigen Bit als Markierung für
die Komprimierung der Rückgabe-Werte.

Request:
                      4 - 5              6 - 9               10 - 11                12                       13 -
                      0 - 1              2 - 5                6 -  7                 8                        9
+------------------+-------------------+-------------------+----------------------+------------------------+--------------------+
| Byte 0 - 3 Länge | 2 Byte Request-ID | 4 Byte Session-ID | Byte 10 - 11 OP-Code | Byte 6 Parameter-Anzahl |   Parameter-Daten  |
+------------------+-------------------+-------------------+----------------------+------------------------+--------------------+
                   |<--------------------------------------------- Länge des Request ------------------------------------------>|

Response:
                      4 - 5              6 - 9               10 - 11                12                            13 -
                      0 - 1              2 - 5                6 -  7                 8                             9
+------------------+-------------------+-------------------+----------------------+------------------------------+--------------------+
| Byte 0 - 3 Länge | 2 Byte Request-ID | 4 Byte Session-ID | Byte 10 - 11 OP-Code | Byte 6 Rückgabe-Werte-Anzahl |   Rückgabe-Daten   |
+------------------+-------------------+-------------------+----------------------+------------------------------+--------------------+
                   |<---------------------------------------------- Länge der Response ---------------------------------------------->|


2 Byte für eine Request-ID TODO
Nachfrage: Session-Id 4 Byte im Header mitsenden ? (Kann für anonyme Request's ausgenullt werden)

Der gesamte Request und Response wird verschlüsselt.

Bei jedem Request und Response wird der positionsabhängige
Verschlüsseler neu gestartet, um ein aus dem Takt
(Synchronisation) laufen der Ver- und Entschlüsseler zu
vermeiden.

Offene Frage: soll die Längen-Information sowie Request-ID und Session-ID mit verschlüsselt werden ?

Bei Bekanntsein dieser Tatsache könnte dadurch das Knacken
der Verschlüsselung erleichtert werden.


Verwendung der Serialisierungs-Formate für die Persistierung
------------------------------------------------------------

Die Serialisierungs-Formate sind unverändert zum Abspeichern
von Konfigurationsdaten oder temporär zu persistierender
Daten (persistenter Client-Cache) geeignet.

Dabei ist aber immer eine serielle Arbeitsweise
erforderlich. Für die Arbeit mit wahlfreiem Zugriff zur RAM-
Einsparung müssen alternative Formate mit festen Längen
verwendet werden (oder besser eine Embedded-DB wie Derby oder
db4o).

Zum Absichern der Versions-Migration (zusätzliche Felder bei
neuen Programm-Versionen) ist ein Versions-Header erforderlich.

Dazu können weitere fachliche Informationen (Kopf-
Informationen like-DBASE oder Inhalts-Informationen
(Datentyp semantisch) ) kommen.

Das Abspeichern der Body-Length ist beim Persistieren im
Datei-System nicht nötig.

+-------------------------------------------------+-------------+
| Versions-Nummer (2 Byte, höchstwertiges zuerst) | Daten-Bytes |
+-------------------------------------------------+-------------+


Datentypen
==========

Jeder Parameter- und Rückgabewert wird durch das erste Byte
in seinem Type identifiziert.

Bei Notwendigkeit wird dieser Type durch weitere Bytes
genauer spezifiert.


Typen mit implizitem Wert
-------------------------

Diese Typen führen keinen Wert mit, da
der Wert im Typ schon enthalten ist.

n - Null  (char 110)
t - true  (char 116)
f - false (char 102)


Elementare Datentypen
---------------------

-Byte vorzeichenbehaftete 8-Bit-Ganzzahl

+-------------+------------+
| b (char 98) | Daten-Byte |
+-------------+------------+


-Short vorzeichenbehaftete 16-Bit-Ganzzahl

+--------------+---------------------+-----------------------+
| s (char 115) | Höchstwertiges Byte | Niederwertigstes Byte |
+--------------+---------------------+-----------------------+


-Integer:

32-bit Integer

+--------------+---------------------+--------+--------+-----------------------+
| i (char 105) | Höchstwertiges Byte | Byte 1 | Byte 2 | Niederwertigstes Byte |
+--------------+---------------------+--------+--------+-----------------------+

-Long:

64-bit Long

+--------------+---------------------+-----------------+-----------------------+
| l (char 108) | Höchstwertiges Byte | Byte 1 - Byte 6 | Niederwertigstes Byte |
+--------------+---------------------+-----------------+-----------------------+

-komprimiertes Short, komprimiertes Integer und komprimiertes Long

Es ist möglich, dass der Wert eines int in den Wertebereich
von byte oder short passt.

Es ist möglich, dass der Wert eines long in den Wertebereich
von byte, int oder short passt.

Für diesen Fall werden nur die jeweils notwendigen Bytes
serialisiert.

Beim Deserialisieren muss aber wieder der originale Typ
bereitgestellt werden, um Probleme mit unterschiedlichen
Klassen (ClassCastException, ArrayStoreException,
Fehlschlagen von instanceof) zu vermeiden.

  Typ  | Speicher-Typ | Prefix-Code
-------+--------------+-------------
 short | byte   (8)   |     1
 int   | byte   (8)   |     2
 int   | short (16)   |     3
 long  | byte   (8)   |     4
 long  | short (16)   |     5
 long  | int   (32)   |     6

-Float 4-Byte (32 Bit) Gleitkommazahl:

Code 'F', weil 'f' schon für false verwendet wird

+--------------+---------------------------------------------------+
| F (char 70)  | 4 Byte im IEEE 754 floating-point "single format" |
+--------------+---------------------------------------------------+

-Double 8-Byte (64 Bit) Gleitkommazahl:

+--------------+------------------------------------------+
| d (char 100) | 8 Byte im IEEE 754 floating-point format |
+--------------+------------------------------------------+

Konvertierung in Java
  Double.longBitsToDouble(long bits)
  Double.doubleToLongBits(double value)

Konvertierung in PHP
 pack( "d",  )
 unpack( "d" ,  )

TODO BigDecimal

-komprimiertes Double wird nicht implementiert wegen evtl. Genauigkeitsverlust

-String:

Die übergebenen Unicode-Strings werden in UTF-8 konvertiert.

+-------------+---------------------------------------------------+-------------------+
| S (char 83) | 4 Byte für die Länge (höchstwertiges Byte zuerst) |    Daten-Bytes    |
+-------------+---------------------------------------------------+-------------------+
                                                                  |<----- Länge ----->|


wenn UTF-8 Länge <= Short.MAX_VALUE (32767)
+--------+---------------------------------------------------+-------------------+
| char 7 | 2 Byte für die Länge (höchstwertiges Byte zuerst) |    Daten-Bytes    |
+--------+---------------------------------------------------+-------------------+
                                                             |<----- Länge ----->|

wenn UTF-8 Länge <= Byte.MAX_VALUE (128)
+--------+----------------------+-------------------+
| char 8 | 1 Byte für die Länge |    Daten-Bytes    |
+--------+----------------------+-------------------+
                                |<----- Länge ----->|

TODO String und alle anderen Typen mit Längenangabe noch mal mit jeweils anderem Code für 2-Byte Längenangabe codieren

-Binärdaten

Binäre Daten werden als Byte-Array ausgetauscht

+-------------+---------------------------------------------------+-------------------+
| B (char 66) | 4 Byte für die Länge (höchstwertiges Byte zuerst) |    Daten-Bytes    |
+-------------+---------------------------------------------------+-------------------+
                                                                  |<----- Länge ----->|

-Datum

Das java.util.Date wird als long-value (8 Byte) in
Millisekunden ab dem 01.01.1970 übertragen.

+-------------+--------------------------------------------------+
| D (char 68) | 8 Byte für die Zeit (höchstwertiges Byte zuerst) |
+-------------+--------------------------------------------------+

Komplexe Datentypen
-------------------

-Arrays ungetypt

In Java wird hier der Type ArrayList verwendet, in PHP numerisches Array.

+-------------+------------------------------------+-------------------+
| a (char 97) | 4 Byte für die Anzahl der Elemente |    Daten-Bytes    |
+-------------+------------------------------------+-------------------+

TODO String-Array (ein- und zweidimensional), int-Array, long-Array, double-Array, Date-Array, Array-Array, Map-Array

-Arrays getypt

Die Verwendung von typisierten Arrays ist im Zusammenhang
mit Objekten sinnvoll. Dabei wird der Typ (Objekt-Klasse)
nur einmal im Kopf angegeben und muss nicht für jedes Array-
Element festgelegt (übergeben) werden, was Platz für die
Typinformation spart.

+-------------+-------------------------------------------+------------------------------------+-------------------+
| A (char 65) | 2 Byte Class-ID-Number (siehe bei Objekt) | 4 Byte für die Anzahl der Elemente |    Daten-Bytes    |
+-------------+-------------------------------------------+------------------------------------+-------------------+

TODO

-Objekte (Struct, Record)

Die Klasse des Objektes wird durch eine 2 Byte Nummer
identifiziert.

Zum Serialisieren muss jede der Remote-Klassen das Interface
RemotableInterface implementieren.

Dieses Interface fordert eine Methode

  short getClassId()

zum Serialisieren eine Methode

  byte[] serialize()

zum Deserialiseren einen parameterlosen Konstruktor
und die Methode

  void deserialize(InByteBuffer)

Auf der Empfängerseite wird im Code ein Switch-Block implementiert

  switch classId
  {
    case FILE_USE_NET_CLASS_ID:
      retObject = new FileUseNet();
      retObject.deserialize(inByteBuffer);
      break;
    case ...
  }

Dadurch muss nicht die uneffektive Standard-Serialisierung
von Java verwendet werden. Ausserdem wird die Übertragung
des evtl. langen Klassen-Namens mit Package gespart.

Ausserdem erhöht sich der Grad der Obfuscation der
übermittelten Daten.

+-------------+------------------------+-------------+
| O (char 79) | 2 Byte Class-ID-Number | Daten-Bytes |
+-------------+------------------------+-------------+

Im Remote-Client ist damit zu rechnen, dass die Anzahl der
Klassen auf jeden Fall mit 2 Byte abgedeckt werden können.
Für Projekte mit vielen Use-Cases ist es sinnvoll, die
Class-ID in eine Major- und eine Minor-Number, bzw. mit
einem dritten Byte in drei Unterteilungsstufen, zu
unterteilen und je Byte separate switch-Verteiler zu
implementieren. Dadurch wird das unbegrenzte Wachstum einer
Methode zum Um-Mappen der Class-ID auf die jeweilige
Implementations-Klasse eingedämmt. Dazu kommt der Vorteil,
dass eventuell nicht alle Klassen geladen werden müssen, was
die Performance beim Start und während der Laufzeit der
Applikation verbessert (Prüfen, ob das überhaupt stimmt).

TODO

-Map (PHP: assoziatives Array)

+-------------+------------------------------------+-------------------+
| M (char 77) | 4 Byte für die Anzahl der Elemente |    Daten-Bytes    |
+-------------+------------------------------------+-------------------+

Jedes Element einer Map besteht aus einem Schlüssel und einem Wert.

TODO Map getypt (Typ für Key und Value separat) einfach (String, int, long, double ) oder auf Objekt getypt

-Tabelle

Tabellen bestehen aus benannten Spalten und einer bestimmten Anzahl Zeilen.

+-------------+-----------------------------+----------------------------------+-------------------+
| T (char 84) | Array mit den Spalten-Namen | 4 Byte für die Anzahl der Zeilen |    Daten-Bytes    |
+-------------+-----------------------------+----------------------------------+-------------------+


-Exception

Spezieller Datentyp zur Übermittlung von Fehlern.

+-------------+---------------------------------+----------------------------------------+----------------+---------------------------+
| E (char 69) | 1 Byte Anzahl nested Exceptions | String Exception-Klasse (Name+Package) | String Message | String Stacktrace (evtl.) |
+-------------+---------------------------------+----------------------------------------+----------------+---------------------------+
                                                |<---------------------------- jeweils für jede nested Exception -------------------->|


GZip-Umschläge
--------------

In einem GZip-Umschlag kann sich wieder eine einzelne
beliebige andere Variable (Parameter) befinden.

Der Erzeuger des Formates kann bestimmte einzelne Variablen
(Parameter) in einen GZip-Umschlag packen. Sollen meherere
Variablen komprimiert werden, müsen diese in einen komplexen
Parameter (Array, Map) verpackt werden.

Für den Empfänger ist der GZip-Umschlag transparent, weil
der Deserialisierer automatisch das Auspacken erledigt.

Alternativ zur Verwendung eines GZip-Umschlages für einen
einzelnen Parameter kann auch der gesamte Parameter-Block
komprimiert werden. Siehe höchstwertigstes Bit der
Parameter-Anzahl.

+-------------+---------------------------------------------------+-------------------+
| G (char 71) | 4 Byte für die Länge (höchstwertiges Byte zuerst) |    Daten-Bytes    |
+-------------+---------------------------------------------------+-------------------+
                                                                  |<----- Länge ----->|

Im Anwendungscode in Java wird das zu komprimierende Objekt
in ein GzipHolder-Objekt verpackt.

  GzipHolder gzipHolder = new GZipHolder( object );

Dadurch kann die if-instanceof-Kaskade erkennen, dass der
entsprechende Parameter komprimiert werden soll.


zusätzliche Anforderung: Verschlüsselungs-Umschlag
--------------------------------------------------

Varianten:

1. Passwort beim Login übermitteln, verschlüsselt mit Standard-Algorithmus

2. Passwort vom letzten mal verwenden um das neue Passwort zu entschlüsseln (persistieren)

3. Doch lieber Handshake

es muss eine Möglichkeit geben, in verschlüsselten Umschlägen das Passwort zu ändern.

Beim Ändern des Passwortes im Applikationsablauf muss darauf
geachtet werden, dass bei asynchroner Arbeitsweise ein
definierter Wechsel erfolgt.

Bereits gestartete Dialog-Transaktionen benutzen noch das
alte Passwort. Alle neu gestarteten Dialoge arbeiten mit den
neuen Passwort.

Im Client wird das Passwort am Start der Dialog-Transaktion
vermerkt und bis zum Ende benutzt. Der Server muss die
Dialog-Transaktion anhand der Request-ID identifizieren und
das entsprechende Passwort benutzen.



Download der Quelldateien REMOTE_PROTOCOL.zip


Installation:

Anlegen eines Projektes in der IDE Ihrer Wahl.

Start mit ProtocolServerBlocked#main bzw. ProtocolServerNonBlocked#main bzw. TestServer#main (Server unbedingt immer zuerst starten) und dananch ProtocolClient#main bzw. TestClient#main.

Achtung: Erweiterungen und Fixes stelle ich ohne Historie und ohne Ankündigung hier bereit.
Deshalb am besten immer die letzte Version runterladen.

Lizenzbedingungen:

Die Programme, Quelltexte und Dokumentationen können ohne irgendwelche Bedingungen kostenlos verwendet werden.
Sie sind Freeware und Open Source. Für Fehler und Folgen wird keinerlei Haftung übernommen.

Hinweise zur Fehlerbeseitigung und Verbesserung sind mir willkommen.

Ich freue mich auch über Feedback bezüglich der erfolgreichen Verwendung meiner Sourcen.

Bei Fragen helfe ich gern mit Hinweisen oder zusätzlicher Dokumentation, falls ich dafür Zeit habe.