Método para transmitir dados do navegador para o servidor via HTTP

Há alguma API do navegador semelhante a XHR disponível para transmissão de binário para um servidor através de HTTP?

Eu quero fazer uma solicitação HTTP PUT e criar dados programaticamente, ao longo do tempo. Eu não quero criar todos esses dados de uma só vez, já que pode haver shows dele na memory. Algum psueudo-código para ilustrar o que eu estou recebendo:

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

Atualmente, tenho uma solução de soquete da web em vigor, mas prefiro o HTTP regular para interoperar melhor com algum código existente no lado do servidor.

EDIT: eu posso usar APIs de navegador de ponta. Eu estava olhando para a API de busca, uma vez que suporta ArrayBuffers, DataViews, arquivos e tal para corpos de solicitação. Se eu pudesse de alguma forma falsificar um desses objects para poder usar a API de busca com dados dynamics, isso funcionaria para mim. Eu tentei criar um object Proxy para ver se foram chamados quaisquer methods que eu poderia monkey patch. Infelizmente, parece que o navegador (pelo menos no Chrome) está fazendo a leitura em código nativo e não em terra JS. Mas, por favor, me corrija se eu estiver errado nisso.

Não sei como fazer isso com APIs HTML5 puras, mas uma solução possível é usar um aplicativo do Google Chrome como um serviço de segundo plano para fornecer resources adicionais a uma página da web. Se você já estiver disposto a usar navegadores de desenvolvimento e ativar resources experimentais, isso parece ser apenas uma etapa incremental além disso.

Os aplicativos do Chrome podem chamar a API chrome.sockets.tcp , na qual você pode implementar qualquer protocolo desejado, incluindo HTTP e HTTPS. Isso forneceria a flexibilidade para implementar o streaming.

Uma página da Web normal pode trocar mensagens com um aplicativo usando a API chrome.runtime , desde que o aplicativo declare esse uso . Isso permitiria que sua página da web fizesse chamadas assíncronas ao seu aplicativo.

Eu escrevi este simples App como uma prova de conceito:

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

Este aplicativo não possui uma interface de usuário. Ele escuta as conexões e faz uma solicitação PUT codificada para http://httpbin.org/put ( httpbin é um site de teste útil, mas observe que ele não suporta codificação em partes ). Os dados PUT (atualmente codificados para exatamente 17 octetos) são transmitidos do cliente (usando o mínimo possível de mensagens) e enviados para o servidor. A resposta do servidor é transmitida de volta ao cliente.

Esta é apenas uma prova de conceito. Um aplicativo real provavelmente deveria:

  • Conecte-se a qualquer host e porta.
  • Use Transfer-Encoding: em pedaços.
  • Sinalize o final dos dados de streaming.
  • Lidar com erros de soquete.
  • Suporte TLS (por exemplo, com Forge )

Aqui está uma página da web de amostra que executa um upload de stream contínuo (de 17 octetos) usando o App como um serviço (observe que você precisará configurar seu próprio ID de aplicativo):

 

Quando carrego esta página da web em um navegador Chrome com o aplicativo instalado, o httpbin retorna:

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

Atualmente estou procurando exatamente a mesma coisa (upstreaming via Ajax). O que eu encontrei atualmente, parece que estamos pesquisando no limite do design de resources do navegador 😉

A definição XMLHttpRequest informa na etapa 4 bodyinit que a extração de conteúdo disso é (ou pode ser) um readablestream .

Eu ainda estou procurando (como um não-webdeveloper) por informações de como criar tal coisa e para alimentar dados para o “outro lado” desse “readablestream” (que deveria ser um “writablestream”, mas eu ainda fiz não ache isso).

Talvez você seja melhor na pesquisa e poste aqui se encontrou um método para implementar esses planos de design.

^ 5
sven

Uma abordagem utilizando o ReadableStream para transmitir dados arbitrários; RTCDataChannel para enviar e, ou, receber dados arbitrários na forma de Uint8Array ; TextEncoder para criar 8000 bytes de dados randoms armazenados em um Uint8Array , TextDecoder para decodificar Uint8Array retornado por RTCDataChannel para string para apresentação, note que poderia alternativamente usar FileReader .readAsArrayBuffer e .readAsText aqui.

O html e o js foram modificados a partir dos exemplos na amostra MDN - WebRTC: Simple RTCDataChannel sample , incluindo o adapter.js que contém os auxiliares do RTCPeerConnection ; Criando seu próprio stream legível .

Observe também que o stream de exemplo é cancelado quando o total de bytes transferidos atinge 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

Você poderia usar Promise , setTimeout , recursion. Veja também PUT vs POST no 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 }); 

Eventos enviados pelo servidor e WebSockets são os methods preferidos, mas no seu caso você está querendo criar uma transferência de estado representacional, REST, API e usar Long Polling. Veja Como faço para implementar o “Long Polling” básico?

Longo processo de pesquisa é tratado no lado do cliente e no lado do servidor. O script do servidor e o servidor http devem ser configurados para suportar pesquisas longas.

Além de sondagem longa, sondagem curta (XHR / AJAX) requer que o navegador pesquise o servidor.