ArrayBuffer und abstrakte Datentypen im HTTP-Request

Senden und Empfangen von c-Structs in XMLHttpRequest/Ajax

In diesem Artikel geht es darum abstrakte Datentypen per Ajax/XMLHttpRequest zu senden und zu empfangen. Am Anfang steht natürlich die Frage des Warum und die Antwort ist recht einfach: Die gesendeten Daten sollen wahlweise mit C, Perl oder PHP verarbeitet oder auch gesendet werden. Darüber hinaus geht es ganz allgemein um eine zweckmäßige Datenserialisierung, also die Umwandlung kompexer Datenstrukturen in Bytesequenzen und die Wiederherstellung dieser Datenstrukturen.

Betrachten wir den Aufbau eines C-Struct mit welchem 2 Strings zu transportieren sind:

typedef struct{
    char vname[32];
    char name[32];
} Person;

Für Name, Vorname, also für jeden String sind jeweils genau 32 Bytes reserviert. Hier an dieser Stelle gibt es folgende Dinge zu beachten:

In JavaScript ist nun zu vermitteln zwischen Character- und Bytesemantic. Per Default arbeitet JavaScript nämlich zeichenorientiert, von daher muß eine Zeichenkette in eine Bytesequenz umgewandelt werden. Diese Vermittlung läßt sich zweckmäßig mit dem TextEncoder machen:

let te = new TextEncoder(); let uha = te.encode('äöüäö');

Das Ergebnis ist ein Uint8Array. Dieses Uint8-Array gilt es nun in einen ArrayBuffer zu kopieren, einen ArrayBuffer welcher mit der Länge von 32 Bytes deklariert wurde. Dabei sind alle Bytes zunächst mit binären Nullen aufgefüllt. Das Kopieren selbst erfolgt über ein DataView:

let ab = new ArrayBuffer(32); let dv = new DataView(ab); for(i = 0; i < 30; i++){ dv.setUint8(i, uha[i]); }

Auf diese Art und Weise bleibt am Ende dieser Bytesequenz eine Binäre Null als String-Terminator. Das Ganze lässt sich nun in einer kleinen Funktion zusammenfassen:

// Null-terminierter String als Arraybuffer function str2ab(str, len){ let ab = new ArrayBuffer(len); let dv = new DataView(ab); let te = new TextEncoder(); let uha = te.encode(str); for(i = 0; i < len-2; i++){ dv.setUint8(i,uha[i]); } return ab; }

Serialisierung - Strings mit festen Längen

Wir kommen nun zum eigentlichen Thema strukturierte Daten. Wenn wir mehrere Name, Vorname-Paare haben, hängen wir die ganz einfach aneinander, in JavaScript heißt das, daß wir einen Blob erzeugen. Und das geht so:

let name1 = str2ab('Möller',32); let vname1 = str2ab('Jürgen',32); let name2 = str2ab('Müller',32); let vname2 = str2ab('Fritz',32); let postData = new Blob([name1,vname1,name2,vname2]);

Serverseitig wird blockweise aus STDIN gelesen und zwar mit 64 Byte je Block hier in diesem Dimensionierungsbeispiel. Das heißt, daß jeder Block einen Namen und einen Vornamen beinhaltet.

Serialisierung - Strings mit variablen Längen

Der Grund dafür, daß man Strings in Sequenzen legt die eine bekannte Länge haben ist klar, denn es geht ja darum die Daten wiederherstellen zu können. Wird die Länge jedoch von vornherein festgelegt, ergeben sich daraus einige Nachteile:

Der letztgenannte Fall kann auftreten, wenn ein Zeichen was mit mehreren Bytes kodiert wird, am Ende einer Zeichenkette steht und der reservierte Speicherplatz nicht mehr für alle Bytes des Zeichens ausreicht. Diese Nachteile lassen sich allesamt vermeiden, wenn genausoviel Speicherplatz für eine Bytsequenz reserviert wird wie diese Sequenz selbst benötigt. Das heißt jedoch auch, daß diese Längen bekannt sein muß damit die Daten wiederhergestellt werden können. Und das heißt, daß diese Längenangabe selbst eine bekannte Länge haben muß. genau dafür ist die Typisierung numerischer Werte zuständig, bspw. benötigt ein vorzeichenloser 32-Bit-Integer eben 32 Bit bzw. genau 4 Bytes.

