【问题标题】:Method for streaming data from browser to server via HTTP通过 HTTP 将数据从浏览器流式传输到服务器的方法
【发布时间】:2016-06-24 07:07:21
【问题描述】:

是否有任何类似 XHR 的浏览器 API 可用于通过 HTTP 将二进制流式传输到服务器?

我想随着时间的推移发出 HTTP PUT 请求并以编程方式创建数据。我不想一次创建所有这些数据,因为内存中可能存在大量数据。一些伪代码来说明我的意思:

var dataGenerator = new DataGenerator(); // Generates 8KB UInt8Array every second
var streamToWriteTo;
http.put('/example', function (requestStream) {
  streamToWriteTo = requestStream;
});

dataGenerator.on('data', function (chunk) {
  if (!streamToWriteTo) {
    return;
  }
  streamToWriteTo.write(chunk);
});

我目前有一个 Web 套接字解决方案,但我更喜欢常规 HTTP,以便与一些现有的服务器端代码更好地互操作。

编辑:我可以使用最前沿的浏览器 API。我正在查看 Fetch API,因为它支持请求正文的 ArrayBuffers、DataViews、Files 等。如果我能以某种方式伪造这些对象之一,以便我可以将 Fetch API 与动态数据一起使用,那将适用于我。我尝试创建一个代理对象,看看是否调用了我可以修补的方法。不幸的是,浏览器(至少在 Chrome 中)似乎正在阅读本机代码,而不是在 JS 领域。但是,如果我错了,请纠正我。

【问题讨论】:

  • 预期的结果是,在未来某个时刻,所有发送到服务器的数据都可以在单个 URI 上检索吗?发送到服务器的每个数据主体是否应该创建一个包含数据的新且不同的 URI?或者,发送到服务器的每个数据主体是否应该覆盖以前发送的数据?
  • @guest271314 服务器端发生的事情无关紧要。但是,我确实需要在单个 HTTP 请求中传输数据。也就是说,当 HTTP 请求启动时,我还没有所有的数据。数据是动态创建的并动态流式传输。我需要在创建数据时 PUT/POST 数据。这有意义吗?
  • js 在 Question 没有做到这一点?您可以将setInterval 替换为在创建时发送数据的方法。当进程开始时,总共有Content-Length 的数据可用吗?
  • @mico 您链接到的问题是处理响应数据,不适合流式传输请求数据。此外,我想做的事情是使用 Web 套接字,但我有一个边缘情况,我需要解决无法使用 Web 套接字的情况。幸运的是,我不需要 Web 套接字的所有功能……我只需要向服务器发送数据。只是数据在创建之前是未知的。
  • @AndreyKiselev 一点也不奇怪。但是,对于服务器端,我已经有了 Web 套接字,这是一个比 WebRTC 简单得多的解决方案。我想直接使用 HTTP。

标签: javascript http browser xmlhttprequest


【解决方案1】:

我不知道如何使用纯 HTML5 API 来做到这一点,但一种可能的解决方法是使用 Chrome 应用程序作为后台服务来为网页提供附加功能。如果您已经愿意使用开发浏览器并启用实验性功能,那么这似乎只是进一步的增量步骤。

Chrome 应用程序可以调用chrome.sockets.tcp API,您可以在该 API 上实现您想要的任何协议,包括 HTTP 和 HTTPS。这将为实现流式传输提供灵活性。

普通网页可以使用chrome.runtime API 与应用程序交换消息,只要应用程序declares this usage。这将允许您的网页对您的应用进行异步调用。

我编写了这个简单的应用程序作为概念证明:

manifest.json

{
  "manifest_version" : 2,

  "name" : "Streaming Upload Test",
  "version" : "0.1",

  "app": {
    "background": {
      "scripts": ["background.js"]
    }
  },

  "externally_connectable": {
    "matches": ["*://localhost/*"]
  },

  "sockets": {
    "tcp": {
      "connect": "*:*"
    }
  },

  "permissions": [
  ]
}

