HTTP Request Parameter als primitive und als abstrakte Datentypen

Die Typisierung ist eine Grundvoraussetzung für den wahlfreien Zugriff und dafür dass Daten gespeichert sowie transportiert werden können

HTTP kennt keine Datentypen. Es ist jedoch möglich sowohl primitive als auch abstrakte Datentypen typegerecht per HTTP zu transportieren. Wie das funktioniert, zeigt dieser Artikel. Des Weiteren sind hier Demos eingebaut welche die Anwendung theoretischer Grundlagen praktisch zeigen:

Bevor Sie jedoch die Beispiele ausproblieren: Stellen Sie sich vor, sie erfassen die Altersangaben einer mittleren Kleinstadt mit 10T Einwohnern, schreiben diese Angaben in dezimaler Darstellung hintereinander weg in eine Datei und schicken diese Datei an das Einwohnermeldeamt damit die das Durchschnittsalter, das Jüngste und das Älteste daraus berechnen.

Nun, der Informationsgehalt ist vorhanden, aber die Aufgabe ist nicht lösbar, weil es Altersangaben mit 1, 2 oder 3 Ziffern gibt: Eine Wiederherstellung der einzelnen Altersangaben ist nicht möglich!

Wurden jedoch die Zahlen typisiert übertragen, kann jede einzelne Zahl wiederhergestellt werden. Eine Typisierung nämlich, legt den Wertebereich einer Zahl fest. Z.B. von 0..255. Und damit hat jede Zahl eine Länge von genau einem Byte (innerhalb dieses Artikels wird unter einem Byte die Oktette verstanden).

Das ist der eigentliche Sinn der Typisierung: Den Wertebereich festlegen. Das Byte als kleinste physikalische Speichereinheit in einer Datei transportiert einen primitiven Datentyp: Einen 8 Bit Integer den es auf jedem System gibt.

Typisierung schränkt den Wertebereich einer Variable ein und legt die Anzahl der Oktetten/Bytes fest die zum Speichern bzw. für den Transport benötigt werden. Umgekehrt legen zum Beispiel 2 Bytes den Wertebereich auf 0xFFFF fest.

Genauso jedoch, wie es möglich ist dieses eine Byte in eine Datei zu schreiben, kann es selbstverständlich auch in ein Socket geschrieben werden: Sprich per HTTP übertragen werden. Somit ist eine Typisierung auch plattformübergreifend möglich, was die folgenden Beispiele zeigen werden.

Typisierte Übertragung eines vorzeichenlosen Integer

Betrachte untenstehenden Code:

JavaScript:

    var buffer = new ArrayBuffer(1);
    var dv = new DataView( buffer );

    /* Diese Zahl wird gesendet */
    dv.setUint8(0, 131);


    fetch("/typisierung.html?zahl=1",{
        headers: new Headers({
            'Content-Type': 'application/body+query'
        }),
        method: 'POST',
        body: buffer
    }).then( function(response){
        response.text().then( function(msg){  console.log(msg) } )
    });

Serverseitig (Perl):
    # Daten aus STDIN
    # Schablone C implementiert einen vorzeichenlosen
    # 8 Bit Integer Uint8
    $self->{CONTENT} = unpack("C", $self->{CGI}->rawdata());

Erläuterung: Über ein DataView-Objekt wird die Zahl 131 in einen ArrayBuffer mit der Länge 1 gesetzt. Diese Länge entspricht genau einem Byte und mittels fetch() wird dieser ArrayBuffer per POST gesendet. FetchAPI setzt spontan den Request-Header Content-Length: 1 also mit der richtigen Länge des ArrayBuffer. Serverseitig wird die Binary aus STDIN gelesen und über die pack()-Schablone "C" die Zahl in einer lesbaren Form zurückgesendet. Letztere ist dann in der Console sichtbar.

Objekt like c-struct in einer Response

Für das nächste Beispiel soll ein Objekt übertragen werden, was vom Aufbau her einem Struct in c entspricht:

typedef struct{
    uint8_t age;
    char vname[50];
    char name[50];
} Person;

Mit Perl lässt sich ein solches Struct wie folgt in eine Byte-Sequenz serialisieren:

$self->{CONTENT} = pack("CZ50Z50", 68, "Fritz", "Müller");

Nun wird mit JavaScript/Ajax dieses Struct als Binary angefordert und daraus die Daten wiederhergestellt:

