在 Firefox OS 中嵌入 HTTP Web 服务器

去年年底,Mozilla 的员工们聚在一起进行了一周的合作和规划。在这周期间,一个小组组建起来,设想以更加 P2P 为中心的 Web,Firefox OS 的未来将会是什么样。特别是,我们一直在研究利用技术来共同实现离线 P2P 连接,例如 蓝牙NFCWiFi Direct.

由于这些技术只提供设备之间通信的方式,因此立即明确我们需要一个协议让应用程序发送和接收数据。我很快意识到我们已经拥有一个可以利用的标准协议,用于在 Web 应用程序中传输数据——HTTP。

通过使用 HTTP,我们已经拥有了应用程序在客户端发送和接收数据所需的一切,但我们仍然需要在浏览器中运行一个 Web 服务器来启用离线 P2P 通信。虽然这种类型的 HTTP 服务器功能最适合作为标准化 WebAPI 的一部分,以便将其嵌入到 Gecko 中,但实际上我们已经在 Firefox OS 中拥有了今天在 JavaScript 中实现这一切所需的一切!

navigator.mozTCPSocket

打包的应用程序 可以访问原始 TCP 和 UDP 网络套接字,但由于我们处理的是 HTTP,因此我们只需要使用 TCP 套接字。对 TCPSocket API 的访问是通过 navigator.mozTCPSocket 公开的,目前它只对具有 tcp-socket 权限的“特权”打包的应用程序公开。

"type": "privileged",
"permissions": {
  "tcp-socket": {}
},

为了响应传入的 HTTP 请求,我们需要创建一个新的 TCPSocket,它在已知端口(例如 8080)上监听。

var socket = navigator.mozTCPSocket.listen(8080);

当收到传入的 HTTP 请求时,TCPSocket 需要通过onconnect 处理程序处理请求。onconnect 处理程序将接收一个用于服务请求的TCPSocket 对象。您收到的TCPSocket 将在每次收到额外的 HTTP 请求数据时调用其自己的ondata 处理程序。

socket.onconnect = function(connection) {
  connection.ondata = function(evt) {
    console.log(evt.data);
  };
};

通常,HTTP 请求会导致ondata 处理程序被调用一次。但是,在 HTTP 请求有效负载非常大的情况下(例如文件上传),ondata 处理程序将在每次缓冲区被填充时触发,直到整个请求有效负载被传递。

为了响应 HTTP 请求,我们必须将数据发送到从onconnect 处理程序收到的TCPSocket

connection.ondata = function(evt) {
  var response = 'HTTP/1.1 200 OK\r\n';
  var body = 'Hello World!';
  
  response += 'Content-Length: ' + body.length + '\r\n';
  response += '\r\n';
  response += body;
  
  connection.send(response);
  connection.close();
};

上面的示例发送了一个正确的 HTTP 响应,主体中包含“Hello World!”。有效的 HTTP 响应必须包含一个状态行,该状态行包含 HTTP 版本HTTP/1.1、响应代码200 和响应原因OK,后面是 CR+LF \r\n 字符序列。状态行紧随其后的是 HTTP 标头,每行一个,用 CR+LF 字符序列分隔。在标头之后,需要另一个 CR+LF 字符序列将标头与 HTTP 响应的主体分隔开。

FxOS Web 服务器

现在,我们很可能希望超越简单的静态“Hello World!”响应,去做一些事情,比如解析 URL 路径并从 HTTP 请求中提取参数,以便用动态内容进行响应。碰巧我之前已经实现了一个基本功能的 HTTP 服务器库,您可以将其包含在您自己的 Firefox OS 应用程序中!

FxOS Web 服务器 可以解析 HTTP 请求的所有部分,以获取各种内容类型,包括application/x-www-form-urlencodedmultipart/form-data。它还可以优雅地处理用于文件上传的大型 HTTP 请求,并可以发送用于提供诸如图像和视频等内容的大型二进制响应。您可以下载 GitHub 上的 FxOS Web 服务器的源代码,以便手动将其包含在您的项目中,或者您可以使用 Bower 获取最新版本。

bower install justindarc/fxos-web-server --save

