为了账号安全,请及时绑定邮箱和手机立即绑定

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

/ 猿问

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

当年话下 2019-11-19 15:26:40

是否有任何类似XHR的浏览器API可用于通过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);

});

我目前有一个Web套接字解决方案,但是我希望使用常规HTTP以便更好地与某些现有服务器端代码互操作。


编辑:我可以使用最新的浏览器API。我正在查看Fetch API,因为它支持请求主体的ArrayBuffers,DataViews,Files等。如果我能以某种方式伪造这些对象之一,以便可以将Fetch API与动态数据一起使用,那将对我有用。我尝试创建一个Proxy对象,以查看是否有任何方法可以调用猴子补丁。不幸的是,似乎浏览器(至少在Chrome中)使用本机代码而不是JS语言进行读取。但是,如果我做错了,请纠正我。


查看完整描述

3 回答

?
繁花如伊

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


Chrome Apps可以调用chrome.sockets.tcpAPI,您可以在该API上实现所需的任何协议,包括HTTP和HTTPS。这将提供实现流的灵活性。


常规网页可以使用chrome.runtimeAPI 与应用程序交换消息,只要该应用程序声明此用法即可。这将允许您的网页对应用程序进行异步调用。


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


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个八位字节)从客户端流式传输(使用所需的消息数量少或多),并发送到服务器。来自服务器的响应被流回客户端。


这仅仅是概念的证明。真正的应用程序可能应该:


连接到任何主机和端口。

使用传输编码:分块。

向流数据结束发出信号。

处理套接字错误。

支持TLS(例如,使用Forge)

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

当我在安装了应用程序的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"

}


查看完整回答
反对 回复 2019-11-19
?
慕斯王

我目前正在搜索完全相同的东西(通过Ajax上游)。我目前发现的结果似乎就像我们在浏览器功能设计的前沿一样;-)


XMLHttpRequest定义在步骤4中告诉bodyinit,其内容提取是(或可以是)可读流。


我仍在(以非Web开发人员的身份)搜索有关如何创建此类事物并将数据馈入该“可读流”(即应为“可写流”)的“另一端”的信息,但我确实做到了找不到)。


也许您会更好地进行搜索,如果找到了实现这些设计计划的方法,则可以在此处发布。


^ 5

斯文


查看完整回答
反对 回复 2019-11-19
?
白衣染霜花

一种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);

})();

plnkr http://plnkr.co/edit/cln6uxgMZwE2EQCfNXFO?p=preview


查看完整回答
反对 回复 2019-11-19

添加回答

回复

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信