Fetch API

Emscripten Fetch API 允许本地代码通过 XHR(HTTP GET、PUT、POST)从远程服务器传输文件,并在浏览器的 IndexedDB 存储中本地保存下载的文件,以便在后续页面访问时本地重新访问它们。Fetch API 可以从多个线程调用,并且网络请求可以根据需要同步或异步运行。

注意

为了使用 Fetch API,您需要使用 -sFETCH 编译您的代码。

介绍

通过一个示例可以快速说明 Fetch API 的用法。以下应用程序异步地从 Web 服务器下载文件到应用程序堆中的内存中。

#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");
}

如果对 emscripten_fetch 的调用指定了相对路径名,如上面的示例,则 XHR 将相对于当前页面的 href(URL)执行。传递完全限定的绝对 URL 允许跨域下载文件,但这些文件受 HTTP 访问控制 (CORS) 规则 的约束。

默认情况下,Fetch API 异步运行,这意味着 emscripten_fetch() 函数调用会立即返回,并且操作将在后台继续进行。当操作完成时,将调用成功或失败回调。

持久化数据

Fetch API 发出的 XHR 请求会受到通常的浏览器缓存行为的影响。这些缓存是短暂的(临时的),因此无法保证数据将在特定时间段内保留在缓存中。此外,如果文件比较大(几兆字节),浏览器通常根本不会缓存下载。

为了能够更明确地控制持久化下载的文件,Fetch API 与浏览器的 IndexedDB API 交互,该 API 可以加载和存储在后续访问页面时可用的大型数据文件。要启用 IndexedDB 存储,请在 fetch 属性中传递 EMSCRIPTEN_FETCH_PERSIST_FILE 标志

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");
}

有关完整示例,请参阅文件 test/fetch/test_fetch_persist.c

从内存持久化数据字节

有时将应用程序内存中的字节范围持久化到 IndexedDB(无需执行任何 XHR)非常有用。可以通过将特殊 HTTP 动词“EM_IDB_STORE”传递给 Emscripten Fetch 操作,使用 Emscripten Fetch API 实现此功能。

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);
}

从 IndexedDB 删除文件

可以使用 HTTP 动词“EM_IDB_DELETE”从 IndexedDB 中清理文件

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");
}

同步 Fetch

在某些情况下,能够在调用线程中同步执行 XHR 请求或 IndexedDB 文件操作会很有用。这可以通过避免需要传递回调来简化应用程序移植并简化代码流程。

所有类型的 Emscripten Fetch API 操作(XHR、IndexedDB 访问)都可以通过传递 EMSCRIPTEN_FETCH_SYNCHRONOUS 标志同步执行。当传递此标志时,调用线程将阻塞以休眠,直到 fetch 操作完成。请参见以下示例。

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);
}

在上面的代码示例中,未使用成功和失败回调函数。但是,如果指定,它们将在 emscripten_fetch() 返回之前同步调用。

注意

同步 Emscripten Fetch 操作会受到一些限制,具体取决于使用的是哪个 Emscripten 构建模式(链接器标志)

  • 无标志:仅提供异步 Fetch 操作。

  • --proxy-to-worker:仅针对仅执行 XHR 但不与 IndexedDB 交互的 fetch 允许同步 Fetch 操作。

  • -pthread:同步 Fetch 操作在 pthreads 上可用,但在主线程上不可用。

  • --proxy-to-worker + -pthread:同步同步 Fetch 操作在主线程和 pthreads 上都可用。

跟踪进度

为了进行稳健的 fetch 管理,有几个字段可用于跟踪 XHR 的状态。

每当收到新数据时,都会调用 onprogress 回调。这允许您测量下载速度并计算完成的预计时间。此外,emscripten_fetch_t 结构会传递 XHR 对象字段 readyState、status 和 statusText,这些字段提供了有关请求的 HTTP 加载状态的信息。

emscripten_fetch_attr_t 对象具有一个 timeoutMSecs 字段,允许您指定传输的超时持续时间。此外,emscripten_fetch_close() 可以随时为异步和可等待的 fetch 调用以中止下载。以下示例说明了这些字段和 onprogress 处理程序。

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");
}

管理大型文件

应特别注意 fetch 的内存使用策略。前面的示例都传递了 EMSCRIPTEN_FETCH_LOAD_TO_MEMORY 标志,这会导致 emscripten_fetch() 在 onsuccess() 回调中完全填充下载的文件到内存中。这在需要立即访问整个文件时很方便,但对于大型文件,这在内存使用方面可能是一种浪费的策略。如果文件非常大,它甚至可能无法容纳在应用程序的堆区域内。

以下部分提供了以内存高效的方式管理大型 fetch 的方法。

直接下载到 IndexedDB

如果应用程序想要下载文件以供本地访问,但不需要立即使用该文件(例如,在预先加载数据以备后用时),则最好完全避免使用 EMSCRIPTEN_FETCH_LOAD_TO_MEMORY 标志,而只传递 EMSCRIPTEN_FETCH_PERSIST_FILE 标志。这会导致 fetch 直接将文件下载到 IndexedDB,从而避免在下载完成后暂时将文件填充到内存中。在这种情况下,onsuccess() 处理程序只会报告下载文件的总大小,但不会包含文件的数据字节。

流式下载

注意:这目前仅在 Firefox 中有效,因为它使用的是“moz-chunked-arraybuffer”。

如果应用程序不需要对文件进行随机查找访问,但能够以流式方式处理文件,则可以使用 EMSCRIPTEN_FETCH_STREAM_DATA 标志在下载文件时按顺序流式传输文件中的字节。如果传递此标志,则下载的数据块将按一致的文件顺序传递到 onprogress() 回调中。请参见以下代码段,了解示例。

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");
}

在这种情况下,onsuccess() 处理程序根本不会收到最终的文件缓冲区,因此内存使用量将保持在最低限度。

字节范围下载

也可以通过对大型文件执行字节范围下载,以较小的块来管理大型文件。这将启动一个 XHR 或 IndexedDB 传输,该传输仅获取整个文件的所需子范围。例如,当一个大型包文件在某些查找偏移处包含多个较小的文件时,这很有用,这些文件可以分别处理。

#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");
}

待文档化

Emscripten_fetch() 也支持以下操作,需要进行文档化

  • Emscripten_fetch 可用于通过 HTTP PUT 将文件上传到远程服务器

  • Emscripten_fetch_attr_t 允许设置自定义 HTTP 请求标头(例如,用于缓存控制)

  • 记录 Emscripten_fetch_attr_t 中的 HTTP 简单身份验证字段。

  • 记录 Emscripten_fetch_attr_t 中的 overriddenMimeType 属性。

  • 参考 Emscripten_fetch_attr_t、Emscripten_fetch_t 和 #defines 中各个字段的文档。

  • 关于仅从 IndexedDB 加载而无需 XHR 的示例。

  • 关于使用新的 XHR 覆盖 IndexedDB 中现有文件的示例。

  • 关于如何将整个文件系统预加载到 IndexedDB 以便轻松替换 –preload-file 的示例。

  • 关于如何将内容压缩为 gzip 后持久化到 IndexedDB,以及在加载时解压缩的示例。

  • 关于如何中止和恢复对 IndexedDB 的部分传输的示例。