Способ потоковой передачи данных из браузера на сервер через HTTP
Существуют ли какие-либо API-интерфейсы браузера, подобные XHR, для потоковой передачи двоичного файла на сервер через 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);
});
В настоящее время у меня вместо этого есть решение для веб-сокетов, но я бы предпочел обычный HTTP для лучшего взаимодействия с некоторым существующим серверным кодом.
РЕДАКТИРОВАТЬ: я могу использовать передовые API браузера. Я смотрел на API Fetch, так как он поддерживает ArrayBuffers, DataViews, Files и тому подобное для тел запросов. Если бы я мог каким-то образом подделать один из этих объектов, чтобы я мог использовать API Fetch с динамическими данными, это сработало бы для меня. Я попытался создать объект Proxy, чтобы узнать, были ли вызваны какие-либо методы, которые я мог бы исправить. К сожалению, кажется, что браузер (по крайней мере, в Chrome) выполняет чтение в нативном коде, а не на земле JS. Но, пожалуйста, поправьте меня, если я ошибаюсь.
6 ответов
В настоящее время я ищу точно такую же вещь (апстрим через Ajax). То, что я нашел в настоящее время, выглядит так, как будто мы ищем передовой край дизайна функций браузера;-)
Определение XMLHttpRequest сообщает на шаге 4 bodyinit, что извлечение содержимого этого является (или может быть) потоком для чтения.
Я все еще ищу (как сторонний разработчик) информацию о том, как создать такую вещь и передать данные в "другой конец" этого "читаемого потока" (который должен быть "доступным для записи", но я еще не сделал не найти этого).
Может быть, вы лучше в поиске и можете опубликовать здесь, если вы нашли способ реализовать эти планы дизайна.
^ 5
Свен
Я не знаю, как это сделать с помощью чистых API-интерфейсов HTML5, но одно из возможных решений - использовать приложение Chrome в качестве фоновой службы для предоставления дополнительных функций веб-странице. Если вы уже готовы использовать браузеры для разработки и включить экспериментальные функции, это, похоже, лишь дополнительный шаг вперед.
Приложения Chrome могут вызывать chrome.sockets.tcp
API, на котором вы можете реализовать любой протокол, который вы хотите, включая HTTP и HTTPS. Это обеспечит гибкость для реализации потоковой передачи.
Обычная веб-страница может обмениваться сообщениями с приложением, используя chrome.runtime
API, если приложение декларирует это использование. Это позволит вашей веб-странице совершать асинхронные вызовы для вашего приложения.
Я написал это простое приложение в качестве доказательства концепции:
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);
});
}
}
});
});
Это приложение не имеет пользовательского интерфейса. Он слушает соединения и делает жестко запрошенный запрос PUT http://httpbin.org/put
( httpbin - полезный тестовый сайт, но обратите внимание, что он не поддерживает чанкованное кодирование). Данные PUT (в настоящее время жестко запрограммированы с точностью до 17 октетов) передаются от клиента (используя столько сообщений или столько сообщений, сколько необходимо) и отправляются на сервер. Ответ от сервера передается обратно клиенту.
Это просто доказательство концепции. Настоящее приложение должно, вероятно:
- Подключайтесь к любому хосту и порту.
- Использовать Transfer-Encoding: chunked.
- Сигнал об окончании потоковой передачи данных.
- Обрабатывать ошибки сокета.
- Поддержка TLS (например, с Forge)
Вот пример веб-страницы, которая выполняет потоковую загрузку (из 17 октетов) с использованием приложения в качестве службы (обратите внимание, что вам придется настроить свой собственный идентификатор приложения):
<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"
}
Подход, использующий ReadableStream
для потоковой передачи произвольных данных; RTCDataChannel
отправлять и / или получать произвольные данные в виде Uint8Array
; TextEncoder
создавать 8000
байты случайных данных, хранящихся в Uint8Array
, TextDecoder
расшифровать Uint8Array
вернулся RTCDataChannel
в строку для презентации, примечание может альтернативно использовать FileReader
.readAsArrayBuffer
а также .readAsText
Вот.
Разметка и код скрипта были изменены из примеров на MDN - WebRTC: Simple RTCDataChannel sample
, в том числе adapter.js
который содержит RTCPeerConnection
хелперы; Создание вашего собственного читаемого потока.
Обратите также внимание, что пример потока отменяется, когда общее количество переданных байтов достигает 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);
})();
Я думаю, что короткий ответ - нет. На момент написания этого ответа (ноябрь 2021 г.) он был недоступен ни в одном из основных браузеров.
Длинный ответ:
я думаю, что вы ищете в правильном месте с Fetch API. ReadableStream в настоящее время является допустимым типом для свойства 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 .
Вы могли бы использовать Promise
, setTimeout
рекурсия. Смотрите также PUT против POST в 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
});
Отправляемые сервером события и WebSockets являются предпочтительными методами, но в вашем случае вы хотите создать репрезентативную передачу состояния, REST, API и использовать длинный опрос. См. Как мне реализовать базовый "длинный опрос"?
Долгий процесс опроса обрабатывается как на стороне клиента, так и на стороне сервера. Серверный сценарий и http-сервер должны быть настроены для поддержки длительного опроса.
Помимо длинного опроса, короткий опрос (XHR/AJAX) требует браузера для опроса сервера.