function getstruct(){
    var xhr = new XMLHttpRequest();
    xhr.open('GET',"/typisierung.html?getstruct=1");
    xhr.responseType = "arraybuffer";
    xhr.onload = function(){
        var dv = new DataView( this.response );
        var age = dv.getUint8(0);
        var vname = dv.getString(1,50);
        var name  = dv.getString(51,50);

        pretext("Alter: "+age+"\nVorname: "+vname+"\nName: "+name);
    };
    xhr.send();
}

Und das wird ausgegeben:
 Alter: 68
 Vorname: Fritz
 Name: Müller

Erläuterung: Die Perlfunktion pack() erzeugt mit der entsprechenden Schablone eine Binärsequenz welche die Daten enthält. Diese Binary hat genau eine Länge von 101 Byte, wobei für die Angabe des Alters genau ein Byte benötigt wird (C-Schablone). Für die Strings Vorname, Name werden jeweils 50 Byte reserviert wobei infolge der Z-Schablone nicht benötigte Platzhalter-Bytes mit Nullen augefüllt werden (null padded). Damit sind diese Strings auch nullterminiert, was für die Ausgabe wichtig ist.

Browserseitig wird aus dem ArrayBuffer ein DataView erzeugt. Damit die dem Datentype entsprechenden Methoden getUint8() und getString() angewandt werden können, ist die JS Library StringView.js (MDN) einzubinden.

Weitere Datentypen siehe MDN Dokumentation zu typed Arrays und DataView. Diesen Datentypen entsprechende Schablonen mit denen die Binary erzeugt und auch gelesen werden kann, zeigt perldoc -f pack also die Dokumentation zu Perls Funktion pack(). In PHP sind diese Schablonen gleichermaßen verwendbar.

Mehrere Objekte lassen sich auf diese Art und Weise bytegenau (binary safe) übertragen indem sie einfach aneinandergehängt werden. Maßgeblich zur Wiederherstellung der Daten ist, daß alle Objekte dieselbe Länge haben.

POST Request mit Daten like c-struct

Im nächsten Beispiel werden Daten im Browser erhoben und vom Aufbau her dasselbe Struct gesendet was im vorherigen Beispiel verwendet wurde. Hierzu wir ein ArrayBuffer mit der Länge von 101 erstellt was einer Binary mit ebendieser Länge in Bytes entspricht. Über ein DataView und den Methoden dvsetUint8) und dv.setString() werden anschließend die Daten in diesen Buffer gesetzt und per POST gesendet:

    var buffer = new ArrayBuffer(101);
    var dv = new DataView(buffer);
    dv.setUint8(0, 68);
    dv.setString(1, "Fritz");
    dv.setString(51, "Müller");
    var xhr = new XMLHttpRequest();
    xhr.open('POST', "/typisierung.html?setstruct=1");
    xhr.setRequestHeader("Content-Type","application/body+query");

    xhr.onload = function(){
        pretext(this.response);
    };

    xhr.send(buffer);

Serverseitig wird die Binaray aus STDIN gelesen und die Daten anhand der verwendeten Schablone wiederhergestellt, das ist an Einfachheit nicht zu übertreffen:

$self->{CONTENT} = join "\n", unpack("CZ50Z50", $self->{CGI}->rawdata() );

Ausgabe im Browser:
  68
  Fritz
  Müller

Anmerkung: Das null padding erfolgt insofern automatisch als daß beim Anlegen des ArrayBuffer dieser ja mit Nullen vorbelegt wird.

Ein auf diese Art und Weise übertragener Datenstrom beeinhaltet einen abstrakten Datentyp, also serialisierte Daten und kann unverändert in einer Datei persistent gemacht sowie aus dieser wiederhergestellt werden.

Datentypen sämtlicher Programmiersprachen sind portierbar

Neben den Typen für ganze Zahlen sowohl mit als auch ohne Vorzeichen gibt es in jeder Programmiersprache noch die sog. Gleitkommazahlen, z.B. Float32 in JavaScript, JAVA, c usw. und in Perl, PHP gibt es die passenden Schablonen zur Binary. Damit sind alldiese Datentypen auch transportfähig. Ebenso gibt es den Begriff der Byteorder (Endianness) plattformübergreifend, was bei einer Kodierung > 1 Byte von Interesse ist.