background.js

var mapSocketToPort = {};

chrome.sockets.tcp.onReceive.addListener(function(info) {
  var port = mapSocketToPort[info.socketId];
  port.postMessage(new TextDecoder('utf-8').decode(info.data));
});

chrome.sockets.tcp.onReceiveError.addListener(function(info) {
  chrome.sockets.tcp.close(info.socketId);
  var port = mapSocketToPort[info.socketId];
  port.postMessage();
  port.disconnect();
  delete mapSocketToPort[info.socketId];
});

// Promisify socket API for easier operation sequencing.
// TODO: Check for error and reject.
function socketCreate() {
  return new Promise(function(resolve, reject) {
    chrome.sockets.tcp.create({ persistent: true }, resolve);
  });
}

function socketConnect(s, host, port) {
  return new Promise(function(resolve, reject) {
    chrome.sockets.tcp.connect(s, host, port, resolve);
  });
}

function socketSend(s, data) {
  return new Promise(function(resolve, reject) {
    chrome.sockets.tcp.send(s, data, resolve);
  });
}

chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    if (!port.state) {
      port.state = msg;

      port.chain = socketCreate().then(function(info) {
        port.socket = info.socketId;
        mapSocketToPort[port.socket] = port;
        return socketConnect(port.socket, 'httpbin.org', 80);
      }).then(function() {
        // TODO: Layer TLS if needed.
      }).then(function() {
        // TODO: Build headers from the request.
        // TODO: Use Transfer-Encoding: chunked.
        var headers =
            'PUT /put HTTP/1.0\r\n' +
            'Host: httpbin.org\r\n' +
            'Content-Length: 17\r\n' +
            '\r\n';
        return socketSend(port.socket, new TextEncoder('utf-8').encode(headers).buffer);
      });
    }
    else {
      if (msg) {
        port.chain = port.chain.then(function() {
          // TODO: Use chunked encoding.
          return socketSend(port.socket, new TextEncoder('utf-8').encode(msg).buffer);
        });
      }
    }
  });
});

此应用程序没有用户界面。它侦听连接并向http://httpbin.org/put 发出硬编码的PUT 请求(httpbin 是一个有用的测试站点,但请注意does not support chunked encoding)。 PUT 数据(目前硬编码为 17 个八位字节)从客户端流入(根据需要使用尽可能少或尽可能多的消息)并发送到服务器。来自服务器的响应流回客户端。

这只是一个概念证明。一个真正的应用程序可能应该:

  • 连接到任何主机和端口。
  • 使用传输编码:分块。
  • 表示流数据结束。
  • 处理套接字错误。
  • 支持 TLS(例如 Forge

这是一个使用应用即服务执行流式上传(17 个八位字节)的示例网页(请注意,您必须配置自己的应用 ID):

<pre id="result"></pre>
<script>
 var MY_CHROME_APP_ID = 'omlafihmmjpklmnlcfkghehxcomggohk';

 function streamingUpload(url, options) {
   // Open a connection to the Chrome App. The argument must be the 
   var port = chrome.runtime.connect(MY_CHROME_APP_ID);

   port.onMessage.addListener(function(msg) {
     if (msg)
       document.getElementById("result").textContent += msg;
     else
       port.disconnect();
   });

   // Send arguments (must be JSON-serializable).
   port.postMessage({
     url: url,
     options: options
   });

   // Return a function to call with body data.
   return function(data) {
     port.postMessage(data);
   };
 }

 // Start an upload.
 var f = streamingUpload('https://httpbin.org/put', { method: 'PUT' });

 // Stream data a character at a time.
 'how now brown cow'.split('').forEach(f);
</script>

当我在安装了应用程序的 Chrome 浏览器中加载此网页时,httpbin 返回:

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 19 Jun 2016 16:54:23 GMT
Content-Type: application/json
Content-Length: 240
Connection: close
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
  "args": {}, 
  "data": "how now brown cow", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Content-Length": "17", 
    "Host": "httpbin.org"
  }, 
  "json": null, 
  "origin": "[redacted]", 
  "url": "http://httpbin.org/put"
}

