通过HTTP将数据从浏览器传输到服务器的方法

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

我想做一个HTTP PUT请求,并且随着时间的推移以编程方式创build数据。 我不想一次创build所有这些数据,因为可能会有一些数据在内存中。 一些psueudo代码来说明我所得到的:

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套接字解决scheme,但是更喜欢普通的HTTP与现有的服务器端代码更好地互操作。

编辑:我可以使用stream血的浏览器API。 我正在查看Fetch API,因为它支持ArrayBuffers,DataViews,Files等这样的请求体。 如果我可以以某种方式伪造这些对象之一,以便我可以使用具有dynamic数据的Fetch API,那对我来说是有用的。 我试图创build一个代理对象,看看是否有任何方法被称为我可以猴子补丁。 不幸的是,浏览器(至less在Chrome浏览器)正在用本地代码阅读,而不是在JS领域。 但是,如果我错了,请纠正我。

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

Chrome应用可以调用chrome.sockets.tcp API,您可以在其中实现所需的任何协议,包括HTTP和HTTPS。 这将提供实现stream的灵活性。

一个普通的网页可以使用chrome.runtime API与App交换消息,只要App 声明了这个用法 。 这将允许您的网页asynchronous调用您的应用程序。

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

的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是一个有用的testing站点,但是不支持分块编码 )发出一个硬编码的PUT请求。 PUT数据(目前被硬编码为17个八位位组)从客户端stream入(使用尽可能less或多个消息)并发送到服务器。 来自服务器的响应被回送到客户端。

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

  • 连接到任何主机和端口。
  • 使用Transfer-Encoding:分块。
  • 指示stream数据的结束。
  • 处理套接字错误。
  • 支持TLS(例如Forge )

这是一个示例网页,使用应用程序作为服务执行stream媒体上传(17个八位字节)(请注意,您将不得不configuration您自己的应用程序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> 

当我在装有App的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" } 

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

XMLHttpRequest定义在步骤4中告诉bodyinit这个内容提取是(或可能是)一个可读的stream 。

我仍然在search(作为非web开发者)如何创build这样的事情,并提供数据到“可读stream”的另一端(即应该是一个“可写stream”,但我还没有没有find)。

也许你更好的search,如果你find一个方法来实现这些devise计划可以发布在这里。

^ 5
斯文

利用ReadableStream传输任意数据的方法; RTCDataChannel发送和/或以Uint8Arrayforms接收任意数据; TextEncoder创build存储在Uint8Array 8000字节的随机数据, TextDecoder解码由RTCDataChannel返回的string表示的RTCDataChannel ,注意也可以在这里使用FileReader .readAsArrayBuffer.readAsText

htmljs是从MDN - WebRTC: Simple RTCDataChannel sample进行修改的,包括包含RTCPeerConnection助手的RTCPeerConnection ; 创build您自己的可读stream 。

另请注意,传输总字节数达到8000 * 864000时,示例stream将被取消

 (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

你可以使用PromisesetTimeout ,recursion。 请参阅REST中的PUT vs POST

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

服务器发送的事件和WebSockets是首选的方法,但在你的情况下,你想创build一个Representational状态转移,REST,API和使用长轮询。 请参阅我如何实现基本的“长轮询”?

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

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