Fetch API

Die Emscripten Fetch API ermöglicht nativem Code, Dateien via XHR (HTTP GET, PUT, POST) von Remote-Servern zu übertragen und die heruntergeladenen Dateien lokal im IndexedDB-Speicher des Browsers zu persistieren, sodass sie bei späteren Seitenbesuchen lokal wieder aufgerufen werden können. Die Fetch API kann von mehreren Threads aufgerufen werden, und die Netzwerkanfragen können wahlweise synchron oder asynchron ausgeführt werden.

Hinweis

Um die Fetch API zu verwenden, müssten Sie Ihren Code mit -sFETCH kompilieren.

Einleitung

Die Verwendung der Fetch API lässt sich schnell anhand eines Beispiels veranschaulichen. Die folgende Anwendung lädt eine Datei von einem Webserver asynchron in den Arbeitsspeicher des Anwendungshaufens herunter.

#include <stdio.h>
#include <string.h>
#include <emscripten/fetch.h>

void downloadSucceeded(emscripten_fetch_t *fetch) {
  printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
  // The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
  emscripten_fetch_close(fetch); // Free data associated with the fetch.
}

void downloadFailed(emscripten_fetch_t *fetch) {
  printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
  emscripten_fetch_close(fetch); // Also free data on failure.
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  attr.onsuccess = downloadSucceeded;
  attr.onerror = downloadFailed;
  emscripten_fetch(&attr, "myfile.dat");
}

Wenn bei einem Aufruf von emscripten_fetch ein relativer Pfadname angegeben wird, wie im obigen Beispiel, wird der XHR relativ zum href (URL) der aktuellen Seite ausgeführt. Das Übergeben einer vollständig qualifizierten absoluten URL ermöglicht das Herunterladen von Dateien über Domänen hinweg, unterliegt jedoch den HTTP-Zugriffskontrollregeln (CORS).

Standardmäßig läuft die Fetch API asynchron, was bedeutet, dass der Funktionsaufruf emscripten_fetch() sofort zurückkehrt und die Operation im Hintergrund fortgesetzt wird. Wenn die Operation abgeschlossen ist, wird entweder der Erfolgs- oder der Fehler-Callback aufgerufen.

Datenpersistenz

Die von der Fetch API ausgegebenen XHR-Anfragen unterliegen dem üblichen Browser-Caching-Verhalten. Diese Caches sind transient (temporär), sodass keine Garantie besteht, dass die Daten für einen bestimmten Zeitraum im Cache verbleiben. Wenn die Dateien außerdem etwas groß sind (mehrere Megabyte), cachen Browser die Downloads typischerweise überhaupt nicht.

Um eine explizitere Kontrolle über die Persistenz der heruntergeladenen Dateien zu ermöglichen, interagiert die Fetch API mit der IndexedDB API des Browsers, die große Datendateien laden und speichern kann, die bei späteren Besuchen der Seite verfügbar sind. Um die IndexedDB-Speicherung zu aktivieren, übergeben Sie das Flag EMSCRIPTEN_FETCH_PERSIST_FILE in den Fetch-Attributen.

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  ...
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_PERSIST_FILE;
  ...
  emscripten_fetch(&attr, "myfile.dat");
}

Ein vollständiges Beispiel finden Sie in der Datei test/fetch/test_fetch_persist.c.

Persistenz von Datenbytes aus dem Speicher

Manchmal ist es nützlich, einen Bereich von Bytes aus dem Anwendungsspeicher in IndexedDB zu persistieren (ohne XHRs durchführen zu müssen). Dies ist mit der Emscripten Fetch API möglich, indem das spezielle HTTP-Aktionsverb „EM_IDB_STORE“ an die Emscripten Fetch-Operation übergeben wird.

void success(emscripten_fetch_t *fetch) {
  printf("IDB store succeeded.\n");
  emscripten_fetch_close(fetch);
}

void failure(emscripten_fetch_t *fetch) {
  printf("IDB store failed.\n");
  emscripten_fetch_close(fetch);
}

void persistFileToIndexedDB(const char *outputFilename, uint8_t *data, size_t numBytes) {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "EM_IDB_STORE");
  attr.attributes = EMSCRIPTEN_FETCH_REPLACE | EMSCRIPTEN_FETCH_PERSIST_FILE;
  attr.requestData = (char *)data;
  attr.requestDataSize = numBytes;
  attr.onsuccess = success;
  attr.onerror = failure;
  emscripten_fetch(&attr, outputFilename);
}

int main() {
  // Create data
  uint8_t *data = (uint8_t*)malloc(10240);
  srand(time(NULL));
  for(int i = 0; i < 10240; ++i) data[i] = (uint8_t)rand();

  persistFileToIndexedDB("outputfile.dat", data, 10240);
}

Löschen einer Datei aus IndexedDB

Dateien können aus IndexedDB entfernt werden, indem das HTTP-Aktionsverb „EM_IDB_DELETE“ verwendet wird.

void success(emscripten_fetch_t *fetch) {
  printf("Deleting file from IDB succeeded.\n");
  emscripten_fetch_close(fetch);
}