Die Typisierung ist eine Grundvoraussetzung dafür, daß Daten transportiert werden können! Daß heißt andersherum ausgedrückt, daß Daten portierbar sind weil sie typisiert sind.

Abstrakte Datentypen like c-verkettete Listen

Wie bereits festgestellt, ein c-struct definiert nicht nur einen abstrakten Datentyp sondern stellt auch die Abdeckung des Speicherbedarfs für einen Solchen sicher wobei der Speicherbedarf derart zusammengefasster Daten in einer Bytesequenz 1:1 derselbe ist wie im Hauptspeicher. Aneinandergehängt in einer Bytesequenz ist die Länge dieser ein ganzzahliges Vielfaches von dem eines einzelnen Structs.

Das Beispiel zum Ausprobieren, siehe Formular untenstehend, die Daten einer jeden Person sind jeweils in einem Struct zusammengefasst:

Ehemann::
Ehefrau:

Diese Datenabstraktion, jede Person ist ein eigenes Struct, führt sowohl physikalisch als auch logisch sowie programmiertechnisch zu einer besseren Trennung der Datenobjekte, was mit den herkömmlichen Content-Types application/x-www-form-urlencoded und multipart/form-data gar nicht möglich ist!

Erläuterungen zur Anwendung

Clientseitiger Code, zur Anwendung kommt das bisher Gesagte, dem struct hinzugefügt wurde ein weiterer Datentyp Float32 für die Körpergröße einer Person. Erfasst und gesendet werden die Daten für 2 Personen, hierzu werden aus den Binaries Blobs gebildet die schließlich zum Senden aneinandergehängt werden. Somit ergibt sich für den Request eine Content-Length: 210.

function senden(){
    var personen = ['A','B'];
    var blobs = [];
    personen.forEach( function(p){
        var buffer = new ArrayBuffer(50+50+1+4);
        // 105 Bytes je Person
        var dv = new DataView(buffer);
        var name = document.getElementById("name"+p).value;
        var vname = document.getElementById("vname"+p).value;
        var age = document.getElementById("age"+p).value;
        var len = document.getElementById("len"+p).value;

        dv.setString(0, name);
        dv.setString(50, vname);
        dv.setUint8(100, age);
        dv.setFloat32(101, len, 1);
        blobs.push(new Blob([buffer]));
        //console.log(name, vname, age, len);
    });

    var xhr = new XMLHttpRequest();
    xhr.open('POST',"/typisierung.html?personen=1");
    xhr.setRequestHeader("Content-Type","application/body+query");

    xhr.onload = function(){
        pretext(this.response);
    };

    xhr.send( new Blob(blobs) );
}

Serverseitiger Code. Wichtig ist auch hierbei, daß die dem serialisierten Datentyp entsprechende Schablone eingesetzt wird. Jeder einer Person entsprechende Datensatz hat eine fest vorgegebene Länge von 105 Bytes und dieser Längenangabe entsprechend werden die Daten auch aus STDIN gelesen.

    elsif($self->param('personen')){
        my @personen = ();
        # Lese die Binary aus STDIN
        # Jeder Record hat eine Länge von 105 Bytes
        while( read($self->{CGI}{STDIN}, my $buffer, 105) ){
            push @personen, unpack "Z50Z50Cf", $buffer;
        }

        # Response
        $self->{CONTENT} = join "\n", @personen;
    }

Hinweis: STDIN und STDOUT sind die Standards für Ein/Ausgab (IO). In diesen Handles, die Filehandles ebenbürdig sind, wird mit Bytes operiert. Wie weiter oben schon festgestellt, können derart serialisierte Daten ohne Weiteres in Dateien gespeichert und aus diesen wiederhergestellt werden. Wobei die Binary, d.h., die zu sendende Datei nicht erst auf dem Server sondern bereits im Browser erstellt wird. Die Übertragung per HTTP ist damit transparent und die Daten typegerecht, vollumfänglich und plattformübergreifend portierbar.

Warum überhaupt eine Typisierung

Typisierung ist eine Voraussetzung dafür daß Daten überhaupt transportiert und gespeichert werden können. Das betrifft insbesondere strukturierte Daten, also Daten die in Feldern, Records oder Tupel gespeichert oder transportiert werden sollen. Diese Datenfelder müssen eine dem Programm bekannte Länge haben sonst ist eine Wiederherstellung der Inhalte nämlich gar nicht möglich. So hat im letzten Beispiel jeder Record für eine Person stets eine Länge von 105 Bytes und ist in Felder unterteilt, für die ebenfalls eine ganz bestimmte Länge festgelegt wurde. Dieser Sachverhalt ist schließlich nicht nur für die Wiederherstellung der Daten wichtig sondern auch für den wahlfreien Zugriff auf Diese (Random Access).