【讨论】:

  • 这可以拯救我的一天!
【解决方案2】:



我目前正在寻找完全相同的东西(通过 Ajax 上传)。 我目前的发现,看起来好像我们正在搜索浏览器功能设计的最前沿;-)

XMLHttpRequest definition 在第 4 步中告诉 bodyinit 的内容提取是(或可以是) readablestream.

我仍在(作为非网络开发人员)搜索有关如何创建这样的东西并将数据馈送到“可读流”的“另一端”的信息(即应该是“可写流”,但我还没有没找到)。

如果您找到了实施这些设计计划的方法,也许您在搜索方面做得更好,并且可以在此处发布。

^5
斯文

【讨论】:

【解决方案3】:

一种利用ReadableStream 流式传输任意数据的方法; RTCDataChannelUint8Array 的形式发送和接收任意数据; TextEncoder 创建8000 存储在Uint8Array 中的随机数据字节,TextDecoderRTCDataChannel 返回的Uint8Array 解码为字符串进行演示,注意也可以使用FileReader .readAsArrayBuffer 和@987654328 @ 这里。

标记和脚本代码是根据MDN - WebRTC: Simple RTCDataChannel sample 的示例修改的,包括adapter.js,其中包含RTCPeerConnection 助手; Creating your own readable stream.

另请注意,当传输的总字节数达到 8000 * 8 时,示例流被取消:64000