下载源代码后,您需要使用<script> 标签或类似 RequireJS 的模块加载器将dist/fxos-web-server.js 包含到您的应用程序中。

简单的文件存储应用程序

接下来,我将向您展示如何使用 FxOS Web 服务器 来构建一个简单的 Firefox OS 应用程序,该应用程序可以让您像使用便携式闪存驱动器一样使用您的移动设备来存储和检索文件。您可以在 GitHub 上查看完成产品的源代码。

在我们深入代码之前,让我们设置我们的 应用程序清单,以获得访问 DeviceStorageTCPSocket 的权限。

{
  "version": "1.0.0",
  "name": "WebDrive",
  "description": "A Firefox OS app for storing files from a web browser",
  "launch_path": "/index.html",
  "icons": {
    "128": "/icons/icon_128.png"
  },
  "type": "privileged",
  "permissions": {
    "device-storage:sdcard": { "access": "readwrite" },
    "tcp-socket": {}
  }
}

我们的应用程序不需要太多 UI,只需要在设备上的“WebDrive”文件夹中列出文件,因此我们的 HTML 会非常简单。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>WebDrive</title>
  <meta name="description" content="A Firefox OS app for storing files from a web browser">
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
  <script src="bower_components/fxos-web-server/dist/fxos-web-server.js"></script>
  <script src="js/storage.js"></script>
  <script src="js/app.js"></script>
</head>
<body>
  <h1>WebDrive</h1>
  <hr>
  <h3>Files</h3>
  <ul id="list"></ul>
</body>
</html>

如您所见,除了 app.js 之外,我还包含了 fxos-web-server.js。我还包含了一个名为 storage.js 的 DeviceStorage 帮助程序模块,因为枚举文件可能变得比较复杂。这将有助于我们专注于与手头任务相关的代码。

我们需要做的第一件事是创建HTTPServerStorage 对象的新实例。

var httpServer = new HTTPServer(8080);
var storage = new Storage('sdcard');

这将在端口 8080 上初始化一个新的HTTPServer,并创建一个指向设备 SD 卡的新Storage 帮助程序实例。为了让我们的HTTPServer 实例发挥作用,我们必须监听并处理“request”事件。当收到传入的 HTTP 请求时,HTTPServer 将发出一个“request”事件,将解析后的 HTTP 请求作为HTTPRequest 对象传递给事件处理程序。

HTTPRequest 对象包含 HTTP 请求的各种属性,包括 HTTP 方法、路径、标头、查询参数和表单数据。除了请求数据之外,“request”事件处理程序还传递了一个HTTPResponse 对象。HTTPResponse 对象允许我们以文件或字符串的形式发送响应,并设置响应标头。

httpServer.addEventListener('request', function(evt) {
  var request  = evt.request;
  var response = evt.response;

  // Handle request here...
});

当用户请求我们 Web 服务器的根 URL 时,我们希望向他们显示设备上“WebDrive”文件夹中存储的文件列表,以及一个用于上传新文件的FileInput。为了方便起见,我们将创建两个帮助程序函数来生成我们要在 HTTP 响应中发送的 HTML 字符串。一个只是生成文件列表,我们将在本地重新使用它来显示设备上的文件,另一个将生成要在 HTTP 响应中发送的整个 HTML 文档。

function generateListing(callback) {
  storage.list('WebDrive', function(directory) {
    if (!directory || Object.keys(directory).length === 0) {
      callback('<li>No files found</li>');
      return;
    }

    var html = '';
    for (var file in directory) {
      html += `<li><a href="/${encodeURIComponent(file)}" target="_blank">${file}</a></li>`;
    }

    callback(html);
  });
}

function generateHTML(callback) {
  generateListing(function(listing) {
    var html =
`<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>WebDrive</title>
</head>
<body>
  <h1>WebDrive</h1>
  <form method="POST" enctype="multipart/form-data">
    <input type="file" name="file">
    <button type="submit">Upload</button>
  </form>
  <hr>
  <h3>Files</h3>
  <ul>${listing}</ul>
</body>
</html>`;

    callback(html);
  });
}

