javascript - Método para transmitir datos desde el navegador al servidor a través de HTTP
browser xmlhttprequest (5)
¿Hay alguna API de navegador similar a XHR disponible para transmitir binarios a un servidor a través de HTTP?
Quiero hacer una solicitud HTTP PUT y crear datos mediante programación, con el tiempo. No quiero crear todos estos datos a la vez, ya que podría haber algunos conciertos en la memoria. Algunos psueudocódigos para ilustrar a qué me refiero:
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);
});
Actualmente tengo una solución de socket web en su lugar, pero preferiría HTTP regular para una mejor interoperabilidad con algún código existente del lado del servidor.
EDITAR: puedo usar API de navegador de última generación. Estaba mirando la API Fetch, ya que es compatible con ArrayBuffers, DataViews, Files y similares para los cuerpos de solicitud. Si de alguna manera pudiera falsificar uno de estos objetos para poder usar la API Fetch con datos dinámicos, eso funcionaría para mí. Traté de crear un objeto Proxy para ver si se llamaba a algún método que pudiera mono parchear. Desafortunadamente, parece que el navegador (al menos en Chrome) está haciendo la lectura en código nativo y no en tierra JS. Pero, corrígeme si me equivoco en eso.
No sé cómo hacer esto con API HTML5 puras, pero una posible solución es usar una aplicación de Chrome como un servicio en segundo plano para proporcionar funciones adicionales a una página web. Si ya está dispuesto a usar navegadores de desarrollo y habilitar funciones experimentales, esto parece un paso incremental más allá de eso.
Chrome Apps puede llamar a la API
chrome.sockets.tcp
, en la que puede implementar cualquier protocolo que desee, incluidos HTTP y HTTPS.
Esto proporcionaría la flexibilidad para implementar la transmisión.
Una página web normal puede intercambiar mensajes con una aplicación utilizando la API
chrome.runtime
, siempre que la aplicación
declare este uso
.
Esto permitiría que su página web realice llamadas asincrónicas a su aplicación.
Escribí esta aplicación simple como prueba de concepto:
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);
});
}
}
});
});
Esta aplicación no tiene una interfaz de usuario.
Escucha las conexiones y realiza una solicitud PUT codificada a
http://httpbin.org/put
(
httpbin
es un sitio de prueba útil, pero tenga en cuenta que
no admite codificación fragmentada
).
Los datos PUT (actualmente codificados exactamente a 17 octetos) se transmiten desde el cliente (utilizando la menor cantidad de mensajes que se desee) y se envían al servidor.
La respuesta del servidor se devuelve al cliente.
Esto es solo una prueba de concepto. Una aplicación real probablemente debería:
- Conéctese a cualquier host y puerto.
- Use Transfer-Encoding: fragmentado.
- Señale el final de la transmisión de datos.
- Manejar errores de socket.
- Soporte TLS (por ejemplo, con Forge )
Aquí hay una página web de muestra que realiza una carga de transmisión (de 17 octetos) utilizando la aplicación como servicio (tenga en cuenta que tendrá que configurar su propia identificación de aplicación):
<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>
Cuando cargo esta página web en un navegador Chrome con la aplicación instalada, httpbin devuelve:
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"
}
Puede usar
Promise
,
setTimeout
, recursión.
Ver también
PUT vs POST en 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
});
Un enfoque que utiliza
ReadableStream
para transmitir datos arbitrarios;
RTCDataChannel
para enviar y / o recibir datos arbitrarios en forma de
Uint8Array
;
TextEncoder
para crear
8000
bytes de datos aleatorios almacenados en un
Uint8Array
,
TextDecoder
para decodificar
Uint8Array
devuelto por
RTCDataChannel
a la cadena para la presentación, la nota podría usar alternativamente
FileReader
.readAsArrayBuffer
y
.readAsText
aquí.
El marcado y el código del script se modificaron a partir de ejemplos en
MDN - WebRTC: Simple RTCDataChannel sample
, incluido
adapter.js
que contiene ayudantes de
RTCPeerConnection
;
Creando tu propia secuencia legible
.
Tenga en cuenta también que la secuencia de ejemplo se cancela cuando el total de bytes transferidos alcanza
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);
})();
Los eventos enviados por el servidor y los WebSockets son los métodos preferidos, pero en su caso desea crear una transferencia de estado representativa, REST, API y utilizar Long Polling. Consulte ¿Cómo implemento el "Sondeo largo" básico?
El proceso de sondeo largo se maneja tanto del lado del cliente como del lado del servidor. El script del servidor y el servidor http deben configurarse para admitir sondeos largos.
Además del sondeo largo, el sondeo corto (XHR / AJAX) requiere que el navegador realice una encuesta al servidor.
Actualmente estoy buscando exactamente lo mismo (corriente arriba a través de Ajax).
Lo que encontré actualmente, parece como si estuviéramos buscando en el borde del diseño de funciones del navegador ;-)
La definición XMLHttpRequest
indica en el paso 4
bodyinit
que la extracción de contenido de este es (o puede ser) un
readablestream
.
Todavía estoy buscando (como no desarrollador web) información sobre cómo crear tal cosa y alimentar los datos en el "otro extremo" de ese "flujo legible" (que debería ser un "flujo de escritura", pero todavía lo hice no encontrar eso).
Quizás sea mejor en la búsqueda y pueda publicar aquí si encuentra un método para implementar estos planes de diseño.
^ 5
sven