(function init() {
  var interval, reader, stream, curr, len = 0,
    totalBytes = 8000 * 8,
    data = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
    randomData = function randomData() {
      var encoder = new TextEncoder();
      var currentStream = "";
      for (var i = 0; i < 8000; i++) {
        currentStream += data[Math.floor(Math.random() * data.length)]
      }
      return encoder.encode(currentStream)
    },
    // optionally reconnect to stream if cancelled
    reconnect = function reconnect() {
      connectButton.disabled = false;
      startup()
    };

  // Define "global" variables

  var connectButton = null;
  var disconnectButton = null;
  var messageInputBox = null;
  var receiveBox = null;

  var localConnection = null; // RTCPeerConnection for our "local" connection
  // adjust this to remote address; or use `ServiceWorker` `onfetch`; other
  var remoteConnection = null; // RTCPeerConnection for the "remote"

  var sendChannel = null; // RTCDataChannel for the local (sender)
  var receiveChannel = null; // RTCDataChannel for the remote (receiver)

  // Functions

  // Set things up, connect event listeners, etc.

  function startup() {
    connectButton = document.getElementById("connectButton");
    disconnectButton = document.getElementById("disconnectButton");
    messageInputBox = document.getElementById("message");
    receiveBox = document.getElementById("receivebox");

    // Set event listeners for user interface widgets

    connectButton.addEventListener("click", connectPeers, false);
    disconnectButton.addEventListener("click", disconnectPeers, false);
  }

  // Connect the two peers. Normally you look for and connect to a remote
  // machine here, but we"re just connecting two local objects, so we can
  // bypass that step.

  function connectPeers() {
    // Create the local connection and its event listeners
    if (len < totalBytes) {
      localConnection = new RTCPeerConnection();

      // Create the data channel and establish its event listeners
      sendChannel = localConnection.createDataChannel("sendChannel");
      sendChannel.onopen = handleSendChannelStatusChange;
      sendChannel.onclose = handleSendChannelStatusChange;

      // Create the remote connection and its event listeners

      remoteConnection = new RTCPeerConnection();
      remoteConnection.ondatachannel = receiveChannelCallback;

      // Set up the ICE candidates for the two peers

      localConnection.onicecandidate = e => 
        !e.candidate || remoteConnection.addIceCandidate(e.candidate)
      .catch(handleAddCandidateError);

      remoteConnection.onicecandidate = e => 
        !e.candidate || localConnection.addIceCandidate(e.candidate)
      .catch(handleAddCandidateError);

      // Now create an offer to connect; this starts the process

      localConnection.createOffer()
      .then(offer => localConnection.setLocalDescription(offer))
      .then(() => remoteConnection
                 .setRemoteDescription(localConnection.localDescription)
       )
      .then(() => remoteConnection.createAnswer())
      .then(answer => remoteConnection
                      .setLocalDescription(answer)
       )
      .then(() => localConnection
                 .setRemoteDescription(remoteConnection.localDescription)
      )
      // start streaming connection
      .then(sendMessage)
      .catch(handleCreateDescriptionError);
    } else {

      alert("total bytes streamed:" + len)
    }

  }

  // Handle errors attempting to create a description;
  // this can happen both when creating an offer and when
  // creating an answer. In this simple example, we handle
  // both the same way.

  function handleCreateDescriptionError(error) {
    console.log("Unable to create an offer: " + error.toString());
  }

  // Handle successful addition of the ICE candidate
  // on the "local" end of the connection.

  function handleLocalAddCandidateSuccess() {
    connectButton.disabled = true;
  }

  // Handle successful addition of the ICE candidate
  // on the "remote" end of the connection.

  function handleRemoteAddCandidateSuccess() {
    disconnectButton.disabled = false;
  }

  // Handle an error that occurs during addition of ICE candidate.

  function handleAddCandidateError() {
    console.log("Oh noes! addICECandidate failed!");
  }

  // Handles clicks on the "Send" button by transmitting
  // a message to the remote peer.

  function sendMessage() {

    stream = new ReadableStream({
      start(controller) {
          interval = setInterval(() => {
            if (sendChannel) {
              curr = randomData();
              len += curr.byteLength;
              // queue current stream
              controller.enqueue([curr, len, sendChannel.send(curr)]);

              if (len >= totalBytes) {
                controller.close();
                clearInterval(interval);
              }
            }
          }, 1000);
        },
        pull(controller) {
          // do stuff during stream
          // call `releaseLock()` if `diconnect` button clicked
          if (!sendChannel) reader.releaseLock();
        },
        cancel(reason) {
          clearInterval(interval);
          console.log(reason);
        }
    });

    reader = stream.getReader({
      mode: "byob"
    });

    reader.read().then(function process(result) {
        if (result.done && len >= totalBytes) {
          console.log("Stream done!");
          connectButton.disabled = false;
          if (len < totalBytes) reconnect();
          return;
        }

        if (!result.done && result.value) {
          var [currentStream, totalStreamLength] = [...result.value];
        }

        if (result.done && len < totalBytes) {
          throw new Error("stream cancelled")
        }

        console.log("currentStream:", currentStream
                   , "totalStremalength:", totalStreamLength
                   , "result:", result);
        return reader.read().then(process);
      })
      .catch(function(err) {
        console.log("catch stream cancellation:", err);
        if (len < totalBytes) reconnect()
      });

    reader.closed.then(function() {
      console.log("stream closed")
    })

  }

  // Handle status changes on the local end of the data
  // channel; this is the end doing the sending of data
  // in this example.

  function handleSendChannelStatusChange(event) {
    if (sendChannel) {
      var state = sendChannel.readyState;

      if (state === "open") {
        disconnectButton.disabled = false;
        connectButton.disabled = true;
      } else {
        connectButton.disabled = false;
        disconnectButton.disabled = true;
      }
    }
  }

  // Called when the connection opens and the data
  // channel is ready to be connected to the remote.

  function receiveChannelCallback(event) {
    receiveChannel = event.channel;
    receiveChannel.onmessage = handleReceiveMessage;
    receiveChannel.onopen = handleReceiveChannelStatusChange;
    receiveChannel.onclose = handleReceiveChannelStatusChange;
  }

  // Handle onmessage events for the receiving channel.
  // These are the data messages sent by the sending channel.

  function handleReceiveMessage(event) {
    var decoder = new TextDecoder();
    var data = decoder.decode(event.data);
    var el = document.createElement("p");
    var txtNode = document.createTextNode(data);

    el.appendChild(txtNode);
    receiveBox.appendChild(el);
  }

  // Handle status changes on the receiver"s channel.

  function handleReceiveChannelStatusChange(event) {
    if (receiveChannel) {
      console.log("Receive channel's status has changed to " +
        receiveChannel.readyState);
    }

    // Here you would do stuff that needs to be done
    // when the channel"s status changes.
  }

  // Close the connection, including data channels if they"re open.
  // Also update the UI to reflect the disconnected status.

  function disconnectPeers() {

    // Close the RTCDataChannels if they"re open.

    sendChannel.close();
    receiveChannel.close();

    // Close the RTCPeerConnections

    localConnection.close();
    remoteConnection.close();

    sendChannel = null;
    receiveChannel = null;
    localConnection = null;
    remoteConnection = null;

    // Update user interface elements


    disconnectButton.disabled = true;
    // cancel stream on `click` of `disconnect` button, 
    // pass `reason` for cancellation as parameter
    reader.cancel("stream cancelled");
  }

  // Set up an event listener which will run the startup
  // function once the page is done loading.

  window.addEventListener("load", startup, false);
})();