Wird zum Beispiel eine Datei durchsucht um ein bestimmtes Zeichen zu finden, muss der Algorithmus hierzu die zum Zeichen gehörige Bytesequenz kennen. Damit das Zeichen jedoch gefunden werden kann, ist es unumgänglich die Datei Byte für Byte zu lesen um die gesuchte Sequenz finden zu können, da es möglicherweise auch Zeichen gibt die nur ein Byte lang sind. Wenn jedoch sichergestellt ist, daß jedes Zeichen mit jeweils 2 Byte kodiert ist, kann die Datei in Schritten von 2 Byte gelesen werden. Somit ist abstrakt gesehen die Zeichenkodierung im Gunde genommen eine Typisierung und wenn es Letztere nicht geben würde, wäre ist nicht möglich eine CSV-Datei am Trennzeichen zu splitten oder eine XML-Datei zu parsen.

Numerische Datentypen

Die Zahl 0xFFFFFFFF (hexadezimal) benötigt zur Darstellung in dezimaler Form 4294967295 10 Zeichen, die Zahl 0x1 hingegen nur 3 bzw. 1 Zeichen in dezimaler Form. Wird jedoch eine Typisierung vorgenommen, ist die Anzahl der in einer Datei benötigten Bytes immer gleich. Somit ist es möglich, eine Datei die beliebig viele 32-Bit-Zahlen enthält, in Schritten von 4 Byte zu lesen womit jede einzelne Zahl gefunden und wiederhergestellt werden kann. Für Zahlen aus dem 16-Bit-Wertebereich gilt diese Aussage sinngemäß, hierzu wird die Sequenz in Schritten von 2 Byte gelesen. Und schließlich gibt es noch den kleinsten numerischen Type für den 8-Bit-Wertebereich von 0..255 und einer Länge von genau 1 Byte. Wenn jedoch Zahlen nicht typisiert wären, ist es nicht möglich in einer Datei die nur Zahlen enthält, eine bestimmte Zahl wiederzufinden weil die Zahlen unterschiedliche Längen hätten!

Offsetangaben in Dateien

Auf heutigen Rechnern mit einer Architektur > 32 Bit kann es auch entsprechend lange Dateien geben. Von daher ist es naheliegend, für Offsetangaben einen entsprechend dimensionierten numerischen Datentyp zu benutzen der vorzeichenlos ist. Offsetangaben in Dateien ermöglichen Tupel, Rekords und Felder mit variabler Länge, d.h., die Offsetangabe selbst hat stets eine feste Länge, enthält jedoch eine variable Anzahl der als nächstes aus der Datei zu lesenden Bytes.

Zum Lesen/Parsen von Dateien die UTF-8-kodierte Zeichen enthalten ist gerade dieser Sachverhalt wichtig: Byte für Byte muss eine solche Datei gelesen und jedes einzelne Byte auf seine Wertigkeit hin untersucht werden. Anhand dieser Wertigkeit ist nämlich festgelegt, wieviele folgende Bytes für das betreffende Zeichen zu lesen sind und das ist in UTF-8 bekanntlich unterschiedlich. Hier eine einfache Klasse in PHP welche diesen Algorithmus implementiert. Somit beinhaltet das erste Byte eines jeden UTF-8-kodierten Zeichen stets eine Offsetangabe, die jeder Texteditor ermitteln muss damit er das jeweilige Zeichen überhaupt darstellen kann wofür der Codepoint aus der Binary heraus berechnet werden muss.

Abstrakt: In Dateien sind UTF-8-kodierte Zeichen Tupel mit variabler Länge deren Offset das jeweils erste Byte beeinhaltet als numerischer Datentyp eines vorzeichenlosen Integer Uint8 welcher einen Wert von 0..255 annehmen kann. UTF-8-kodierte Zeichen sind abstrakte bzw. zusammengesetzte Datentypen.

Datum und Uhrzeit als 32-Bit-Integer

