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);
}
可以使用 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");
}
在某些情况下,能够在调用线程中同步执行 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 的方法。
如果应用程序想要下载文件以供本地访问,但不需要立即使用该文件(例如,在预先加载数据以备后用时),则最好完全避免使用 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 的部分传输的示例。