plnkr http://plnkr.co/edit/cln6uxgMZwE2EQCfNXFO?p=preview

【讨论】:

  • 有趣。请注意,您的答案缺少关键步骤,即必须在 Chrome 中启用实验标志才能使用ReadableStream。此外,Disconnect 按钮会引发错误:Cannot read property 'cancel' of undefined。但是,OP 的问题需要 HTTP (PUT/POST),但您的解决方案使用 WebRTC。你无法让fetchReadableStream 像你intended 一样工作吗?
  • @tony19 “请注意,您的答案缺少关键步骤,即必须在 Chrome 中启用实验性标志” 公平点。 OP 似乎熟悉浏览器技术“我可以使用最前沿的浏览器 API”,虽然是的,但尝试将 --enable-experimental-web-platform-features 标志设置为 chromium。 “OP 的问题需要 HTTP (PUT/POST)”, 没有从问题中收集,cmets 只能使用 PUTPOST “但我有一个极端情况我需要解决""Cannot read property 'cancel' of undefined" 此处不会发生错误。不能单独使用fetch
  • 真的吗? Title: "to server via HTTP", Body: "to a server over HTTP", "I want to make an HTTP PUT request", "prefer regular HTTP for better互操作”。对我来说,这一切都意味着PUTPOST。 :-) 但我猜他会考虑 WebRTC,因为他提到了“最前沿的 API”。
  • @tony19 另一种选择是使用browserifynodejs 等效PUTPOST、可写流功能和实现转换为可在浏览器中使用的文件。不确定文件会有多长。
  • @Brad 另请参阅Stream ConsumersStream Producers
【解决方案4】:

我认为简短的回答是否定的。截至撰写此回复时(2021 年 11 月),这在任何主要浏览器中均不可用。

长答案是:

我认为您正在使用 Fetch API 寻找正确的位置。 ReadableStream 目前是 Request 构造函数的 body 属性的有效类型:
https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#parameters

但是,如果您查看浏览器支持矩阵,可悲的是:
https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#browser_compatibility
您可以看到“在请求正文中发送 ReadableStream”对于所有主要浏览器仍然为否。虽然它目前在某些浏览器(包括 Chrome)中以实验模式提供。

这里有一个很好的关于如何在实验模式下进行操作的教程:
https://web.dev/fetch-upload-streaming/

查看帖子的日期和在此功能上完成的工作,我认为这项技术很明显处于停滞状态,我们可能不会很快看到它。因此,遗憾的是,WebSockets 可能仍然是我们为数不多的好选择之一(用于无限制的流传输):
https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API