Modifizieren wir hierzu die obenstehende Funtkion:

function str2ab(str){ let te = new TextEncoder(); let uha = te.encode(str); let ab = new ArrayBuffer(4+uha.length); let dv = new DataView(ab); dv.setUint32(0,uha.length); for(i = 0; i < uha.length; i++) dv.setUint8(4+i, uha[i]); return ab; }

Erläuterung: Die Zeichenkette wird vermittels TextEncoder in ein Uint8Array umgewandelt. Mit der Anzahl der Bytes plus 4 Byte für die Längenangabe wird ein ArrayBuffer bereitgestellt. Nun wird die Länge des Strings, also die Anzahl der Bytes als Uint32/BigEndian in die ersten 4 Bytes geschrieben. Die verbleibenden Bytes werden mit dem Uint8Array aufgefüllt.

Eine andere Möglichkeit besteht darin, den String (Uint8Array) als ArrayBuffer einfach an den Arraybuffer der Längenangabe zu hängen:

function str2ab(str){ let te = new TextEncoder(); let uha = te.encode(str); let ab = new ArrayBuffer(4); let dv = new DataView(ab); dv.setUint32(0,uha.length); return new Blob([ab,uha.buffer]); }

Damit entfällt das byteweise Kopieren der einzelnen Bytes der Zeichenkette. Eine andere möglichkeit besteht darin, direkt aus der Zeichenkette einen Blob zu erzeugen und zur Längenangabe das Attribut size heranzuziehen. Das TextEncoder-Objekt entfällt:

function str2ab(str){ let strblob = new Blob([str]); let ab = new ArrayBuffer(4); let dv = new DataView(ab); dv.setUint32(0, strblob.size); return new Blob([ab, strblob]); }

Vom C-Struct zum Array

Arrays sind im Grunde genommen genauso abstrakte Datentypen wie C-Structs. Neben einfachen String-Literalen oder numerischen Werten kann ein Array genausogut eine Sammlung mehrerer Objekte sein oder in JavaScript mehrere ArrayBuffer, Blobs, typed Arrays oder Klasseninstanzen enthalten. Betrachte untenstehenden Code:

let names = ['Müller','Meier','Schulze','Heßling']; let data = []; for(let i = 0; i < names.length; i++) data.push( str2ab(names[i]) ); let xhr = new XHR(cb); xhr.post('/str2ab.html?s=1', new Blob(data));

Gesendet wird also ein Blob und der ist nichts weiter als eine gewöhnliche Datei mit aufeinanderfolgenden Bytesequenzen. Der Algorithmus zur Wiederherstellung des Arrays bzw. der einzelnen Elemente ist einfach: Beginnend mit genau 4 Byte wird solange aus der Datei gelesen wie Daten kommen. Im Einzelnen:

Aus einen Solchen Array lassen sich, je nach Gruppierung einzelner Elemente weitere abstrakte Datentypen erstellen, z.B. Assoziative Arrays nach dem Muster: [key,value,key,value,key,value,usw.] Oder auch eine Datenhaltung nach dem Muster Entity.Attribute.Value:

[ent,att,val,ent,att,val,usw.]

Bis hierhin sei noch angemerkt, daß eine derartige Serialisierung binärsicher, also auch für die Übertragung von Binaries geeignet ist.

Serialisierte Daten empfangen

Wir setzen den XHR.responseType = 'arraybuffer'. Gesendet wird eine Binary die so aufgebaut ist, daß die ersten 4 Bytes die Längenangabe der angehängten Bytesequenz enthalten. Die gesamte Sequenz können wir auch als einen Tupel bezeichnen, mit Perl wird sie z.B. so erzeugt: my $str = 'Hällö'; $self->{CONTENT} = pack("N", length $str).$str;