您会注意到,我们使用的是 ES6 模板字符串 来生成我们的 HTML。如果您不熟悉 模板字符串,它们允许我们拥有多行字符串,这些字符串会自动包含空格和换行符,而且我们可以执行基本字符串插值,它会自动插入${} 语法中的值。这对于生成 HTML 特别有用,因为它允许我们跨越多行,因此当嵌入到 JavaScript 代码中时,我们的模板标记保持高度可读性。

现在我们有了帮助程序函数,让我们在“request”事件处理程序中发送我们的 HTML 响应。

httpServer.addEventListener('request', function(evt) {
  var request  = evt.request;
  var response = evt.response;

  generateHTML(function(html) {
    response.send(html);
  });
});

截至目前,我们的“request”事件处理程序将始终使用设备上“WebDrive”文件夹中所有文件的 HTML 页面进行响应。但是,在我们能够接收任何请求之前,我们必须首先启动HTTPServer。我们将在 DOM 准备好后执行此操作,并且在执行此操作的同时,我们还将在本地呈现文件列表。

window.addEventListener('DOMContentLoaded', function(evt) {
  generateListing(function(listing) {
    list.innerHTML = listing;
  });
  
  httpServer.start();
});

我们还应该确保在应用程序终止时停止HTTPServer,否则网络套接字可能永远不会被释放。

window.addEventListener('beforeunload', function(evt) {
  httpServer.stop();
});