void failure(emscripten_fetch_t *fetch) {
  printf("Deleting file from IDB failed.\n");
  emscripten_fetch_close(fetch);
}

int main() {
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "EM_IDB_DELETE");
  emscripten_fetch(&attr, "filename_to_delete.dat");
}

Synchrone Abrufe

In einigen Szenarien wäre es wünschenswert, eine XHR-Anfrage oder eine IndexedDB-Dateioperation synchron im aufrufenden Thread durchführen zu können. Dies kann die Portierung von Anwendungen erleichtern und den Codefluss vereinfachen, indem die Notwendigkeit, einen Callback zu übergeben, vermieden wird.

Alle Arten von Emscripten Fetch API Operationen (XHRs, IndexedDB Zugriffe) können synchron durchgeführt werden, indem das Flag EMSCRIPTEN_FETCH_SYNCHRONOUS übergeben wird. Wenn dieses Flag übergeben wird, blockiert der aufrufende Thread, bis die Fetch-Operation abgeschlossen ist. Siehe folgendes Beispiel.

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_SYNCHRONOUS;
  emscripten_fetch_t *fetch = emscripten_fetch(&attr, "file.dat"); // Blocks here until the operation is complete.
  if (fetch->status == 200) {
    printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
    // The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
  } else {
    printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
  }
  emscripten_fetch_close(fetch);
}

Im obigen Codebeispiel werden die Callback-Funktionen für Erfolg und Fehler nicht verwendet. Wenn sie jedoch angegeben sind, werden sie synchron aufgerufen, bevor emscripten_fetch() zurückkehrt.

Hinweis

Synchrone Emscripten Fetch-Operationen unterliegen einer Reihe von Einschränkungen, abhängig vom verwendeten Emscripten-Build-Modus (Linker-Flags).

  • Keine Flags: Nur asynchrone Fetch-Operationen sind verfügbar.

  • --proxy-to-worker: Synchrone Fetch-Operationen sind für Fetches erlaubt, die nur einen XHR durchführen, aber nicht mit IndexedDB interagieren.

  • -pthread: Synchrone Fetch-Operationen sind auf pthreads verfügbar, aber nicht auf dem Hauptthread.

  • --proxy-to-worker + -pthread: Synchrone Fetch-Operationen sind sowohl auf dem Hauptthread als auch auf pthreads verfügbar.

Fortschritt verfolgen

Für eine robuste Fetch-Verwaltung stehen mehrere Felder zur Verfügung, um den Status eines XHR zu verfolgen.

Der onprogress-Callback wird immer dann aufgerufen, wenn neue Daten empfangen wurden. Dies ermöglicht die Messung der Download-Geschwindigkeit und die Berechnung einer geschätzten Fertigstellungszeit (ETA). Zusätzlich übergibt die Struktur emscripten_fetch_t die XHR-Objektfelder readyState, status und statusText, die Informationen über den HTTP-Ladezustand der Anfrage geben.

Das Objekt emscripten_fetch_attr_t besitzt ein Feld timeoutMSecs, welches die Angabe einer Timeout-Dauer für die Übertragung ermöglicht. Zusätzlich kann emscripten_fetch_close() jederzeit für asynchrone und wartbare Fetches aufgerufen werden, um den Download abzubrechen. Das folgende Beispiel veranschaulicht diese Felder und den onprogress-Handler.

void downloadProgress(emscripten_fetch_t *fetch) {
  if (fetch->totalBytes) {
    printf("Downloading %s.. %.2f%% complete.\n", fetch->url, fetch->dataOffset * 100.0 / fetch->totalBytes);
  } else {
    printf("Downloading %s.. %lld bytes complete.\n", fetch->url, fetch->dataOffset + fetch->numBytes);
  }
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  attr.onsuccess = downloadSucceeded;
  attr.onprogress = downloadProgress;
  attr.onerror = downloadFailed;
  emscripten_fetch(&attr, "myfile.dat");
}

Umgang mit großen Dateien

Besondere Aufmerksamkeit sollte der Speichernutzungsstrategie eines Abrufs gewidmet werden. Frühere Beispiele haben alle das Flag EMSCRIPTEN_FETCH_LOAD_TO_MEMORY übergeben, wodurch emscripten_fetch() die heruntergeladene Datei im onsuccess()-Callback vollständig im Speicher ablegt. Dies ist praktisch, wenn die gesamte Datei sofort danach zugänglich sein soll, aber für große Dateien kann dies eine verschwenderische Strategie in Bezug auf die Speichernutzung sein. Wenn die Datei sehr groß ist, passt sie möglicherweise nicht einmal in den Heap-Bereich der Anwendung.

Die folgenden Unterabschnitte bieten Möglichkeiten zur speichereffizienten Verwaltung großer Abrufe.

Direktes Herunterladen nach IndexedDB

