首页 文章

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

提问于
浏览
8

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

我希望随着时间的推移制作HTTP PUT请求并以编程方式创建数据 . 我不想一次创建所有这些数据,因为它可能会存在于内存中 . 一些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套接字解决方案,但更喜欢常规HTTP以更好地与一些现有的服务器端代码互操作 .

EDIT: 我可以使用最前沿的浏览器API . 我正在查看Fetch API,因为它支持请求体的ArrayBuffers,DataViews,Files等 . 如果我能以某种方式伪造其中一个对象,以便我可以使用动态数据的Fetch API,这对我有用 . 我尝试创建一个Proxy对象,看看是否有任何方法被调用,我可以修补 . 不幸的是,似乎浏览器(至少在Chrome中)正在使用本机代码而不是JS版本进行读取 . 但是,如果我错了,请纠正我 .

5 回答

  • 0

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

    标记和脚本代码是在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

  • -3

    你可以使用 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
    });
    
  • 4

    Server-Sent EventsWebSockets是首选方法,但在您的情况下,您希望创建一个Representational状态转移,REST,API并使用长轮询 . 见How do I implement basic “Long Polling”?

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

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

  • 1

    我'm currently searching for exactly the same thing (upstreaming via Ajax). What I currently found, looks as if we are searching at the bleeding edge of browser'的功能设计;-)

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

    我仍在搜索(作为非web开发人员)以获取有关如何创建此类事物以及将数据提供到"readablestream"的"other end"(其应该是"writablestream",但我还没有找到)的信息 .

    也许你更善于搜索,如果你找到了实现这些设计方案的方法,可以在这里发帖 .

    ^ 5
    斯文

  • 4

    我不知道如何使用纯HTML5 API执行此操作,但一种可能的解决方法是使用Chrome应用作为后台服务来为网页提供其他功能 . 如果您已经愿意使用开发浏览器并启用实验性功能,那么这似乎只是一个渐进的步骤 .

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

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

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

    的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个八位字节)从客户端流入(根据需要使用少量或多个消息)并发送到服务器 . 来自服务器的响应将流回客户端 .

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

    • 连接到任何主机和端口 .

    • 使用Transfer-Encoding:chunked .

    • 表示流数据的结束 .

    • 处理套接字错误 .

    • 支持TLS(例如Forge

    这是一个示例网页,使用App作为服务执行流上传(17个八位字节)(请注意,您必须配置自己的App 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"
    }
    

相关问题