【讨论】:

  • 嘿,流式传输请求正文实际上正在 Chrome 原始试验中!不幸的是,作者决定禁止我们在 HTTP/1.1 上使用它。仅限 HTTP/2。
  • 谢谢@brad,知道这很有趣。我猜 HTTP/1.1 中使用的分块数据编码存在一些阻力。
【解决方案5】:

您可以使用PromisesetTimeout、递归。另见PUT vs POST in REST

var count = 0, total = 0, timer = null, d = 500, stop = false, p = void 0
, request = function request () {
              return new XMLHttpRequest()
            };
function sendData() {
  p = Promise.resolve(generateSomeBinaryData()).then(function(data) { 
    var currentRequest = request();
    currentRequest.open("POST", "http://example.com");
    currentRequest.onload = function () {
      ++count; // increment `count`
      total += data.byteLength; // increment total bytes posted to server
    }

    currentRequest.onloadend = function () {
      if (stop) { // stop recursion
        throw new Error("aborted") // `throw` error to `.catch()`
      } else {
        timer = setTimeout(sendData, d); // recursively call `sendData`
      }
    }
    currentRequest.send(data); // `data`: `Uint8Array`; `TypedArray`
    return currentRequest; // return `currentRequest` 
  });
  return p // return `Promise` : `p`
}

var curr = sendData();

curr.then(function(current) {
  console.log(current) // current post request
})
.catch(function(err) {
  console.log(e) // handle aborted `request`; errors
});

【讨论】:

  • 感谢您对此进行破解。但是,您的代码会为每个块发出一个新的 HTTP 请求。我需要将所有内容保存在一个 HTTP 请求中。我在问题中做了一个新的例子,这样我们就不会被数据的生成方式所困扰。这里的问题是如何在请求进行中时继续将数据放入单个请求的请求正文中。我想知道是否有一种方法可以实现像 ArrayBufferView 这样的接口,以便 XHR 可以以这种方式使用它。或者,甚至以某种方式修改 Blob。
  • @guest271314,我不相信WritableStream 在任何浏览器中都可用。 ReadableStream 最近才在 Chrome 和 Opera 中可用。但是 Node 确实实现了WritableStream
  • @guest271314 我现在很困惑。我认为@Brad 想将数据流式传输到服务器,那么fetchReadableXXX 将如何提供帮助?
  • @guest271314 嗯。您提供的所有链接似乎都表明响应是 ReadableStream 并且没有提及有关请求正文的任何​​内容。是否有我需要查看的特定部分?无论如何,我期待您的实施。
【解决方案6】:

Server-Sent EventsWebSockets 是首选方法,但在您的情况下,您希望创建代表性状态传输、REST、API 并使用长轮询。见How do I implement basic “Long Polling”?

在客户端和服务器端都处理长轮询过程。服务器脚本和 http 服务器必须配置为支持长轮询。

除了长轮询,短轮询 (XHR/AJAX) 需要浏览器轮询服务器。

【讨论】:

  • 长轮询仅对从服务器获取响应数据有用。我需要将请求数据流式传输到服务器。我正在使用网络套接字,但是我需要解决一个奇怪的边缘情况。由于我不需要 web socket 的所有功能,因此 HTTP PUT 请求是合适的。诀窍是让浏览器让我这样做。
  • 长轮询(或 Comet)保持与服务器的 HTTP 连接打开。它对于即时发送很有用,因为 TCP 连接已经建立。这不是你想做的吗?
  • 不,这不是我想做的。我想流式传输请求正文。 XHR.send() 要求所有请求主体在调用时都存在。长轮询仅对处理完全不相关的潜在响应数据有用。
  • @RonRoyston 此外,他没有要立即发送的 blob -- 你所说的 blob 在这里是一个流,流的大小不定,它是一个数据队列,这就是为什么分块传输这里需要编码。
猜你喜欢
  • 2010-12-29
  • 1970-01-01
  • 2012-08-06
  • 2011-04-21
  • 1970-01-01
  • 1970-01-01
  • 2015-07-29
  • 2023-03-28
  • 1970-01-01
相关资源
最近更新 更多