Zeitserver nach RFC 868 liefern die genaue Uhrzeit als Anzahl der seit 1900 verstrichenen Sekunden. Die an einen Zeitserver gerichtete Anfrage liefert einen 32 Bit numerischen Datentyp in Network-Order (Big Endian), zu lesen sind also genau 4 Bytes. Untenstehender Code nimmt diese Zahl als Anzahl der Sekunden und rechnet diese auf Lokalzeit um::

my $socket = IO::Socket::INET->new("ptbtime2.ptb.de:37") or die $@;
read($socket, my $buffer, 4);

# 2208988800 Differenz in Sekunden 1.1.1900 1.1.1970
print scalar localtime unpack("N", $buffer) - 2208988800;
# Thu Apr 19 13:47:14 2018

c-Library time.h

Definiert einen abstrakten Datentyp wie folgt:

struct  tm {
    int tm_sec;
    int tm_min;
    int tm_hour;
    int tm_mday;
    int tm_mon;
    int tm_year;
    int tm_wday;
    int tm_yday;
    int tm_isdst;
};

Was den Speicherbedarf einer typgerechten Übertragung auf genau 9 * 16 Bit => 18 Byte festlegt. Die Oktettenwertigkeiten sehen bspw. so aus: 1.0.5.0.22.0.18.0.2.0.118.0.0.0.76.0.0.0 und diese Binary lässt sich plattformübergreifend übertragen wann immer es darum geht Datumsangaben zu erfassen, zu transportieren und zu speichern. Diesen abstrakten Datentyp gibt es 1:1 auch in PHP! Nach der Übertragung eines Solchen in Form von 18 Bytes würde auch in PHP nur ein einziger Aufruf der unpack()-Funktion genügen um sämtliche Einzelfelder als Array wiederherzustellen: unpack("s*", $binary); In c hingegen würde man diesen Datentyp direkt wie folgt aus dem Socket lesen:

tm date_time;
fread(date_time, sizeof(tm), 1, socket);

Ebenso aber auch aus einer Datei, STDIN oder einem beliebigen Handle.

JS Codebeispiel: Das aktuelle Lokale Datum mit dem beschriebenen Datentype als ArrayBuffer per AJAX senden, zum Vergleich wird auch die Zeit vom Server ausgegeben:

    var date = new Date();
    var tm_buffer = new ArrayBuffer(18);
    var tm_dataview = new DataView(tm_buffer);
    tm_dataview.setInt16(0, date.getSeconds(), true);
    tm_dataview.setInt16(2, date.getMinutes(), true);
    tm_dataview.setInt16(4, date.getHours(), true);
    tm_dataview.setInt16(6, date.getDate(), true);
    tm_dataview.setInt16(8, date.getMonth(), true);
    tm_dataview.setInt16(10, date.getYear(), true);

    var xhr = new XMLHttpRequest();
    xhr.open('POST', "/typisierung.html?senddate=1");
    xhr.setRequestHeader("Content-Type","application/body+query");
    xhr.onload = function(){
        pretext(this.response);
    };
    xhr.send( tm_buffer );

    /*
        Serverseitig wird das Array mit den numerischen
        Datentypen wiederhergestellt:
        $self->{CONTENT} = join "\n", unpack "s*", $self->{CGI}->rawdata;

        Oder als String formatiert ausgegeben
        $self->{CONTENT} =
         localtime( timelocal( unpack( "s*", $self->{CGI}->rawdata)))."\n";

        s ist die Schablone für den short integer
    */

Der per POST gesendete ArrayBuffer verkörpert einen abstrakten Datentyp mit denselben Feldern wie sie auch in time.h deklariert sind. Aufgrunddessen daß die inneren Attribute (Felder) von Datentyp integer sind, hat jedes dieser Felder eine Länge von 2 Byte (16 Bit Little Endian). Infolgedessen hat das gesamte Struct eine Länge von 18 Byte, was bei diesem POST mit dem Header Content-Length: 18 übereinstimmt. Praktisch transportiert HTTP ein Array mit numerischen Datentypen und zur Wiederherstellung des Arrays genügt es, eine diesen Datentypen entsprechende Schablone auf die Binary zu legen.

Zeichenketten, Strings mit konstanter Länge