// this.response ist ein ArrayBuffer let dv = new DataView(this.response); let len = dv.getUint32(0); // Längenangabe // lese Rohdaten ab Offset 4 mit der Längenangabe let uha = new Uint8Array(this.response, 4, len) let te = new TextDecoder(); // UTF-8-Zeichenkette pretext(len+' bytes: '+te.decode(uha));

Und natürlich läßt sich auch dieser Algorithmus in einen Schleifenkörper setzen. Iteriert wird über den ArrayBuffer (Bytesequenz), dabei werden wechselweise die Längenangabe als Uint32 und dann die Bytes als Uint8Array gelesen. Aus dem Uint8Array lassen sich dann, je nach Verwendung, Blob-Urls oder lesbare UTF-8-kodierte Zeichenketten wiederherstellen.

// ArrayBuffer, BigEnd+Bytes function BufferDecode( buffer ){ // Konstruktor this.ab = buffer; this.dv = new DataView(buffer); this.offs = 0; this.blen = buffer.byteLength; this.result = []; // Array für Uint8Arrays // Decode einzelne Tupel this.decode = function(){ for( this.offs = 0; this.offs < this.blen; ){ let len = this.dv.getUint32(this.offs); this.offs += 4; let uha = new Uint8Array(this.ab, this.offs, len); this.result.push( uha ); this.offs += len; } return this.result; }; }

Praktische Beispiele

Download Multimedia: Per Ajax werden Images mit Beschreibung angefordert und im Browser dargestellt. Klicken Sie also zum Starten der Übertragung.

Formular senden: Die eingegebenen Daten werden als Key+Value-Paare serialisiert. Wobei Key der Name des Eingabefeldes ist und Value eben das was eingegeben wurde und das kann auch eine Dateianlage sein, denn die Übertragung ist binärsicher.

Name:     
Vorname: 
         

Bytesequenzen und Informationsgehalt

Betrachten wir das eingangs gezeigte C-Strukt, wird Folgendes klar:

Interessant ist die unter Punkt (2) getroffene Feststellung, denn daß es sich bei den Daten um Vorname und Name handelt, weiß nur die interne Programmlogik und wie diese beiden Informationen zu trennen sind, ist über die Längenangabe geregelt. Selbst wenn in einer Datei mehrere Tupel (Vorname, Name) vorliegen sind diese im Einzelnen sauber zu trennen. Abstrakt gesehen liefert die Wiederherstellung der Daten ein Array nach dem Schema [[name,vname],[name,vname],...] und genau dieselbe Datenstruktur läßt sich auch aus einer Datei erzeugen, in welcher [name,vname] mit einer variablen Längenangabe transportiert werden.

Serverseitige Algorithmen

Dem Datentyp Uint32 (4 Byte) entspricht on Perl die pack-Schablone "N" (Big Endian) bzw "V" (Little Endian). Eine Einigung auf Big bzw. Little Endian ist also erforderlich.

Mit pack("N", length $str).$str wird ein Tupel kodiert. Dabei ist darauf zu achten daß die Bytesemantic eingeschaltet ist, also daß $str eine Bytesequenz beeinhaltet und nicht etwa eine Zeichenkette. Die Wiederherstellung eines ganzen Array aus den gesendeten Daten kann z.B. so gemacht werden:

while( read($self->{CGI}->{STDIN}, my $bin, 4) ){ my $len = unpack "N", $bin; read($self->{CGI}->{STDIN}, my $name, $len); push @names, $name; }

Abwechselnd werden beim Lesen der ganzen Datei also erst die Längenangabe ermittelt und damit dann die Daten gelesen. Auf diese Art und Weise wird eine Datei genauso sequentiell gelesen wie sie geschrieben wird. Dieses sequentielle Lesen ist wesentlich effizienter, als das Parsen eines Blobs, bspw. ein Splitten an Trennzeichen (CSV-Datei oder application/x-www-form-urlencoded usw.).


Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. s​os­@rolf­rost.de.