Wenn eine Anwendung eine Datei für den lokalen Zugriff herunterladen möchte, die Datei aber nicht sofort verwenden muss, z.B. beim Vorladen von Daten für einen späteren Zugriff, ist es eine gute Idee, das Flag EMSCRIPTEN_FETCH_LOAD_TO_MEMORY ganz zu vermeiden und stattdessen nur das Flag EMSCRIPTEN_FETCH_PERSIST_FILE zu übergeben. Dies bewirkt, dass der Fetch die Datei direkt in IndexedDB herunterlädt, wodurch ein temporäres Laden der Datei in den Speicher nach Abschluss des Downloads vermieden wird. In diesem Szenario meldet der onsuccess()-Handler nur die gesamte heruntergeladene Dateigröße, enthält aber nicht die Datenbytes der Datei.

Streaming-Downloads

Hinweis: Dies funktioniert derzeit nur in Firefox, da es „moz-chunked-arraybuffer“ verwendet.

Wenn die Anwendung keinen wahlfreien Lesezugriff auf die Datei benötigt, sondern die Datei in einem Streaming-Verfahren verarbeiten kann, kann sie das Flag EMSCRIPTEN_FETCH_STREAM_DATA verwenden, um die Bytes der Datei während des Downloads zu streamen. Wenn dieses Flag übergeben wird, werden die heruntergeladenen Datenblöcke in kohärenter, dateisequentieller Reihenfolge an den onprogress()-Callback übergeben. Siehe den folgenden Schnipsel für ein Beispiel.

void downloadProgress(emscripten_fetch_t *fetch) {
  printf("Downloading %s.. %.2f%%s complete. HTTP readyState: %d. HTTP status: %d.\n"
    "HTTP statusText: %s. Received chunk [%llu, %llu[\n",
    fetch->url, fetch->totalBytes > 0 ? (fetch->dataOffset + fetch->numBytes) * 100.0 / fetch->totalBytes : (fetch->dataOffset + fetch->numBytes),
    fetch->totalBytes > 0 ? "%" : " bytes",
    fetch->readyState, fetch->status, fetch->statusText,
    fetch->dataOffset, fetch->dataOffset + fetch->numBytes);

  // Process the partial data stream fetch->data[0] thru fetch->data[fetch->numBytes-1]
  // This buffer represents the file at offset fetch->dataOffset.
  for(size_t i = 0; i < fetch->numBytes; ++i)
    ; // Process fetch->data[i];
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_STREAM_DATA;
  attr.onsuccess = downloadSucceeded;
  attr.onprogress = downloadProgress;
  attr.onerror = downloadFailed;
  attr.timeoutMSecs = 2*60;
  emscripten_fetch(&attr, "myfile.dat");
}

In diesem Fall erhält der onsuccess()-Handler den endgültigen Dateipuffer überhaupt nicht, sodass die Speichernutzung auf ein Minimum beschränkt bleibt.

Byte-Bereichs-Downloads

Große Dateien können auch in kleineren Blöcken durch Byte-Bereichs-Downloads verwaltet werden. Dies initiiert eine XHR- oder IndexedDB-Übertragung, die nur den gewünschten Unterbereich der gesamten Datei abruft. Dies ist nützlich, wenn beispielsweise eine große Paketdatei mehrere kleinere an bestimmten Such-Offsets enthält, die separat behandelt werden können.

#include <stdio.h>
#include <string.h>
#include <emscripten/fetch.h>

void downloadSucceeded(emscripten_fetch_t *fetch) {
  printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
  // The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
  emscripten_fetch_close(fetch); // Free data associated with the fetch.
}

void downloadFailed(emscripten_fetch_t *fetch) {
  printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
  emscripten_fetch_close(fetch); // Also free data on failure.
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  // Make a Range request to only fetch bytes 10 to 20
  const char* headers[] = {"Range", "bytes=10-20", NULL};
  attr.requestHeaders = headers;
  attr.onsuccess = downloadSucceeded;
  attr.onerror = downloadFailed;
  emscripten_fetch(&attr, "myfile.dat");
}

TODO Dokumentieren

Emscripten_fetch() unterstützt auch die folgenden Operationen, die dokumentiert werden müssen:

  • Emscripten_fetch kann verwendet werden, um Dateien über HTTP PUT auf Remote-Server hochzuladen.

  • Emscripten_fetch_attr_t ermöglicht das Setzen benutzerdefinierter HTTP-Anfrage-Header (z.B. für die Cache-Kontrolle).

  • Dokumentieren Sie die einfachen HTTP-Authentifizierungsfelder in Emscripten_fetch_attr_t.

  • Dokumentieren Sie das Attribut overriddenMimeType in Emscripten_fetch_attr_t.

  • Referenzdokumentation der einzelnen Felder in Emscripten_fetch_attr_t, Emscripten_fetch_t und #defines.

  • Beispiel zum Laden nur aus IndexedDB ohne XHRing.

  • Beispiel zum Überschreiben einer bestehenden Datei in IndexedDB mit einem neuen XHR.

  • Beispiel, wie ein komplettes Dateisystem in IndexedDB vorgeladen werden kann, um den Ersatz von –preload-file zu vereinfachen.

  • Beispiel, wie Inhalte als Gzipped in IndexedDB gespeichert und beim Laden dekomprimiert werden können.

  • Beispiel zum Abbrechen und Fortsetzen teilweiser Übertragungen nach IndexedDB.