Typisierung legt den Wertebereich einer Zahl fest. Aber auch bei Strings kann man eine Festlegung treffen: Nämlich betreff der Stringlänge. Programmiertechnisch wird sowas Padding genannt und zum Auffüllen eine Strings sind in c Nullbytes üblich. Das kommt daher weil einerseits Strings in c nämlich auch nur zusammengesetzte Datentypen sind, Strings in c mit null zu terminieren sind und andererseits c den Speicher nicht dynamisch verwaltet. Von daher muss in c die Länge einer Zeichenkette deklariert werden bevor sie verwendet wird.

Genau das jedoch gilt auch für die Speicherung und den Transport von Strings. Insbesondere, wenn eine Datei mehrere Strings transportieren soll: Stellen Sie sich vor, eine Datei mit den Namen aller Einwohner einer Kleinstadt enthält diese Namen fortlaufend aneinandergereiht -- Es wäre nicht möglich die Namen aus dieser Datei heraus wiederherzustellen. Wenn jedoch für jeden Namen ein Feld mit konstanter und bekannter Länge vorgegeben ist, können alle Namen problemlos wiederhergestellt werden. Die Perl/PHP Funktionen pack()/unpack() implementieren diese Art von Padding in zwei Varianten: Die Z-Schablone unterstützt das Zero-Padding, die A-Schablone hingegen das Auffüllen mit Leerzeichen. Beispiel:

my @names = qw(Habedank Scherzinger Brockhorst);
my $fh = IO::String->new;
# Feldlänge 50 Bytes
foreach my $name( @names ){
    # Padding anwenden
    $fh->print( pack "Z50", $name);
}

# Namen mit derselben Schablone wiederherstellen
$fh->seek(0,0);
# lese für jeden Namen 50 Oktten
while( read($fh, my $buffer, 50)){
    # Padding entfernen
    print unpack("Z50", $buffer), "\n";
}

Wobei mit unpack() das Padding wieder entfernt wird. Auf diese Art und Weise ist es auch möglich, Strings in c-structs einzubauen bzw. Felder für Strings zu definieren, Demo siehe weiter oben. Abstrakt gesehen findet mit Padding eine Typisierung statt weil damit die Länge einer Zeichenkette festgelegt wird.

Untenstehend noch eine erprobte Funktion in JavaScript womit Zero-Padded Zeichenketten erzeugt werden können. Übergeben wird der String und die gewünschte Pufferlänge, im Default sind das 50 Bytes. Die Funktion gibt die Binary als Blob zurück:

/* Anzahl der Bytes für einen String begrenzen */
function bcut(str, blen){
    blen = blen != null ? blen : 50;
    var buffer = new ArrayBuffer(blen);
    var dv = new DataView(buffer);
    dv.setString(0, str);
    return new Blob([buffer]);
}

Prozentkodierung, Percentencoding

Auch das ist ein Beispiel dafür daß es ohne eine Festlegung der Länge nicht möglich ist, aus einem prozentkodierten String wie %E2%82%AC die Oktetten des dazugehörigen Zeichen (im Beispiel das Eurozeichen) wiederherzustellen: Hexadezimal dargestellt ist jede Oktettenwertigkeit immer genau zweistellig.

Die Bedeutung des Content-Type

Mit dem Verständnis der bisherigen Ausführungen sollte auch klar sein, daß es nicht nur auf den Inhalt einer Datei ankommt sondern auch darauf, daß der Empfänger weiß was er mit diesem Inhalt anstellen soll, wie der zu verarbeiten oder darzustellen ist. Nehmen wir einmal an, es liegt ein 32 Bit Datentyp in einer Datei mit 4 Bytes Länge. Da ergeben sich folgende Möglichkeiten hinsichtlich Verwendung, also des Content-Type:

LE, BE: Little bzw. Big Endian. Bekanntlich wird der Content-Type bei einem HTTP Request in einem dem entsprechenden Header übertragen damit der im Message Body gesendete Inhalt auf der Empfängerseite richtig dargestellt werden kann.

Allgemein: Wenn Daten ein System verlassen, verlassen sie es als eine Folge primitiver Datentypen. Nur der dazu mitgelieferte Content-Type respective Charset entscheidet beim Empfänger darüber was der damit machen soll. Ein Texteditor oder ein WWW-Browser wird diese primitiven Datentypen immer als Zeichen darstellen wofür er die Kodierung kennen muss.


Anbieter: nmq​rstx-18­@yahoo.de, die Seite verwendet funktionsbedingt einen Session-Cookie und ist Bestandteil meines nach modernen Aspekten in Perl entwickelten Frameworks.