此时,我们的 Web 服务器应该已经启动并运行!使用 WebIDE 在您的设备或模拟器上安装该应用程序。安装完成后,启动该应用程序,并将您的桌面浏览器指向您设备的 IP 地址和端口 8080(例如:http://10.0.1.12:8080)。

您应该会在您的桌面浏览器中看到我们的索引页面加载,但上传表单尚未连接,并且如果您在设备上的“WebDrive”文件夹中存在任何文件,则无法下载它们。让我们首先通过创建一个新的帮助程序函数来保存HTTPRequest 中接收到的文件来连接文件上传。

function saveFile(file, callback) {
  var arrayBuffer = BinaryUtils.stringToArrayBuffer(file.value);
  var blob = new Blob([arrayBuffer]);
  
  storage.add(blob, 'WebDrive/' + file.metadata.filename, callback);
}

此函数将首先使用 fxos-web-server.js 附带的BinaryUtils 实用程序将文件的內容转换为 ArrayBuffer。然后,我们创建一个 Blob,将其传递给我们的Storage 帮助程序,以将其保存在 SD 卡上的“WebDrive”文件夹中。请注意,文件名可以从文件的metadata 对象中提取,因为它使用 ‘multipart/form-data’ 编码传递到服务器。

现在我们有了用于保存上传文件的帮助程序,让我们在“request”事件处理程序中将其连接起来。

httpServer.addEventListener('request', function(evt) {
  var request  = evt.request;
  var response = evt.response;

  if (request.method === 'POST' && request.body.file) {
    saveFile(request.body.file, function() {
      generateHTML(function(html) {
        response.send(html);
      });
      
      generateListing(function(html) {
        list.innerHTML = html;
      });
    });
    
    return;
  }

  generateHTML(function(html) {
    response.send(html);
  });
});

现在,每当收到包含请求主体中“file”参数的 HTTPPOST 请求时,我们都会将该文件保存到 SD 卡上的“WebDrive”文件夹中,并使用更新的文件列表索引页面进行响应。同时,我们还将更新本地设备上的文件列表,以显示新添加的文件。

我们应用程序中唯一剩下的部分是连接下载文件的功能。再次,让我们更新“request”事件处理程序来执行此操作。

httpServer.addEventListener('request', function(evt) {
  var request  = evt.request;
  var response = evt.response;

  if (request.method === 'POST' && request.body.file) {
    saveFile(request.body.file, function() {
      generateHTML(function(html) {
        response.send(html);
      });
      
      generateListing(function(html) {
        list.innerHTML = html;
      });
    });
    
    return;
  }

  var path = decodeURIComponent(request.path);
  if (path !== '/') {
    storage.get('WebDrive' + path, function(file) {
      if (!file) {
        response.send(null, 404);
        return;
      }
      
      response.headers['Content-Type'] = file.type;
      response.sendFile(file);
    });
    
    return;
  }

  generateHTML(function(html) {
    response.send(html);
  });
});

这一次,我们的“请求”事件处理程序将检查请求的路径,以查看是否请求了根目录以外的 URL。如果是,我们假设用户正在请求下载文件,然后我们使用我们的 `Storage` 助手获取该文件。如果找不到文件,我们将返回 HTTP 404 错误。否则,我们将响应头中的“Content-Type”设置为文件的 MIME 类型,并将文件与 `HTTPResponse` 对象一起发送。

您现在可以使用 WebIDE 将应用程序重新安装到您的设备或模拟器,然后再次将您的桌面浏览器指向您设备的 IP 地址(端口 8080)。现在,您应该能够使用您的桌面浏览器从您的设备上传下载文件!

将 Web 服务器嵌入 Firefox OS 应用程序中所带来的潜在用例几乎是无限的。您不仅可以从您的设备向桌面浏览器提供 Web 内容,就像我们刚刚在此处所做的那样,而且您还可以从一台设备向另一台设备提供内容。这也意味着您可以使用 HTTP 在同一设备上的应用程序之间发送和接收数据!自从问世以来,FxOS Web Server 一直作为 Mozilla 的一些激动人心的实验的基础。

  • wifi-columns

    Guillaume Marty 将 FxOS Web Server 与他令人惊叹的 jsSMS Master System/Game Gear 模拟器相结合,以结合 WiFi Direct 在两台设备之间实现多人游戏。

  • sharing

    Gaia 团队的几位成员已经使用 FxOS Web Server 和 dns-sd.js 创建了一个应用程序,允许用户通过 WiFi 发现并与朋友分享应用程序。

  • firedrop

    我个人已经使用 FxOS Web Server 构建了一个应用程序,它允许您使用 WiFi Direct 与附近的用户共享文件,而无需互联网连接。您可以在此处查看应用程序的实际操作

我期待着看到 FxOS Web Server 下一步将构建的所有令人兴奋的事情!

关于 Justin D'Arcangelo

Mozilla 的软件工程师,创建尖端的移动 Web 应用程序。自豪的父亲、丈夫、音乐家、黑胶唱片爱好者,以及无所不能的修补匠。

Justin D'Arcangelo 的更多文章…


14 条评论

  1. Tom4hawk

    太棒了。我真的需要购买一部 FirefoxOS 手机来进行修补。
    我不是最大的 JS 粉(就目前而言,我讨厌我用它编写的每一行代码…),但我将对 FOS 另眼相待;)
    我应该购买哪款手机?我仍然是一名失业的大学生,所以价格很重要;)

    2015 年 2 月 9 日 下午 4:33

    1. Havi Hoffman [编辑]

      @Tom4hawk 以下列出了目前出售的 Firefox OS 设备,但它们目前在美国不可用:https://www.mozilla.org/en-US/firefox/os/devices/

      我们应该很快就会有新的 Flame 参考设备库存。(https://mdn.org.cn/en-US/Firefox_OS/Phone_guide/Flame

      与此同时,如果您愿意进行修补,有一些设备可以运行 FirefoxOS。以下是一个这样的项目:https://www.mozilla.org/en-US/firefox/os/devices/

      2015 年 2 月 10 日 上午 12:26

      1. Tom4hawk

        我来自波兰,所以它目前在美国不可用并不是问题;)
        Flame 看起来很棒,但不幸的是,它对我来说太贵了…
        另一方面,ZTE Open C 的价格范围更能接受,但我找不到如何将其升级到更新的 FFOS(除非升级 Geck 和 Gaia 表示更新的 FFOS 版本:https://mdn.org.cn/en-US/Firefox_OS/Phone_guide/ZTE_OPEN_C)。

        2015 年 2 月 11 日 上午 7:32

        1. Justin D’Arcangelo

          构建您自己的 Gecko+Gaia 允许您安装更新的 FxOS 版本。此外,Open C 与我们的 Flame 参考手机兼容,因此您可以按照此处的说明对 Open C 进行 root 操作并安装更新的 FxOS 构建:http://paulrouget.com/e/openc/

          2015 年 2 月 11 日 上午 9:48

          1. Tom4hawk

            @Havi Hoffman,@Justin D’Arcangelo
            感谢您的帮助。

            我将在下个月购买 ZTE。

            2015 年 2 月 11 日 下午 1:47

  2. Doug Reeder

    其他一些有用的应用程序将是分享手机上的照片,例如 Zap Photoshare(http://hominidsoftware.com/zapphotoshare/index.html)或协同文本编辑,例如 Etherpad(http://etherpad.org/)。这两者都依赖于 node.js,因此无法移植到 Firefox OS。

    不幸的是,在手机上启动和运行 Web 服务器只是成功的一半。获取到另一台设备上的浏览器的 URL 非常困难。NFC 允许与许多(但并非所有)移动设备共享 URL。DNS-SD 只需要软件(而不是硬件)支持,但唯一允许用户查看 DNS-SD 广告的通用浏览器是桌面 Safari(并且默认情况下已禁用首选项)。桌面 Firefox 和 Firefox OS 的支持将使 DNS-SD 更有用。

    缺少这些,移动设备可以发送电子邮件或即时消息,前提是您拥有收件人的地址。通常,从 Zap 分享 URL 的最快方法是在目标浏览器中键入 URL,这不是很好的用户体验。

    如果您想避免使用蜂窝数据,您还需要在同一个 WiFi 网络上,而这比您想象的要少见。

    但是,当您可以让所有内容协同工作时,那就是神奇!https://www.youtube.com/watch?v=2oLfKqpUJT4

    2015 年 2 月 9 日 下午 8:33

    1. Justin D’Arcangelo

      Doug,我在文章末尾提到的“分享”应用程序使用 DNS-SD,方法是包含我的 dns-sd.js 库:https://github.com/justindarc/dns-sd.js

      因此,实际上可以使用此库构建 FxOS 应用程序,以进行服务发现。请关注未来关于 dns-sd.js 的文章:-)

      2015 年 2 月 10 日 上午 8:20

      1. Doug Reeder

        是的,我对 FxOS 中的 dns-sd.js 和 NFC 等发展感到兴奋(https://mdn.org.cn/en-US/docs/Web/API/NFC_API)!WiFi-Direct 有助于解决不在同一个网络上的问题。

        难题的另一部分是提高用户对非专有共享技术的意识。

        2015 年 2 月 10 日 上午 9:20

        1. Doug Reeder

          Aaaaand NFC API 现在在 FxOS 2.2 中可供第三方应用程序使用:https://bugzilla.mozilla.org/show_bug.cgi?id=1102019

          2015 年 2 月 25 日 下午 3:44

  3. Steven Male

    我只想感谢你们多年来的所有黑客行为!使 Mozilla 更加强大(和有趣)。

    干杯!

    史蒂文!

    2015 年 2 月 10 日 上午 12:26

  4. voracity

    是否可以使用 HTTP/2?我担心 HTTP/2 的一件事是,它的复杂性和要求可能会限制这种创造力,但如果有人可以证明它适用于这种任务,我的担忧就会大部分消除。

    2015 年 2 月 10 日 下午 8:52

    1. Justin D’Arcangelo

      目前,FxOS Web Server 只支持 HTTP/1.1。您是对的,HTTP/2 在理解规范方面确实增加了复杂性,这使得实现与 HTTP/2 兼容的 Web 服务器更加困难。但是,HTTP/1.1 不会很快消失。整个 Web 几乎都是基于 HTTP/1.1 的,因此我预计 HTTP/1.1 客户端/服务器将在未来很多年内存在。

      2015 年 2 月 11 日 上午 8:33

  5. niutech

    这很棒,因为它将允许 FxOS 用于 RPi 等微型计算机来提供数据流。这也会出现在桌面 Firefox 上吗,以提供 Opera Unite 功能?

    与 TCPSocket API 相似的是 chrome.sockets.tcpServer。我希望它们之间能够相互操作。

    另一个有趣的项目是 PeerServer,它使用 WebRTC 从您的浏览器提供数据。

    2015 年 2 月 11 日 下午 1:37

  6. Erica

    这真是太酷了!而且宝宝真可爱!

    2015 年 2 月 13 日 下午 3:44

本文的评论已关闭。